View on GitHub

FOnline Engine

Flexible cross-platform isometric game engine

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:

msize provides checked/clamped helpers:

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:

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:

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:

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:

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:

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:

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:

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:

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:

MovingContext stores:

Key operations:

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:

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:

Change routing

Validation checklist

  1. Run movement/path/map-loader tests relevant to the changed algorithm.
  2. Test both direct and multihex movement if blocker logic changes.
  3. Validate FO_GEOMETRY assumptions when changing directions/projection.
  4. Validate map loading with real baked map resources from an embedding project when parser behavior changes.
  5. If movement state is replicated, validate network and client/server behavior; update Networking.md if packet/property flow changes.
  6. If script-visible movement APIs change, update GeneratedApiAndMetadata.md and Nullability.md if signatures/nullability changed.