Add grid primitives and data-backed maps #17

Merged
icub3d merged 1 commit from 2-grid-and-map-primitives into main 2026-08-06 22:40:13 +00:00
Owner

Stands up src/world/ and the spatial types M1 builds on.

Coordinates

Two types, deliberately. Footprint is a column of the map seen from above; GridPos
is a cell in space, footprint plus height. Conflating them is the classic 3D-tactics bug
(x, y) quietly meaning (x, z) — and it is cheaper to let the compiler reject it
than to find it later. Map::tile has a test named after exactly that failure.

Neighbour order (east, south, west, north) is documented as part of the contract rather
than an implementation detail: pathfinding breaks ties by visit order, so changing it
changes which of two equal-cost paths a unit walks.

Maps

Authored as rows of glyphs against a legend, not as tile records — a tactical map is a
picture, and a format you can read as one shows a misplaced wall or a walled-in spawn at
a glance. Follows src/character/ throughout: authored *Def, validated type, plugin
that drives loading, RonAsset with its own *.map.ron extension.

Validation names the thing to fix — the glyph, the row, the column — because the file is
hand-written and "invalid map" alone is useless. Ragged rows, unknown glyphs, duplicate
legend entries, spawns out of bounds, spawns on walls, two spawns in one column, and a
side with nowhere to start all fail at load with the offending location.

Two things worth reviewing

Placement differs from the issue. The issue said to put the spatial types in
src/world/, but CLAUDE.md's layout assigns "grid/space primitives" to core/ and
"map loading" to world/. Canon won: GridPos/Footprint are in core/.

Loading needed a gate, which the issue did not scope. With two registries loading
during Loading, whichever finished first set MainMenu while the other was still
reading — the race the existing TODO(data) anticipated. Subsystems now declare
themselves at plugin-build time and report in when validated, and LoadingSet orders
reporting before the check. The gate's test builds an app with both real subsystems and
asserts the menu never opens with either missing; I verified it fails when the gate is
short-circuited, rather than passing vacuously.

Verification

45 tests pass (was 14), clippy --all-targets -D warnings and fmt --check clean.

No display here, so nothing visual is claimed — nothing renders yet. That is #3, which
will draw Map::footprints().

Closes #2

Stands up `src/world/` and the spatial types M1 builds on. ## Coordinates Two types, deliberately. `Footprint` is a column of the map seen from above; `GridPos` is a cell in space, footprint plus height. Conflating them is the classic 3D-tactics bug — `(x, y)` quietly meaning `(x, z)` — and it is cheaper to let the compiler reject it than to find it later. `Map::tile` has a test named after exactly that failure. Neighbour order (east, south, west, north) is documented as part of the contract rather than an implementation detail: pathfinding breaks ties by visit order, so changing it changes which of two equal-cost paths a unit walks. ## Maps Authored as rows of glyphs against a legend, not as tile records — a tactical map is a picture, and a format you can read as one shows a misplaced wall or a walled-in spawn at a glance. Follows `src/character/` throughout: authored `*Def`, validated type, plugin that drives loading, `RonAsset` with its own `*.map.ron` extension. Validation names the thing to fix — the glyph, the row, the column — because the file is hand-written and "invalid map" alone is useless. Ragged rows, unknown glyphs, duplicate legend entries, spawns out of bounds, spawns on walls, two spawns in one column, and a side with nowhere to start all fail at load with the offending location. ## Two things worth reviewing **Placement differs from the issue.** The issue said to put the spatial types in `src/world/`, but `CLAUDE.md`'s layout assigns "grid/space primitives" to `core/` and "map loading" to `world/`. Canon won: `GridPos`/`Footprint` are in `core/`. **Loading needed a gate, which the issue did not scope.** With two registries loading during `Loading`, whichever finished first set `MainMenu` while the other was still reading — the race the existing `TODO(data)` anticipated. Subsystems now declare themselves at plugin-build time and report in when validated, and `LoadingSet` orders reporting before the check. The gate's test builds an app with both real subsystems and asserts the menu never opens with either missing; I verified it fails when the gate is short-circuited, rather than passing vacuously. ## Verification 45 tests pass (was 14), `clippy --all-targets -D warnings` and `fmt --check` clean. No display here, so nothing visual is claimed — nothing renders yet. That is #3, which will draw `Map::footprints()`. Closes #2
Two coordinate types, not one. A Footprint is a column seen from above —
what a map authors and what pathfinding will walk. A GridPos is a cell in
space. Conflating them is the classic 3D-tactics bug where (x, y) quietly
means (x, z), and it is much cheaper to let the compiler reject it.

Maps are authored as rows of glyphs against a legend rather than as tile
records. A tactical map is a picture, and a format you can read as one shows
a misplaced wall or a walled-in spawn at a glance. Validation names the thing
to go and fix — a glyph, a row, a column — because "invalid map" alone is
useless when the file is hand-written.

Placement follows CLAUDE.md's layout rather than the issue text: grid/space
primitives are core's, map loading is world's.

Loading needed a gate. Two registries now load during GameState::Loading, and
whichever finished first was setting MainMenu while the other was still
reading — the race the TODO(data) in character/mod.rs anticipated. Subsystems
now declare themselves at plugin-build time and report in when validated, and
LoadingSet orders reporting before the check so the gate opens in the same
frame rather than a frame late.

Closes #2
icub3d merged commit f92e28d766 into main 2026-08-06 22:40:13 +00:00
icub3d deleted branch 2-grid-and-map-primitives 2026-08-06 22:40:13 +00:00
Sign in to join this conversation.
No description provided.