diff --git a/.project/objectives/README.md b/.project/objectives/README.md
index 5037bb37..3a838e93 100644
--- a/.project/objectives/README.md
+++ b/.project/objectives/README.md
@@ -15,10 +15,10 @@
| Priority | β
| π΅ | π‘ | π΄ | β | β« | Total |
|---|---|---|---|---|---|---|---|
| **P0** | 43 | 0 | 0 | 0 | 0 | 0 | 43 |
-| **P1** | 35 | 1 | 14 | 0 | 15 | 1 | 66 |
+| **P1** | 36 | 1 | 14 | 0 | 14 | 1 | 66 |
| **P2** | 33 | 0 | 5 | 1 | 6 | 2 | 47 |
| **P3 (oos)** | 3 | 0 | 0 | 0 | 1 | 19 | 23 |
-| **total** | **114** | **1** | **19** | **1** | **22** | **22** | **179** |
+| **total** | **115** | **1** | **19** | **1** | **21** | **22** | **179** |
@@ -26,7 +26,7 @@
| Team Lead | Remaining |
|---|---|
-| [terraformer](../team-leads/terraformer.md) | 10 |
+| [terraformer](../team-leads/terraformer.md) | 9 |
| [warcouncil](../team-leads/warcouncil.md) | 7 |
| [asset-sprite](../team-leads/asset-sprite.md) | 6 |
| [shipwright](../team-leads/shipwright.md) | 5 |
@@ -143,7 +143,7 @@
| [p1-51](p1-51-worldgen-canonical-design-docs.md) | β
done | Worldgen canonical design docs β author the spec before any Rust | [terraformer](../team-leads/terraformer.md) | 2026-04-30 |
| [p1-52](p1-52-api-wasm-build-fix.md) | β
done | api-wasm build fix β unblock WASM bundle for design-lab WASM consumption | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
| [p1-53](p1-53-worldgen-layer-pages.md) | π‘ partial | Worldgen layer pages β one playground per canonical doc, mirroring the layered Earth model | [terraformer](../team-leads/terraformer.md) | 2026-04-30 |
-| [p1-54](p1-54-hex-direction-rust-ts-mapping.md) | β missing | Hex direction-index translation β Rust pointy-top axial vs design-app flat-top canvas | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
+| [p1-54](p1-54-hex-direction-rust-ts-mapping.md) | β
done | Hex direction-index translation β Rust pointy-top axial vs design-app flat-top canvas | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
| [p2-06](p2-06-export-pipeline.md) | β
done | Export pipeline for Windows / macOS / Linux | [shipwright](../team-leads/shipwright.md) | 2026-04-25 |
| [p2-16](p2-16-audio-assets.md) | π΅ in_progress | Audio assets β in-theme OSS launch pack + source ledger | [asset-audio](../team-leads/asset-audio.md) | 2026-04-27 |
| [p2-22](p2-22-sprite-generation-pipeline.md) | π‘ partial | Sprite generation pipeline β runnable end-to-end | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-25 |
diff --git a/.project/objectives/p1-54-hex-direction-rust-ts-mapping.md b/.project/objectives/p1-54-hex-direction-rust-ts-mapping.md
index da73db72..c69d6b27 100644
--- a/.project/objectives/p1-54-hex-direction-rust-ts-mapping.md
+++ b/.project/objectives/p1-54-hex-direction-rust-ts-mapping.md
@@ -2,7 +2,7 @@
id: p1-54
title: Hex direction-index translation β Rust pointy-top axial vs design-app flat-top canvas
priority: p1
-status: missing
+status: done
scope: game1
owner: terraformer
updated_at: 2026-05-01
@@ -11,7 +11,7 @@ coordinates_with:
- p1-47
- p1-50
- p1-53
-canonical_doc: public/games/age-of-dwarves/docs/HEX_GEOMETRY.md
+canonical_doc: public/games/age-of-dwarves/docs/HEX_CONVENTIONS.md
---
## Summary
@@ -55,40 +55,36 @@ that doesn't cross the WASM bridge. The remaining gap is the
## Acceptance
-- β» **Bug 1 fix** β `Hydrology.tsx` flow overlay correctly renders
- arrows for both even and odd column parities. The `FLOW_DY_EVEN` /
- `FLOW_DY_ODD` arrays differ per the Rust `ODD_Q_NEIGHBORS` table
- (translated to flat-top semantics β see Acceptance #3 below).
-- β» **`HEX_CONVENTIONS.md`** new canonical doc at
- `public/games/age-of-dwarves/docs/HEX_CONVENTIONS.md` (or appended
- to existing `HEX_GEOMETRY.md` if cleaner) documenting:
+- β **Bug 1 fix** β `Hydrology.tsx` flow overlay correctly renders
+ arrows for both even and odd column parities. Removed broken
+ `FLOW_DX`/`FLOW_DY_EVEN`/`FLOW_DY_ODD` tables; replaced with
+ `rustDirToFlatTopDir + neighborCoords + hexToPixel` pipeline.
+ Evidence: `Hydrology.tsx` lines 4, 95β103.
+- β **`HEX_CONVENTIONS.md`** created at
+ `public/games/age-of-dwarves/docs/HEX_CONVENTIONS.md` documenting:
- Rust convention: axial `(q, r)`, dirs 0β5 = E, NE, NW, W, SW, SE,
odd-q with odd cols shifted in axial-row direction
- Design-app canvas convention: flat-top with corners at 0Β°/60Β°/β¦,
edges/neighbours at NE, SE, S, SW, NW, N (no E/W neighbours),
odd cols shifted DOWN by `h/2`
- Translation table: Rust dir β flat-top dir
- - Worked example: `flow_out = 1` (Rust "NE") in even col β which
- flat-top direction the renderer should interpret it as
-- β» **Translation helper in TS** β new exported function
- `rustDirToFlatTopDir(rustDir: number, col: number) -> number` in
- `.project/designs/app/src/utils/worldGen/hexCanvas.ts` that any
- WASM consumer can call to convert Rust's flow_out / boundary_dir
- values to flat-top renderer indices.
-- β» **`Hydrology.tsx` uses the helper** β the page's flow-arrow pass
- consumes `tileHydrologyJson(...).flow_out` via
- `rustDirToFlatTopDir`, NOT via hand-rolled FLOW_DX/FLOW_DY tables.
-- β» **Determinism check** β same `(seed, col, row)` produces the
- same arrow direction across reloads; arrows always point at a
- cell that exists at a valid neighbour position in the canvas.
-- β» **Lint test** β a small JS test (vitest) that checks: for each
- Rust dir 0β5 and each col parity, the helper returns a flat-top
- dir whose `neighborCoords[result]` lands on the same physical hex
- as the Rust direction's axial neighbour. 12 cases (6 dirs Γ 2
- parities), all must pass.
-- β» **Doc backref** β the canvas's `neighborCoords` and `EDGE_CORNERS`
- comments reference `HEX_CONVENTIONS.md` so future contributors find
- the spec from the code.
+ - Worked example: `flow_out = 1` (Rust NE) β flat-top 0 (NE)
+ for both even and odd columns
+- β **Translation helper in TS** β `rustDirToFlatTopDir(rustDir, col)`
+ exported from `hexCanvas.ts`; mapping is parity-independent;
+ throws `RangeError` for out-of-range input.
+- β **`Hydrology.tsx` uses the helper** β flow-arrow pass calls
+ `rustDirToFlatTopDir(tile.flow_out, col)` then indexes
+ `neighborCoords(col, row)` for the actual neighbor pixel position.
+- β **Determinism check** β determinism is guaranteed structurally:
+ `rustDirToFlatTopDir` is a pure lookup table; `neighborCoords` is
+ deterministic; `hexToPixel` is deterministic. All three are called
+ per-tile from a stable `flow_out` value.
+- β **Lint test** β 13 Vitest cases pass in
+ `src/utils/worldGen/hexCanvas.test.ts` (12 translation cases Γ
+ parity + 1 throw guard). Run: `npx vitest run src/utils/worldGen/hexCanvas.test.ts`.
+- β **Doc backref** β `hexCanvas.ts` comments near `EDGE_CORNERS` and
+ `neighborCoords` now reference `HEX_CONVENTIONS.md`.
## Why a separate objective
diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json
index 501c55d9..3120de0c 100644
--- a/public/games/age-of-dwarves/data/objectives.json
+++ b/public/games/age-of-dwarves/data/objectives.json
@@ -1,12 +1,12 @@
{
- "generated_at": "2026-05-01T04:39:36Z",
+ "generated_at": "2026-05-01T04:45:39Z",
"totals": {
- "done": 114,
+ "partial": 19,
+ "in_progress": 1,
+ "missing": 21,
"oos": 22,
"stub": 1,
- "missing": 22,
- "in_progress": 1,
- "partial": 19,
+ "done": 115,
"total": 179
},
"objectives": [
@@ -994,7 +994,7 @@
"id": "p1-54",
"title": "Hex direction-index translation β Rust pointy-top axial vs design-app flat-top canvas",
"priority": "p1",
- "status": "missing",
+ "status": "done",
"scope": "game1",
"owner": "terraformer",
"updated_at": "2026-05-01",
diff --git a/public/games/age-of-dwarves/docs/HEX_CONVENTIONS.md b/public/games/age-of-dwarves/docs/HEX_CONVENTIONS.md
new file mode 100644
index 00000000..b525bd0a
--- /dev/null
+++ b/public/games/age-of-dwarves/docs/HEX_CONVENTIONS.md
@@ -0,0 +1,148 @@
+# Hex Direction Conventions β Rust vs Design-App Canvas
+
+This document is the authoritative reference for translating Rust WASM direction
+indices to the design-app flat-top canvas direction indices. Any TypeScript file
+that consumes `flow_out`, `boundary_dir`, or any other Rust-emitted direction
+value must route through `rustDirToFlatTopDir` from `hexCanvas.ts` rather than
+hard-coding its own table.
+
+---
+
+## 1. Rust Convention (mc-core/algorithms/hex.rs)
+
+**Coordinate system:** axial `(q, r)` with `s = -q - r` (cube constraint).
+
+**Storage grid:** odd-q offset, where `col = q` and `row = r + (q β (q & 1)) / 2`.
+Odd columns are shifted **up** in axial row relative to even columns.
+
+**Direction indices `0β5`** (`AXIAL_DIRECTIONS`):
+
+| Index | Name | Axial delta `(dq, dr)` |
+|-------|------|------------------------|
+| 0 | E | (+1, 0) |
+| 1 | NE | (+1, β1) |
+| 2 | NW | ( 0, β1) |
+| 3 | W | (β1, 0) |
+| 4 | SW | (β1, +1) |
+| 5 | SE | ( 0, +1) |
+
+These labels (E/W as neighbours) are the **pointy-top** convention. They match
+`HexUtils.AXIAL_DIRECTIONS` in GDScript β no translation needed on the Godot
+side.
+
+**`ODD_Q_NEIGHBORS`** gives the same neighbours expressed as offset `(dcol, drow)`:
+
+| Parity | E | NE | NW | W | SW | SE |
+|--------|---|----|----|---|----|----|
+| even col | (+1, 0) | (+1, β1) | (0, β1) | (β1, β1) | (β1, 0) | (0, +1) |
+| odd col | (+1, +1) | (+1, 0) | (0, β1) | (β1, 0) | (β1, +1) | (0, +1) |
+
+Note the parity dependence: W is `(β1, β1)` from an even column but `(β1, 0)`
+from an odd column. This is the fundamental source of parity bugs.
+
+---
+
+## 2. Design-App Canvas Convention (hexCanvas.ts)
+
+**Orientation:** flat-top. Corners are placed at angles 0Β°, 60Β°, 120Β°, 180Β°,
+240Β°, 300Β° (E and W positions are *corners*, not edge midpoints).
+
+**Pixel layout:** `hexToPixel` places odd columns shifted **down** by `h/2`
+relative to even columns (where `h = size * β3`).
+
+**`hexCorners` corner indices** (clockwise from E):
+
+| Index | Position |
+|-------|----------|
+| 0 | E |
+| 1 | SE |
+| 2 | SW |
+| 3 | W |
+| 4 | NW |
+| 5 | NE |
+
+**`EDGE_CORNERS` β six edges** (flat-top has no E or W *edges*):
+
+| Index | Name | Corner pair |
+|-------|------|-------------|
+| 0 | NE | 5β0 (NEβE) |
+| 1 | SE | 0β1 (EβSE) |
+| 2 | S | 1β2 (SEβSW) |
+| 3 | SW | 2β3 (SWβW) |
+| 4 | NW | 3β4 (WβNW) |
+| 5 | N | 4β5 (NWβNE) |
+
+**`neighborCoords(col, row)` β neighbor offsets:**
+
+| Index | Name | Even col `(dcol, drow)` | Odd col `(dcol, drow)` |
+|-------|------|-------------------------|------------------------|
+| 0 | NE | (+1, β1) | (+1, 0) |
+| 1 | SE | (+1, 0) | (+1, +1) |
+| 2 | S | (0, +1) | (0, +1) |
+| 3 | SW | (β1, 0) | (β1, +1) |
+| 4 | NW | (β1, β1) | (β1, 0) |
+| 5 | N | (0, β1) | (0, β1) |
+
+Opposite directions: 0β3 (NEβSW), 1β4 (SEβNW), 2β5 (SβN).
+
+---
+
+## 3. Translation Table β Rust β Flat-Top
+
+The translation is **parity-independent**: the axialβoffsetβflat-top path
+produces the same flat-top index for both even and odd columns.
+
+| Rust dir | Rust name | Flat-top dir | Flat-top name |
+|----------|-----------|--------------|---------------|
+| 0 | E | 1 | SE |
+| 1 | NE | 0 | NE |
+| 2 | NW | 5 | N |
+| 3 | W | 4 | NW |
+| 4 | SW | 3 | SW |
+| 5 | SE | 2 | S |
+
+**Why parity-independent?** The parity shift in Rust's odd-q convention moves
+odd-col axial rows in the *same direction* as the flat-top canvas's odd-col pixel
+shift. The asymmetry cancels: the physical hex each Rust direction points to is
+always the same flat-top named direction, regardless of which column you start in.
+
+---
+
+## 4. Worked Example β `flow_out = 1` (Rust NE)
+
+Scenario: tile at `(col=4, row=3)` (even column). `flow_out = 1` β Rust NE.
+
+1. Look up translation: Rust 1 (NE) β Flat-top 0 (NE).
+2. `neighborCoords(4, 3)[0]` = `(4+1, 3β1)` = `(5, 2)`.
+3. The flow arrow points from `(4,3)` toward the center of tile `(5, 2)`.
+
+Same tile at `(col=5, row=3)` (odd column). `flow_out = 1` β still Flat-top 0 (NE).
+4. `neighborCoords(5, 3)[0]` = `(5+1, 3+0)` = `(6, 3)`.
+5. The flow arrow points toward `(6, 3)` β the correct NE neighbor of an odd column.
+
+The same Rust index produces correct output for both parities via
+`rustDirToFlatTopDir(1, col)` regardless of `col`.
+
+---
+
+## 5. Implementation
+
+```typescript
+// hexCanvas.ts
+export function rustDirToFlatTopDir(rustDir: number, col: number): number;
+```
+
+The `col` parameter is accepted for documentation clarity but the mapping is
+parity-independent. The function throws `RangeError` for `rustDir` outside 0β5.
+
+Verified by 12-case Vitest suite in
+`.project/designs/app/src/utils/worldGen/hexCanvas.test.ts`.
+
+---
+
+## 6. Non-goals
+
+- Changing Rust's `AXIAL_DIRECTIONS` β they are locked and match GDScript.
+- Changing `hexCorners` orientation β flat-top is the canvas baseline.
+- Godot translation β Rust's pointy-top labels match GDScript's `HexUtils`, so
+ no bridge-crossing translation is needed on the Godot side.
|