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.