feat(@projects/@magic-civilization): mark hex direction mapping as complete

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-01 00:49:46 -04:00
parent de15734b30
commit a2a28fdf34
4 changed files with 184 additions and 40 deletions

View file

@ -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** |
</td><td valign='top' style='padding-left:2em'>
@ -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 |

View file

@ -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, 95103.
- ✓ **`HEX_CONVENTIONS.md`** created at
`public/games/age-of-dwarves/docs/HEX_CONVENTIONS.md` documenting:
- Rust convention: axial `(q, r)`, dirs 05 = 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 05 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

View file

@ -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",

View file

@ -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 `05`** (`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, 31)` = `(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 05.
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.