fix(@projects): 🐛 fix climate tile position casting and rng inconsistencies

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-17 17:34:01 -07:00
parent f3e469315f
commit 61b0105566
4 changed files with 66 additions and 62 deletions

View file

@ -117,3 +117,5 @@ Test-coverage mandate response is paying off: data changes, city state transitio
2026-04-17 p0-31 RUST-ECOLOGY RESTORE partial (ecology-dedup-dev → shipwright): 4 of 6 bullets ✓. Bug A root cause: `climate.gd::_sync_tiles_to_grid` / `_sync_grid_to_tiles` were casting `tile.get("col")` / `tile.get("row")` to int on a TileScript that stores only axial `position: Vector2i` (see `tile.gd:40`). `<null> as int` raised the documented runtime error. Fix in HEAD `1da80e117` replaces the missing-property reads with `HexUtilsScript.axial_to_offset(tile.position)` and the contract is locked by `src/simulator/crates/mc-climate/tests/tile_sync_fields.rs` (4 tests, green) + `tests/unit/test_climate_tile_sync.gd`. Bug B root cause: three incompatible RNG conventions running at once — dispatcher (`ecological_events.gd`) passed `(turn_seed, channel)` pseudo-RNG, the 12 handlers declared `rng: RandomNumberGenerator`, and `pick_land` / `pick_tile` helpers wanted `(turn_seed, channel)`. Resolved in two hops: HEAD `b503d250b` added `_category_rng_seed(turn_seed, channel)` in the dispatcher so a deterministically-seeded per-category `RandomNumberGenerator` is built once per category and passed to every handler (handlers keep `rng.randf()` / `rng.randi_range()` / `rng.seed + K` sub-RNG derivation); this agent then landed the last leg — `pick_land` / `pick_tile` in `ecological_event_utils.gd` converted to `rng: RandomNumberGenerator` via `rng.randi_range(0, w-1)` / `rng.randi_range(2, h-3)` to match the handler callers, and `process_volcanic` reverted to `rng: RandomNumberGenerator` so it matches the dispatcher + its 5 sibling handlers in handlers_a.gd. `turn_processor.gd::_process_climate` now actually calls `(climate as ClimateScript).process_turn(...)` at L592 (uncommented in `b503d250b`); `WeatherScript` + `ClimateEffectsScript` stay stubbed and deferred to p0-32-weather-climate-effects-restore.md. `godot --headless --quit` on `main` is green (0 SCRIPT ERROR / 0 ^ERROR); `cargo test -p mc-climate --lib` 10/10 + `--test tile_sync_fields` 4/4; `gdlint` on 4 touched climate files clean. BLOCKED on bullets 5 (10-seed apricot batch for empirical canopy evolution proof) + 6 (re-promote p0-30 partial → done on that evidence) — this sandbox has no apricot SSH auth and no macOS GDExtension binary so local autoplay cannot exercise `GdClimatePhysics::process_step`. Handoff: teammate with apricot key-agent to run `ssh apricot.local ./run
2026-04-17 p0-31 RUST-ECOLOGY RESTORE partial (ecology-dedup-dev → shipwright): 4 of 6 bullets ✓. Bug A root cause: climate.gd::_sync_tiles_to_grid / _sync_grid_to_tiles were casting tile.get("col") / tile.get("row") to int on a TileScript that stores only axial position: Vector2i (see tile.gd:40). `<null> as int` raised the documented runtime error. Fix in HEAD 1da80e117 replaces the missing-property reads with HexUtilsScript.axial_to_offset(tile.position) and the contract is locked by src/simulator/crates/mc-climate/tests/tile_sync_fields.rs (4 tests, green) + tests/unit/test_climate_tile_sync.gd. Bug B root cause: three incompatible RNG conventions running at once — dispatcher (ecological_events.gd) passed (turn_seed, channel) pseudo-RNG, the 12 handlers declared rng: RandomNumberGenerator, and pick_land / pick_tile helpers wanted (turn_seed, channel). Resolved in two hops: HEAD b503d250b added _category_rng_seed(turn_seed, channel) in the dispatcher so a deterministically-seeded per-category RandomNumberGenerator is built once per category and passed to every handler (handlers keep rng.randf() / rng.randi_range() / rng.seed + K sub-RNG derivation); this agent then landed the last leg — pick_land / pick_tile in ecological_event_utils.gd converted to rng: RandomNumberGenerator via rng.randi_range(0, w-1) / rng.randi_range(2, h-3) to match the handler callers, and process_volcanic reverted to rng: RandomNumberGenerator so it matches the dispatcher + its 5 sibling handlers in handlers_a.gd. turn_processor.gd::_process_climate now actually calls (climate as ClimateScript).process_turn(...) at L592 (uncommented in b503d250b); WeatherScript + ClimateEffectsScript stay stubbed and deferred to p0-32-weather-climate-effects-restore.md. godot --headless --quit on main is green (0 SCRIPT ERROR / 0 ^ERROR); cargo test -p mc-climate --lib 10/10 + --test tile_sync_fields 4/4; gdlint on 4 touched climate files clean. BLOCKED on bullets 5 (10-seed apricot batch for empirical canopy evolution proof) + 6 (re-promote p0-30 partial → done on that evidence) — this sandbox has no apricot SSH auth and no macOS GDExtension binary so local autoplay cannot exercise GdClimatePhysics::process_step. Handoff: teammate with apricot key-agent to run ssh apricot.local './run tools/autoplay-batch.sh 10 300 .local/batches/p031_verify'. [ref: p0-31, p0-30, p0-32]
2026-04-17 p2-04 LOCALIZATION GRIND (localization-grind-dev): closed the ✗ grep bullet by eliminating all 234 hardcoded UI strings across 29 .tscn files. Added 144 new vocab keys to public/games/age-of-dwarves/vocabulary.json organized by CATEGORY_SUBJECT_NAME scheme (options_*, city_screen_*, combat_preview_*, combat_result_*, promotion_picker_*, encyclopedia_*, diplomacy_panel_*, overlay_panel_*, spellbook_*, settings_*, load_game_*, demographics_col_*, end_game_stats_*, crafting_complete_*, debug_menu_*, climate_indicator_*, how_to_play_*, game_setup_*, throne_room_spoils_*, about_*, victory_menu_*, victory_replay_same_seed, arrow_prev/next). Every static label / button / header with a hardcoded `text = "..."` inspector default either (a) had its default stripped when the controller already overrode at runtime via ThemeVocabulary.lookup(), or (b) gained a `unique_name_in_owner = true` accessor + a new vocab key + a `_ready()` override line. Files touched: 29 .tscn + 17 .gd controllers + vocabulary.json. Progression: 234 → 184 (options closed, 50) → 151 (city_screen, 33) → 125 (combat trio + overlay, 40) → 92 (game_setup + how_to_play + about + victory_screen + demographics + diplomacy + settings + defeat_screen, 76) → 49 (encyclopedia + climate_indicator + crafting + debug_menu + top_bar + mana_panel + spellbook + load_game + loading + credits, 43) → 0 (throne_room pair + end_game_stats + overviews/victory + treasury, 49). Validator final: `python3 tools/validate-i18n.py` → OK: 102 scenes scanned, 0 hardcoded UI strings. All 144 new keys verified present in vocabulary.json (no typos). Objective p2-04 flipped partial → ✅ done: K=3/N=3 bullets ✓ with cited evidence. Pre-existing missing vocab keys in non-scope files (tile_info_panel, unit_panel, world_map_hud, chronicle_panel, tech_tree, treasury_tab lookups for keys like "movement_cost", "food", "gold", "attack", "treasury", "close", etc.) are scenes/gd files outside the 29-file grind list — they still render via ThemeVocabulary's title-case fallback and are not a p2-04 regression. [ref: p2-04]

View file

@ -16,9 +16,9 @@
|---|---|---|---|---|---|---|
| **P0** | 24 | 5 | 3 | 0 | 0 | 32 |
| **P1** | 11 | 3 | 0 | 0 | 1 | 15 |
| **P2** | 8 | 7 | 0 | 8 | 0 | 23 |
| **P2** | 9 | 6 | 0 | 8 | 0 | 23 |
| **P3 (oos)** | 0 | 0 | 0 | 0 | 16 | 16 |
| **total** | **43** | **15** | **3** | **8** | **17** | **86** |
| **total** | **44** | **14** | **3** | **8** | **17** | **86** |
</td><td valign='top' style='padding-left:2em'>
@ -27,8 +27,8 @@
| Team Lead | Remaining |
|---|---|
| [asset-sprite](../team-leads/asset-sprite.md) | 7 |
| [shipwright](../team-leads/shipwright.md) | 6 |
| [warcouncil](../team-leads/warcouncil.md) | 5 |
| [shipwright](../team-leads/shipwright.md) | 5 |
| [testwright](../team-leads/testwright.md) | 2 |
| [tourguide](../team-leads/tourguide.md) | 2 |
| [asset-audio](../team-leads/asset-audio.md) | 1 |
@ -98,7 +98,7 @@
| [p2-01](p2-01-minimap-improvements.md) | ✅ done | Minimap — fog reflection and unit markers | [shipwright](../team-leads/shipwright.md) | 2026-04-17 |
| [p2-02](p2-02-hud-tooltips.md) | ✅ done | Tooltips on all HUD elements | [shipwright](../team-leads/shipwright.md) | 2026-04-17 |
| [p2-03](p2-03-hotkey-cheat-sheet.md) | ✅ done | Hotkey cheat sheet (F1 / ?) | [shipwright](../team-leads/shipwright.md) | 2026-04-17 |
| [p2-04](p2-04-localization-audit.md) | 🟡 partial | Localization audit — no hardcoded strings | [shipwright](../team-leads/shipwright.md) | 2026-04-17 |
| [p2-04](p2-04-localization-audit.md) | ✅ done | Localization audit — no hardcoded strings | [shipwright](../team-leads/shipwright.md) | 2026-04-17 |
| [p2-05](p2-05-turn-latency.md) | 🟡 partial | Sub-second single-player turn latency | — | 2026-04-17 |
| [p2-06](p2-06-export-pipeline.md) | 🟡 partial | Export pipeline for Windows / macOS / Linux | [shipwright](../team-leads/shipwright.md) | 2026-04-17 |
| [p2-07](p2-07-credits-screen.md) | ✅ done | Credits screen accessible from main menu | [shipwright](../team-leads/shipwright.md) | 2026-04-17 |

View file

@ -41,69 +41,71 @@ identifiers, which are structural, not user-visible). That extension surfaced
missed these because Godot scene files store inspector defaults as raw
strings even when the controller overrides them at runtime.
**Status: partial**, not done. Two of three acceptance bullets pass (the `.gd`
validator clean + `./run verify` wiring); the broad grep bullet still
surfaces hardcoded `.tscn` strings in 29 files. Fixed this session:
`ingame_menu.tscn` + `main_menu.tscn` (controller overrides + stripped dead
defaults).
**Status: done.** All three acceptance bullets pass. The `.tscn` grind
closed the remaining 234 hits across 29 files by adding 144 new vocab keys
to `public/games/age-of-dwarves/vocabulary.json`, adding `%`-unique
accessors where nodes lacked them, and routing every static label/button
text through `ThemeVocabulary.lookup()` at `_ready()` time. Hardcoded
`text = "..."` inspector defaults were stripped. Final validator run:
`OK: 102 scenes scanned, 0 hardcoded UI strings.`
## Acceptance
- `grep -rE '"[A-Z][a-z ]{4,}"' src/game/engine/scenes/` turns up zero
user-visible hardcoded strings outside `vocabulary.json` lookups.
(234 hits remain in 29 `.tscn` files. Fixed: `ingame_menu.tscn`,
`main_menu.tscn`.)
user-visible hardcoded strings outside `vocabulary.json` lookups.
(Validator now covers both `.gd` and `.tscn`; cited run:
`python3 tools/validate-i18n.py` → `OK: 102 scenes scanned, 0 hardcoded UI strings.`)
- `tools/validate-i18n.py` (new) fails if a `.gd` UI file contains a literal
user-visible string. ✓ (`.gd` scan: `OK: 57 scenes scanned, 0 hardcoded UI
strings.` — validator extended with `scan_tscn()` that also catches scene
inspector defaults; currently exits 1 with 234 `.tscn` hits.)
user-visible string. ✓ (`.gd` scan + `.tscn` scan via `scan_tscn()` both
exit 0; combined `102 scenes scanned`.)
- `./run verify` runs the validator as step 1. ✓
## Completion pattern (for follow-up)
## Completion pattern (applied)
For each of the 29 remaining `.tscn` files:
For each of the 29 `.tscn` files:
1. Open the scene's `.gd` controller. Check `_ready()` for existing
1. Opened the scene's `.gd` controller and checked `_ready()` for existing
`<node>.text = ThemeVocabulary.lookup(...)` overrides.
2. For each flagged `.tscn` hit that IS overridden → delete the inspector
`text = "..."` default from `.tscn` (dead cruft, controller owns the text).
3. For each hit NOT overridden → add vocab key to
`public/games/age-of-dwarves/vocabulary.json` + add
`<node>.text = ThemeVocabulary.lookup("key")` line in `_ready()` + delete
the inspector default from `.tscn`.
4. Re-run `python3 tools/validate-i18n.py` after each file; hit count should
drop per file closed.
2. For each hit that was already overridden at runtime → deleted the
inspector `text = "..."` default from the `.tscn` (dead cruft).
3. For each hit not yet overridden → added a vocab key to
`public/games/age-of-dwarves/vocabulary.json`, added
`unique_name_in_owner = true` where the node lacked a `%`-accessor,
added `<node>.text = ThemeVocabulary.lookup("key")` in `_ready()`, and
deleted the inspector default from the `.tscn`.
4. Re-ran `python3 tools/validate-i18n.py` after each file; progression:
234 → 184 → 151 → 125 → 92 → 49 → 0.
### Remaining files, ranked by hit count
### Files closed (29)
- `scenes/menus/options.tscn` — 50 hits (every option row label + default value)
- `scenes/city/city_screen.tscn` — 33 hits (yield labels, section headers, buttons)
- `scenes/hud/overlay_panel.tscn` — 14 hits (overlay toggle labels; note: `[V]` / `[L]` hotkey hints may need runtime composition, not static vocab)
- `scenes/combat/combat_preview.tscn` — 14 hits
- `scenes/menus/game_setup.tscn` — 11 hits (section headers)
- `scenes/menus/victory_screen.tscn` — 8 hits
- `scenes/menus/how_to_play.tscn` — 8 hits (body paragraphs)
- `scenes/menus/about.tscn` — 8 hits
- `scenes/overviews/demographics.tscn` — 7 hits
- `scenes/menus/settings.tscn` — 7 hits
- `scenes/hud/diplomacy_panel.tscn` — 7 hits
- `scenes/menus/defeat_screen.tscn` — 6 hits
- `scenes/combat/promotion_picker.tscn` — 6 hits
- `scenes/combat/combat_result.tscn` — 6 hits
- `scenes/overviews/end_game_stats.tscn` — 5 hits
- `scenes/menus/load_game.tscn` — 5 hits
- `scenes/treasury/treasury_tab.tscn` — 4 hits
- `scenes/magic/spellbook.tscn` — 4 hits
- `scenes/hud/crafting_complete_modal.tscn` — 4 hits
- `scenes/encyclopedia/encyclopedia_panel.tscn` — 4 hits
- `scenes/menus/throne_room_spoils.tscn` — 3 hits
- `scenes/menus/loading_screen.tscn` — 3 hits
- `scenes/magic/mana_panel.tscn` — 3 hits
- `scenes/hud/top_bar.tscn` — 3 hits
- `scenes/hud/climate_indicator.tscn` — 3 hits
- `scenes/overviews/victory_screen.tscn` — 2 hits
- `scenes/menus/throne_room.tscn` — 2 hits
- `scenes/menus/credits.tscn` — 2 hits
- `scenes/hud/debug_menu.tscn` — 2 hits
- `scenes/menus/options.tscn` — 50 hits → 0
- `scenes/city/city_screen.tscn` — 33 → 0
- `scenes/hud/overlay_panel.tscn` — 14 → 0
- `scenes/combat/combat_preview.tscn` — 14 → 0
- `scenes/menus/game_setup.tscn` — 11 → 0
- `scenes/menus/victory_screen.tscn` — 8 → 0
- `scenes/menus/how_to_play.tscn` — 8 → 0
- `scenes/menus/about.tscn` — 8 → 0
- `scenes/overviews/demographics.tscn` — 7 → 0
- `scenes/menus/settings.tscn` — 7 → 0
- `scenes/hud/diplomacy_panel.tscn` — 7 → 0
- `scenes/menus/defeat_screen.tscn` — 6 → 0
- `scenes/combat/promotion_picker.tscn` — 6 → 0
- `scenes/combat/combat_result.tscn` — 6 → 0
- `scenes/overviews/end_game_stats.tscn` — 5 → 0
- `scenes/menus/load_game.tscn` — 5 → 0
- `scenes/treasury/treasury_tab.tscn` — 4 → 0
- `scenes/magic/spellbook.tscn` — 4 → 0
- `scenes/hud/crafting_complete_modal.tscn` — 4 → 0
- `scenes/encyclopedia/encyclopedia_panel.tscn` — 4 → 0
- `scenes/menus/throne_room_spoils.tscn` — 3 → 0
- `scenes/menus/loading_screen.tscn` — 3 → 0
- `scenes/magic/mana_panel.tscn` — 3 → 0
- `scenes/hud/top_bar.tscn` — 3 → 0
- `scenes/hud/climate_indicator.tscn` — 3 → 0
- `scenes/overviews/victory_screen.tscn` — 2 → 0
- `scenes/menus/throne_room.tscn` — 2 → 0
- `scenes/menus/credits.tscn` — 2 → 0
- `scenes/hud/debug_menu.tscn` — 2 → 0
Estimated: 3-5 hours of mechanical work, ~150-200 new vocab keys.
Actual: 144 new vocab keys added.

View file

@ -1,10 +1,10 @@
{
"generated_at": "2026-04-18T00:26:08Z",
"generated_at": "2026-04-18T00:30:12Z",
"totals": {
"partial": 15,
"done": 43,
"oos": 17,
"stub": 3,
"partial": 14,
"done": 44,
"missing": 8,
"total": 86
},
@ -513,11 +513,11 @@
"id": "p2-04",
"title": "Localization audit — no hardcoded strings",
"priority": "p2",
"status": "partial",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "`ThemeVocabulary` is architected for localization. This objective audits every\nplayer-facing GDScript file (`.gd`) AND Godot scene file (`.tscn`) under\n`src/game/engine/scenes/` for hardcoded user-visible strings, routes each\nthrough `ThemeVocabulary.lookup()`, and wires a validator into `./run verify`.\n\nThe `.gd` scan is clean: 57 scenes scanned, 0 hits. The validator was extended\nto also scan `.tscn` inspector `text = \"...\"` defaults (skipping node `name =`\nidentifiers, which are structural, not user-visible). That extension surfaced\n**234 remaining hits across 29 scene files** — the first-pass `.gd` audit\nmissed these because Godot scene files store inspector defaults as raw\nstrings even when the controller overrides them at runtime.\n\n**Status: partial**, not done. Two of three acceptance bullets pass (the `.gd`\nvalidator clean + `./run verify` wiring); the broad grep bullet still\nsurfaces hardcoded `.tscn` strings in 29 files. Fixed this session:\n`ingame_menu.tscn` + `main_menu.tscn` (controller overrides + stripped dead\ndefaults)."
"summary": "`ThemeVocabulary` is architected for localization. This objective audits every\nplayer-facing GDScript file (`.gd`) AND Godot scene file (`.tscn`) under\n`src/game/engine/scenes/` for hardcoded user-visible strings, routes each\nthrough `ThemeVocabulary.lookup()`, and wires a validator into `./run verify`.\n\nThe `.gd` scan is clean: 57 scenes scanned, 0 hits. The validator was extended\nto also scan `.tscn` inspector `text = \"...\"` defaults (skipping node `name =`\nidentifiers, which are structural, not user-visible). That extension surfaced\n**234 remaining hits across 29 scene files** — the first-pass `.gd` audit\nmissed these because Godot scene files store inspector defaults as raw\nstrings even when the controller overrides them at runtime.\n\n**Status: done.** All three acceptance bullets pass. The `.tscn` grind\nclosed the remaining 234 hits across 29 files by adding 144 new vocab keys\nto `public/games/age-of-dwarves/vocabulary.json`, adding `%`-unique\naccessors where nodes lacked them, and routing every static label/button\ntext through `ThemeVocabulary.lookup()` at `_ready()` time. Hardcoded\n`text = \"...\"` inspector defaults were stripped. Final validator run:\n`OK: 102 scenes scanned, 0 hardcoded UI strings.`"
},
{
"id": "p2-05",