Maps, Movement, and Geometry
This document explains the reusable map-coordinate, movement, path-finding, line-tracing, and map-loading primitives used by client/server runtime and tools.
Use it when changing Source/Common/Geometry.*, LineTracer.*, Movement.*, PathFinding.*, MapLoader.*, map baker behavior, or map/movement tests.
Ownership model
The engine owns coordinate types, path algorithms, movement interpolation, and map-file loading mechanics. An embedding game project owns concrete maps, blocker layout, encounter rules, and gameplay decisions around movement.
Keep project-specific map content and quest/navigation rules outside this document.
Coordinate and direction types
Source/Common/Geometry.h defines the exported value types used across runtime, generated API, and scripts:
mpos— map hex/tile position stored asint16 x+int16 y.msize— map dimensions stored asint16 width+int16 height.hdir— discrete hex/tile direction.mdir— movement direction angle; convertible to/from hex direction.
msize provides checked/clamped helpers:
clamp_pos()clamps raw coordinates into map bounds;from_raw_pos()asserts that raw coordinates are already in bounds.
Do not replace these types with generic ipos/isize in public map APIs without checking generated metadata and script-visible value layouts.
Sub-hex offset convention
Sub-hex pixel offsets are measured relative to the hex visual center, bounded by
±{MAP_HEX_WIDTH/2, MAP_HEX_HEIGHT/2}. This single convention is used by critter HexOffset,
MovingContext start/end offsets, move-target offsets (MoveToHex), transparent-egg offsets, and the
value returned by MapView::GetHexAtScreen / Client_Map_GetHexAtScreenPos. A click/cursor offset is
therefore directly usable as a move or critter offset with no conversion. Keep new offset
producers/consumers on this convention.
Implementation note — there are two anchor points that must not be confused:
GeometryHelper::GetHexPos/MapView::GetHexMapPos(and the per-FieldOffsetgrid) return the hex cell top-left draw origin, stepping by fullMAP_HEX_WIDTH/MAP_HEX_LINE_HEIGHT.- Sprites (critters, items) anchor at the hex visual center = top-left
+ {MAP_HEX_WIDTH/2, MAP_HEX_HEIGHT/2}(seeHexView::AddSprite), andMapView::GetHexScreenPosreturns that same visual center (where a centered critter’s feet render).
Because the visual center is half a hex past the cell origin, any code that resolves or applies a
center-relative offset against GetHexMapPos must add {MAP_HEX_WIDTH/2, MAP_HEX_HEIGHT/2}:
GetHexAtScreen biases its lookup point by -{half hex} so GetHexPosCoord snaps to the hex whose
visual center is nearest and the returned offset is already center-relative and in range (no clamping);
UpdateTransparentEgg and the critter-based SetTransparentEgg add/subtract the same half-hex so a
stored center-relative egg offset renders at the intended point. GetHexScreenPos(hex) + offset
projects a (hex, center-relative offset) pair straight to screen with no further bias.
Geometry modes
hdir is compiled differently depending on FO_GEOMETRY:
FO_GEOMETRY == 1— six directions: north-east, east, south-east, south-west, west, north-west.FO_GEOMETRY == 2— eight directions: the six above plus south and north.
GameSettings::MAP_DIR_COUNT participates in direction normalization. When changing geometry, inspect compile-time geometry settings, generated value types, path-finding tests, and any rendering code that projects map positions.
Geometry helper responsibilities
GeometryHelper is a static utility class. It owns coordinate projection and directional math such as:
- map hex/tile to projected screen/map coordinate conversion:
GetHexPos(),GetHexPosCoord(),GetHexOffset(); - hex/offset canonicalization:
NormalizeHexOffset()rewrites a projected point to the nearest in-bounds hex plus a small local offset; - axial-coordinate helpers:
GetHexAxialCoord(),GetAxialHexes(); - distance and direction:
GetDistance(),GetHexDir(),GetDirAngle(), angle-difference helpers; - radius and line/circle checks:
HexesInRadius(),IntersectCircleLine(); - movement stepping:
MoveHexByDir(),MoveHexByDirUnsafe(),MoveHexAroundAway(); - multihex traversal helpers:
ForEachMultihexLines().
Safe helpers check msize bounds; unsafe helpers are for internal algorithms that already proved bounds.
Path finding
Source/Common/PathFinding.h exposes the core path-finding API:
FindPathInputFindPathOutputTraceLineInputTraceLineOutputPathFinding::CheckHexWithMultihex()PathFinding::FindPath()PathFinding::EvaluateFreeMovementEndOffset()PathFinding::TraceLine()
FindPathInput is deliberately callback-driven. Runtime code supplies CheckHex(mpos) so map ownership, blockers, critters, gag items, and game-specific blocking policy stay outside the generic algorithm.
Important FindPathInput fields:
FromHex/ToHex— requested route endpoints.ToHexOffset— the target’s real sub-hex offset withinToHex(the continuous target position isToHexcenter +ToHexOffset). Used only by theFreeMovementend-offset computation.MapSize— bounds for all checks.MaxLength— maximum BFS depth, normally derived from engine settings.Cut— stop when route is within this distance of target;0requires exact target.Multihex— radius for multihex actors.FreeMovement— enables the line-tracer optimization for control steps and the continuous sub-hex end offset (see below).CheckHex— callback returning block/defer status.
FindPathOutput returns a result, direction steps, control steps, the (possibly cut-adjusted) NewToHex, and EndHexOffset (concrete ipos16, zero when FreeMovement is off).
FreeMovement end offset
When FreeMovement is set, the route is still cut to whole hexes by the BFS, but the final
standing position is refined to a sub-hex point instead of snapping to NewToHex center.
PathFinding::EvaluateFreeMovementEndOffset() computes EndHexOffset (relative to NewToHex
center) so the continuous end position sits exactly at the cut gap
R = dist(NewToHex center, ToHex center) from the target’s real position
(ToHex center + ToHexOffset), on the NewToHex side of it. Distances use the camera Y
projection (GeometryHelper::GetYProj()), matching the metric MovingContext uses for segment
distances. Behavior:
EndHexOffsetis a plainipos16: whenFreeMovementis off (or the request short-circuits before the FreeMovement pass) it stays at its zero default, meaning the mover stops at the reached hex center.- A centered target reached short of its hex (
ToHexOffset == 0,Cut > 0) yieldsEndHexOffset == 0(hex center). Cut == 0onto an off-center target yieldsEndHexOffset == ToHexOffset(stop exactly on the target point).- The offset magnitude is bounded by
|ToHexOffset|and clamped to half a hex. - Degenerate stop direction. Internally
EvaluateFreeMovementEndOffsetreturnsoptional<ipos16>and yieldsnulloptwhen the real target essentially coincides with the reached hex center (e.g.Cut == 0onto a centered target, orCut > 0with a target offset that exactly cancels the inter-hex vector). In that caseFindPathsubstitutesFindPathInput::FromHexOffset— the mover’s own current sub-hex offset — so the mover stays where it already is instead of being snapped to the reached hex center.MapView::FindPathandMapManager::FindPathfillFromHexOffsetfrom the supplied critter automatically.
Callers supply ToHexOffset and consume EndHexOffset directly: on the client through
MapView::FindPath plus the cut-aware Critter.MoveToHex(hex, cut, hexOffset, speed) script
overload; on the server through MapManager::FindPath plus
Critter.MoveToHex(hex, cut, endHexOffset, speed). The resolved offset travels in the existing
SendCritterMove end-offset field (a concrete ipos16), so client prediction and server
authority stop at the same continuous point and there is no protocol change.
Blocking model
HexBlockResult expresses path-finding priority:
Passable— hex can be used.Blocked— permanent blocker.DeferGag— blocked by a gag item; route through only after distance gap.DeferCritter— blocked by a critter; route through as last resort.
For multihex actors, CheckHexWithMultihex() checks the directional front arc and returns the worst blocker result across checked hexes.
When gameplay code changes blocker semantics, update the callback provider and tests; do not bake game-specific blocking rules into the generic path algorithm.
Line tracing
TraceLineInput describes a trace from StartHex toward TargetHex:
MaxDistlimits trace length;0means use start/target distance.Anglecan override/drive direction.CheckLastMovableasks the trace to report the last movable hex.IsHexBlockedstops the trace.IsHexMovableoptionally records move-valid candidates.
TraceLineOutput reports whether the trace was full, whether a last movable hex exists, the pre-block hex, block hex, and last movable hex.
Line tracing is used by movement/path logic and by gameplay systems that need visibility, shooting, or straight-line movement checks.
Movement contexts
Source/Common/Movement.h defines movement state and interpolation:
MovingState— completion/error reason.MovingMetrics— end hex, whole time, whole distance.MovingProgress— current hex, offset, direction, completion flag.MovingRawProgress— internal segment/progress data.MovingContext— ref-counted movement plan and runtime evaluator.
MovingContext stores:
- map size;
- speed;
- direction steps;
- control steps;
- start time and offset time;
- start/end hex and offsets;
- computed whole time/distance;
- completion state and block/pre-block hexes.
Key operations:
- evaluate:
EvaluateMetrics(),EvaluateProjectedHex(),EvaluateNearestPathHex(),EvaluatePathHexes(),EvaluateProgress(); - advance time:
UpdateCurrentTime(),UpdateCurrentTimeToNextHex(); - mutate runtime state:
ChangeSpeed(),Complete(),SetBlockHexes(); - sanity check:
ValidateRuntimeState().
Movement is therefore a reusable time-based plan, not just a list of positions. Client prediction, server correction, and script-visible movement data should all preserve that distinction.
Server and client runtime processing keep MovingContext active regardless of CritterCondition. Game scripts own condition-based movement permissions, so a game can represent knockout falls, dead-body slides, or custom state movement while still relying on the same path, offset, and completion state machinery. Attached critters are still stopped by runtime processing because attachment is a transport/ownership relationship rather than a critter condition.
Map loading
Source/Common/MapLoader.h exposes MapLoader::Load():
static void Load(
string_view name,
const string& buf,
const EngineMetadata& meta,
HashResolver& hash_resolver,
const CrLoadFunc& cr_load,
const ItemLoadFunc& item_load);
The loader parses a map buffer and calls engine/runtime-provided callbacks for critters and items:
CrLoadFunc(ident_t id, const ProtoCritter* proto, const map<string_view, string_view>& kv)ItemLoadFunc(ident_t id, const ProtoItem* proto, const map<string_view, string_view>& kv)
This keeps file parsing generic while letting server/tools decide how loaded critters/items become live entities or editor objects.
Map resource production is adjacent to baking. For MapBaker, see BakingPipeline.md.
Tests to inspect
Relevant tests include:
Source/Tests/Test_Movement.cppSource/Tests/Test_MapLoader.cppSource/Tests/Test_MapBaker.cppSource/Tests/Test_ServerMapOperations.cpp- geometry/path/line-tracing tests when present in the checkout.
Change routing
- Coordinate/value-type changes:
Source/Common/Geometry.*and generated metadata docs. - Line tracing:
Source/Common/LineTracer.*andSource/Common/PathFinding.*. - BFS/path blocking behavior:
Source/Common/PathFinding.*plus caller-provided blocker callbacks. - Movement interpolation/state:
Source/Common/Movement.*. - Map file parsing:
Source/Common/MapLoader.*. - Map resource baking:
Source/Tools/MapBaker.*and BakingPipeline.md. - Runtime map entity behavior: ServerRuntime.md, ClientRuntime.md, and EntityModel.md.
Validation checklist
- Run movement/path/map-loader tests relevant to the changed algorithm.
- Test both direct and multihex movement if blocker logic changes.
- Validate
FO_GEOMETRYassumptions when changing directions/projection. - Validate map loading with real baked map resources from an embedding project when parser behavior changes.
- If movement state is replicated, validate network and client/server behavior; update Networking.md if packet/property flow changes.
- If script-visible movement APIs change, update GeneratedApiAndMetadata.md and Nullability.md if signatures/nullability changed.