From f3e469315fa986f9f7f2686522e06dead2141b9d Mon Sep 17 00:00:00 2001 From: Natalie Date: Fri, 17 Apr 2026 17:28:56 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20update=20climate=20system=20and=20localization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .project/CHANGELOG.md | 4 +- .project/objectives/README.md | 9 +-- .../p0-32-weather-climate-effects-restore.md | 2 +- .../objectives/p2-04-localization-audit.md | 12 +++- .../games/age-of-dwarves/data/objectives.json | 24 +++++-- public/games/age-of-dwarves/vocabulary.json | 15 +++- .../scenes/encyclopedia/encyclopedia_panel.gd | 5 ++ .../encyclopedia/encyclopedia_panel.tscn | 5 +- .../engine/scenes/hud/climate_indicator.gd | 4 ++ .../engine/scenes/hud/climate_indicator.tscn | 5 +- .../scenes/hud/crafting_complete_modal.gd | 8 +-- .../scenes/hud/crafting_complete_modal.tscn | 4 -- src/game/engine/scenes/hud/debug_menu.gd | 9 ++- src/game/engine/scenes/hud/debug_menu.tscn | 3 +- src/game/engine/scenes/hud/top_bar.tscn | 3 - src/game/engine/scenes/magic/mana_panel.gd | 2 + src/game/engine/scenes/magic/mana_panel.tscn | 4 +- src/game/engine/scenes/magic/spellbook.gd | 13 ++-- src/game/engine/scenes/magic/spellbook.tscn | 6 +- src/game/engine/scenes/menus/credits.tscn | 2 - src/game/engine/scenes/menus/defeat_screen.gd | 2 + .../engine/scenes/menus/defeat_screen.tscn | 6 -- src/game/engine/scenes/menus/load_game.gd | 6 ++ src/game/engine/scenes/menus/load_game.tscn | 6 +- .../engine/scenes/menus/loading_screen.gd | 4 ++ .../engine/scenes/menus/loading_screen.tscn | 5 +- src/game/engine/scenes/menus/settings.gd | 13 ++++ src/game/engine/scenes/menus/settings.tscn | 16 ++--- src/game/engine/scenes/menus/throne_room.tscn | 2 - .../scenes/menus/throne_room_spoils.tscn | 3 - .../engine/scenes/overviews/end_game_stats.gd | 8 ++- .../scenes/overviews/end_game_stats.tscn | 9 +-- .../scenes/overviews/victory_screen.tscn | 2 - .../engine/scenes/treasury/treasury_tab.tscn | 4 -- .../engine/src/modules/ai/ai_turn_bridge.gd | 46 +++++++++++- .../engine/src/modules/climate/climate.gd | 10 ++- .../climate/ecological_event_handlers_b.gd | 72 ++++++++----------- .../src/modules/climate/ecological_events.gd | 56 +++++---------- 38 files changed, 229 insertions(+), 180 deletions(-) diff --git a/.project/CHANGELOG.md b/.project/CHANGELOG.md index 9501d942..bf410cbd 100644 --- a/.project/CHANGELOG.md +++ b/.project/CHANGELOG.md @@ -114,4 +114,6 @@ Test-coverage mandate response is paying off: data changes, city state transitio 2026-04-17 p0-12 SAVE/LOAD serde close (save-load-audit-dev): closed the two ✗ bullets reopened on this same date. (a) `PlayerState.strategic_axes` + `TechState.progress` changed `HashMap` → `BTreeMap` at `mc-turn/src/game_state.rs:89` and `:202` so JSON output is byte-stable across processes (BTreeMap iterates in sorted key order). (b) Added `relations_as_pairs` serde adapter module at `mc-turn/src/game_state.rs:19-35` + `#[serde(default, with = "relations_as_pairs")]` at `:149` — `BTreeMap<(u8,u8), RelationState>` now round-trips through serde_json as `Vec<((u8,u8), RelationState)>` instead of failing with "key must be a string". Call sites converted to BTreeMap across `mc-turn/src/{processor,snapshot,victory,processor_invariants,gpu/mod,bridge_contract_tests}.rs`, `api-gdext/src/{lib,ai}.rs`, `mc-sim/src/lib.rs` (HashMap→BTreeMap at the StrategyConfig→PlayerState boundary via iter/collect), `mc-sim/src/bin/{dominion_bench,tournament_bench,fauna_pressure_bench,solo_dominion}.rs` (`make_axes` return type → BTreeMap), `mc-turn/tests/full_turn_golden.rs`, `tests/integration/tests/pvp_combat_determinism.rs`. StrategyConfig in mc-balance stays HashMap (build-time config, never save-persisted). `mc-turn/tests/serde_roundtrip.rs`: dropped all three `#[ignore]` attributes; populated fixture now inserts a `(0, 1) → Peace` relation with `peaceful_turns=22, trade_turns=5`; re-enabled the `!restored.relations.is_empty()` assertion + added `peaceful_turns`/`trade_turns` preservation check. Full `cargo test -p mc-turn` green: 89 passed, 0 failed, 1 ignored (unrelated). `cargo check --all-targets` clean except for pre-existing `PlayerSnap.culture_pool` errors in `api-gdext/src/ai.rs` test module (unrelated to p0-12, predates this work). p0-12 flipped partial → ✅ done — K=8/N=8 bullets ✓ with line-cited evidence. [ref: p0-12] -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`). ` 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 \ No newline at end of file +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`). ` 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). ` 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] + diff --git a/.project/objectives/README.md b/.project/objectives/README.md index b3ca7330..72a6c2d5 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -14,11 +14,11 @@ | Priority | ✅ | 🟡 | 🔴 | ❌ | ⚫ | Total | |---|---|---|---|---|---|---| -| **P0** | 23 | 5 | 3 | 0 | 0 | 31 | +| **P0** | 24 | 5 | 3 | 0 | 0 | 32 | | **P1** | 11 | 3 | 0 | 0 | 1 | 15 | | **P2** | 8 | 7 | 0 | 8 | 0 | 23 | | **P3 (oos)** | 0 | 0 | 0 | 0 | 16 | 16 | -| **total** | **42** | **15** | **3** | **8** | **17** | **85** | +| **total** | **43** | **15** | **3** | **8** | **17** | **86** | @@ -50,7 +50,7 @@ | [p0-09](p0-09-ui-completeness.md) | ✅ done | City-screen UI completeness (citizen assign, queue controls, promotion picker) | — | 2026-04-16 | | [p0-10](p0-10-completion-stability.md) | ✅ done | Game-completion stability — ≥7/10 seeds declare a winner | — | 2026-04-17 | | [p0-11](p0-11-mystery-item-authoring.md) | ✅ done | Author the four T8–T10 mystery item drops | — | 2026-04-16 | -| [p0-12](p0-12-save-load-autosave.md) | 🟡 partial | Save / load + autosave on quit | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | +| [p0-12](p0-12-save-load-autosave.md) | ✅ done | Save / load + autosave on quit | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | | [p0-13](p0-13-fog-of-war-exploration.md) | ✅ done | Fog of war and exploration / scout loop | — | 2026-04-17 | | [p0-14](p0-14-map-generation-balanced-starts.md) | ✅ done | Map generation, resource placement, and balanced fair starts | [shipwright](../team-leads/shipwright.md) | 2026-04-16 | | [p0-15](p0-15-happiness-golden-age.md) | ✅ done | Happiness pool and Golden Age mechanics end-to-end | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | @@ -69,7 +69,8 @@ | [p0-28](p0-28-gd-economy-bridge.md) | ✅ done | GdEconomy bridge — live game delegates gold/upkeep to mc-economy | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | | [p0-29](p0-29-gd-tech-bridge.md) | ✅ done | GdTechWeb bridge — live game delegates research to mc-tech | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | | [p0-30](p0-30-ecology-double-tick-fix.md) | 🟡 partial | Remove duplicate GDScript ecology tick (single Rust source) | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p0-31](p0-31-climate-rust-path-restore.md) | 🔴 stub | Restore Rust ecology path — fix ClimateScript bugs + re-enable per-turn tick | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | +| [p0-31](p0-31-climate-rust-path-restore.md) | 🟡 partial | Restore Rust ecology path — fix ClimateScript bugs + re-enable per-turn tick | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | +| [p0-32](p0-32-weather-climate-effects-restore.md) | 🔴 stub | Restore WeatherScript + ClimateEffectsScript — per-turn weather and climate-effects | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | ## P1 — Ship-readiness diff --git a/.project/objectives/p0-32-weather-climate-effects-restore.md b/.project/objectives/p0-32-weather-climate-effects-restore.md index deaf5b57..e9398abd 100644 --- a/.project/objectives/p0-32-weather-climate-effects-restore.md +++ b/.project/objectives/p0-32-weather-climate-effects-restore.md @@ -4,7 +4,7 @@ title: Restore WeatherScript + ClimateEffectsScript — per-turn weather and cli priority: p0 status: stub scope: game1 -owner: unassigned +owner: shipwright updated_at: 2026-04-17 evidence: - src/game/engine/src/modules/climate/weather.gd diff --git a/.project/objectives/p2-04-localization-audit.md b/.project/objectives/p2-04-localization-audit.md index 24662870..d0a5bf0e 100644 --- a/.project/objectives/p2-04-localization-audit.md +++ b/.project/objectives/p2-04-localization-audit.md @@ -2,7 +2,7 @@ id: p2-04 title: Localization audit — no hardcoded strings priority: p2 -status: partial +status: done scope: game1 updated_at: 2026-04-17 owner: shipwright @@ -15,6 +15,16 @@ evidence: - src/game/engine/scenes/ui/ingame_menu.tscn - src/game/engine/scenes/menus/main_menu.gd - src/game/engine/scenes/menus/main_menu.tscn + - src/game/engine/scenes/menus/options.tscn + - src/game/engine/scenes/menus/options.gd + - src/game/engine/scenes/city/city_screen.tscn + - src/game/engine/scenes/city/city_screen.gd + - src/game/engine/scenes/combat/combat_preview.tscn + - src/game/engine/scenes/combat/combat_result.tscn + - src/game/engine/scenes/combat/promotion_picker.tscn + - src/game/engine/scenes/hud/overlay_panel.tscn + - src/game/engine/scenes/hud/diplomacy_panel.tscn + - src/game/engine/scenes/overviews/demographics.tscn --- ## Summary diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index 8a5a93f5..7d7b4920 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-04-18T00:12:27Z", + "generated_at": "2026-04-18T00:26:08Z", "totals": { - "missing": 8, - "done": 42, "partial": 15, + "done": 43, "oos": 17, "stub": 3, - "total": 85 + "missing": 8, + "total": 86 }, "objectives": [ { @@ -123,11 +123,11 @@ "id": "p0-12", "title": "Save / load + autosave on quit", "priority": "p0", - "status": "partial", + "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-17", - "summary": "Save/load UI, autosave-on-quit hook, multi-slot naming, schema version with rejection of mismatches, and a GDScript round-trip test covering the new PlayerState fields all shipped — all 5 original acceptance bullets ✓. **Downgraded 2026-04-17 after Testwright's T2 serde round-trip test (`mc-turn/tests/serde_roundtrip.rs`) surfaced three real save/load regressions the GDScript save round-trip missed:**\n\n1. **`WonderId` newtype serialized as an array, not a string** — `WonderId(pub String)` derived `Serialize` without `#[serde(transparent)]`, so `BTreeMap` (`PlayerState.wonders_built`) failed serde_json with `Error(\"key must be a string\")`. **Fixed 2026-04-17 by Testwright** (one-line: `#[serde(transparent)]` at `mc-core/src/wonder.rs:5`). Verified green on apricot.\n2. **`PlayerState.strategic_axes` and `TechState.progress` use `HashMap` — non-deterministic iteration order** — byte-equal save round-trip cannot hold because the same data serializes differently across processes. Saves are functionally correct but not reproducible byte-for-byte, which breaks the T2 invariant and makes deterministic replay / diff-based save verification impossible. **Owner: shipwright.** Fix: change both fields from `HashMap` → `BTreeMap` in `mc-turn/src/game_state.rs` (and wherever `TechState` is defined). 3 T2 tests (`game_state_json_roundtrip_is_stable`, `player_state_json_roundtrip_is_stable`, `tech_state_json_roundtrip_is_stable`) are `#[ignore]`'d with clear annotations pending this change; un-ignore when it lands.\n3. **`PlayerState.relations: BTreeMap<(u8, u8), RelationState>`** — tuple-keyed maps cannot round-trip through serde_json (`Error(\"key must be a string\")`). T2 fixture leaves this field empty + the `relations.is_empty()` assertion is commented out. Any save file with populated diplomacy either silently drops it, corrupts the round-trip, or crashes the WASM guide. **Owner: shipwright.** Fix in `mc-turn/src/game_state.rs:113`: `#[serde(with = \"vec_of_pairs\")]` wrapping the field as `Vec<((u8,u8), RelationState)>`, OR change key to `String` like `\"0,1\"` with From/Display helpers.\n\nShipwright work to un-downgrade this objective: (a) HashMap → BTreeMap change + un-ignore 3 tests, (b) relations tuple-key fix + un-comment fixture + re-enable assertion. Testwright has the tests ready to verify both." + "summary": "Save/load UI, autosave-on-quit hook, multi-slot naming, schema version with rejection of mismatches, and GDScript + Rust round-trip tests all shipped. The three serde regressions that reopened this objective on 2026-04-17 are closed:\n\n1. **`WonderId` newtype serialized as an array, not a string** — fixed by Testwright via `#[serde(transparent)]` at `mc-core/src/wonder.rs:5`.\n2. **`strategic_axes` + `TechState.progress` non-deterministic key order** — fixed by changing both fields from `HashMap` → `BTreeMap` in `mc-turn/src/game_state.rs`. BTreeMap iteration is sorted, so JSON output is byte-stable across processes. The three `#[ignore]` attributes on `game_state_json_roundtrip_is_stable`, `player_state_json_roundtrip_is_stable`, and `tech_state_json_roundtrip_is_stable` have been removed; all pass.\n3. **`PlayerState.relations` tuple-keyed map fails serde_json** — fixed by a `relations_as_pairs` serde adapter module in `mc-turn/src/game_state.rs` that round-trips the map as `Vec<((u8, u8), RelationState)>`. The T2 fixture now populates a `(0, 1) → Peace` relation and asserts `relations.is_empty() == false` plus field preservation after round-trip.\n\nFull mc-turn test suite green: 89 passed, 0 failed, 1 ignored (unrelated). All 5 tests in `mc-turn/tests/serde_roundtrip.rs` pass." }, { "id": "p0-13", @@ -313,12 +313,22 @@ "id": "p0-31", "title": "Restore Rust ecology path — fix ClimateScript bugs + re-enable per-turn tick", "priority": "p0", - "status": "stub", + "status": "partial", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-17", "summary": "p0-30 deleted the duplicate GDScript ecology pass (`ecosystem.gd`/`flora.gd`, 939 LOC) but could not close its bullet 4 (\"10-seed batch shows evolving canopy values\") because the Rust path is **also** disabled. `turn_processor.gd::_process_climate` (line 583) calls `MarineHarvestScript` only; the three sibling `process_turn` calls (`WeatherScript`, `ClimateScript`, `ClimateEffectsScript`) are commented out, citing real bugs:\n\n- **`ClimateScript.process_turn` (real code, live surface)** — raises `Invalid cast to int` inside `_sync_tiles_to_grid` / `_sync_grid_to_tiles`, and `ecological_events.process_events` has an arg-count mismatch (`process_drought` / `process_wildfire` / `process_marine` expect 8–9 args, fewer passed).\n- **`WeatherScript` + `ClimateEffectsScript`** — empty stubs; aborts propagate and kill the arena turn loop.\n\nAfter p0-30's deletion, ecology runs **0× per turn**. Flora canopy/undergrowth does not evolve — wild biome simulation is frozen. This objective narrowly restores the Rust ecology tick by fixing the `ClimateScript` bugs and re-enabling the call site. The two empty-stub siblings (`WeatherScript` / `ClimateEffectsScript`) are out of scope for p0-31 — they're deferred to follow-ups since they require full implementation, not bug repair.\n\nThis objective unblocks p0-30 bullet 4: once ecology ticks via Rust, a 10-seed batch can capture evolving canopy values and p0-30 flips ✅ done." }, + { + "id": "p0-32", + "title": "Restore WeatherScript + ClimateEffectsScript — per-turn weather and climate-effects", + "priority": "p0", + "status": "stub", + "scope": "game1", + "owner": "shipwright", + "updated_at": "2026-04-17", + "summary": "Forwarding stub. p0-31 restored the Rust ecology tick via `ClimateScript.process_turn`\nbut left the two sibling `process_turn` calls in `turn_processor.gd::_process_climate`\ncommented out (see turn_processor.gd `_process_climate` trailing comment). Both classes\nare empty stubs: calling their `process_turn` aborts `next_player` and kills the arena\nturn loop.\n\nThis objective fully implements:\n\n- `WeatherScript.process_turn(game_map)` — per-turn weather rolls (rain, storms,\n seasonal shifts) feeding `game_map.tiles[*].temperature` / `moisture` deltas.\n- `ClimateEffectsScript.process_turn(game_map, weather, players)` — apply weather\n consequences to unit movement, city production, tile yields.\n\nScope is the bridge from the empty-stub classes to fully-working per-turn calls\nthat slot into the climate sequencing order\n(`marine_harvest → weather → climate → climate_effects`)." + }, { "id": "p1-01", "title": "Diplomacy-lite — peace/war toggle plus one trade action", diff --git a/public/games/age-of-dwarves/vocabulary.json b/public/games/age-of-dwarves/vocabulary.json index 6c597a25..feaaffe3 100644 --- a/public/games/age-of-dwarves/vocabulary.json +++ b/public/games/age-of-dwarves/vocabulary.json @@ -642,7 +642,12 @@ "top_bar_era_placeholder": "Age of Founding", "spellbook_title": "Spellbook", - "spellbook_close": "Close", + "spellbook_close": "Close [S]", + "spellbook_cast_overworld": "Cast (Overworld)", + "spellbook_research": "Research Spell", + "spellbook_tab_researched": "Researched", + "spellbook_tab_available": "Available", + "spellbook_tab_enchantments": "Enchantments", "mana_panel_title": "Mana", @@ -709,6 +714,12 @@ "settings_accessibility_header": "Accessibility", "settings_back": "Back", "settings_apply": "Apply", + "settings_master_volume": "Master Volume", + "settings_music_volume": "Music Volume", + "settings_sfx_volume": "SFX Volume", + "settings_resolution": "Resolution", + "settings_language": "Language", + "settings_back_to_menu": "Back to Menu", "throne_room_title": "Throne Room", "throne_room_spoils_title": "New Acquisitions", @@ -740,6 +751,8 @@ "demographics_wonders_header": "Wonders", "demographics_close": "Close", + "end_game_stats_rankings_header": "Final Rankings", + "end_game_stats_events_header": "Key Events", "end_game_stats_title": "Final Report", "end_game_stats_subtitle": "Game Ended", "end_game_stats_close": "Continue", diff --git a/src/game/engine/scenes/encyclopedia/encyclopedia_panel.gd b/src/game/engine/scenes/encyclopedia/encyclopedia_panel.gd index 7fd3632b..2da21396 100644 --- a/src/game/engine/scenes/encyclopedia/encyclopedia_panel.gd +++ b/src/game/engine/scenes/encyclopedia/encyclopedia_panel.gd @@ -19,9 +19,14 @@ var _search_text: String = "" @onready var _detail_meta: Label = %DetailMeta @onready var _detail_body: RichTextLabel = %DetailBody @onready var _close_button: Button = %CloseButton +@onready var _title_label: Label = %Title func _ready() -> void: + _title_label.text = ThemeVocabulary.lookup("encyclopedia_title") + _close_button.text = ThemeVocabulary.lookup("encyclopedia_close") + _search_field.placeholder_text = ThemeVocabulary.lookup("encyclopedia_search_placeholder") + _detail_name.text = ThemeVocabulary.lookup("enc_select_entry") for cat: Dictionary in CATEGORIES: _tab_bar.add_tab(ThemeVocabulary.lookup(cat.vocab)) _tab_bar.tab_changed.connect(_on_tab_changed) diff --git a/src/game/engine/scenes/encyclopedia/encyclopedia_panel.tscn b/src/game/engine/scenes/encyclopedia/encyclopedia_panel.tscn index 516700f6..177120a9 100644 --- a/src/game/engine/scenes/encyclopedia/encyclopedia_panel.tscn +++ b/src/game/engine/scenes/encyclopedia/encyclopedia_panel.tscn @@ -48,15 +48,14 @@ theme_override_constants/separation = 8 layout_mode = 2 [node name="Title" type="Label" parent="PanelContainer/Margin/VBox/TopBar"] +unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 -text = "Encyclopedia" theme_override_font_sizes/font_size = 20 [node name="CloseButton" type="Button" parent="PanelContainer/Margin/VBox/TopBar"] unique_name_in_owner = true layout_mode = 2 -text = "Close" [node name="TabBar" type="TabBar" parent="PanelContainer/Margin/VBox"] unique_name_in_owner = true @@ -65,7 +64,6 @@ layout_mode = 2 [node name="SearchField" type="LineEdit" parent="PanelContainer/Margin/VBox"] unique_name_in_owner = true layout_mode = 2 -placeholder_text = "Search..." clear_button_enabled = true [node name="Split" type="HSplitContainer" parent="PanelContainer/Margin/VBox"] @@ -87,7 +85,6 @@ theme_override_constants/separation = 6 [node name="DetailName" type="Label" parent="PanelContainer/Margin/VBox/Split/DetailPane"] unique_name_in_owner = true layout_mode = 2 -text = "Select an entry" theme_override_font_sizes/font_size = 18 [node name="DetailMeta" type="Label" parent="PanelContainer/Margin/VBox/Split/DetailPane"] diff --git a/src/game/engine/scenes/hud/climate_indicator.gd b/src/game/engine/scenes/hud/climate_indicator.gd index 4089b052..884fee34 100644 --- a/src/game/engine/scenes/hud/climate_indicator.gd +++ b/src/game/engine/scenes/hud/climate_indicator.gd @@ -20,9 +20,13 @@ var _popup_timer: float = 0.0 @onready var _gauge_bar: ColorRect = %GaugeBar @onready var _gauge_bg: Panel = %GaugeBg @onready var _phase_popup: Label = %PhasePopup +@onready var _climate_icon: Label = %ClimateIcon func _ready() -> void: + _climate_icon.text = "~" + _climate_icon.tooltip_text = ThemeVocabulary.lookup("climate_indicator_tooltip_title") + _phase_label.tooltip_text = ThemeVocabulary.lookup("climate_indicator_tooltip_body") EventBus.turn_ended.connect(_on_turn_ended) _refresh() diff --git a/src/game/engine/scenes/hud/climate_indicator.tscn b/src/game/engine/scenes/hud/climate_indicator.tscn index 849d2287..0605081a 100644 --- a/src/game/engine/scenes/hud/climate_indicator.tscn +++ b/src/game/engine/scenes/hud/climate_indicator.tscn @@ -8,19 +8,16 @@ theme_override_constants/separation = 6 script = ExtResource("1") [node name="ClimateIcon" type="Label" parent="."] +unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 14 theme_override_colors/font_color = Color(0.5, 0.85, 1.0, 1) -text = "~" -tooltip_text = "Climate Phase" [node name="PhaseLabel" type="Label" parent="."] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 13 theme_override_colors/font_color = Color(0.85, 0.88, 0.85, 1) -text = "Climate: Temperate" -tooltip_text = "Current global climate phase based on average temperature" [node name="GaugeBg" type="Panel" parent="."] unique_name_in_owner = true diff --git a/src/game/engine/scenes/hud/crafting_complete_modal.gd b/src/game/engine/scenes/hud/crafting_complete_modal.gd index 84da6487..201c280b 100644 --- a/src/game/engine/scenes/hud/crafting_complete_modal.gd +++ b/src/game/engine/scenes/hud/crafting_complete_modal.gd @@ -25,10 +25,10 @@ func _ready() -> void: visible = false layer = 20 # Above the city overlay layer (5). - _title_label.text = ThemeVocabulary.lookup("item_crafted_title") - _equip_button.text = ThemeVocabulary.lookup("equip_on_squad") - _store_button.text = ThemeVocabulary.lookup("store_in_treasury") - _skip_button.text = ThemeVocabulary.lookup("close") + _title_label.text = ThemeVocabulary.lookup("crafting_complete_title") + _equip_button.text = ThemeVocabulary.lookup("crafting_complete_equip_on_squad") + _store_button.text = ThemeVocabulary.lookup("crafting_complete_store_in_treasury") + _skip_button.text = ThemeVocabulary.lookup("crafting_complete_close") EventBus.item_crafted.connect(_on_item_crafted) _equip_button.pressed.connect(_on_equip_pressed) diff --git a/src/game/engine/scenes/hud/crafting_complete_modal.tscn b/src/game/engine/scenes/hud/crafting_complete_modal.tscn index b1bcf66c..2650ab44 100644 --- a/src/game/engine/scenes/hud/crafting_complete_modal.tscn +++ b/src/game/engine/scenes/hud/crafting_complete_modal.tscn @@ -42,7 +42,6 @@ theme_override_constants/separation = 10 [node name="TitleLabel" type="Label" parent="Panel/Margin/Root"] unique_name_in_owner = true layout_mode = 2 -text = "Item Crafted" theme_override_font_sizes/font_size = 18 theme_override_colors/font_color = Color(0.95, 0.78, 0.32, 1) @@ -75,14 +74,11 @@ alignment = 2 [node name="SkipButton" type="Button" parent="Panel/Margin/Root/ButtonRow"] unique_name_in_owner = true layout_mode = 2 -text = "Close" [node name="StoreButton" type="Button" parent="Panel/Margin/Root/ButtonRow"] unique_name_in_owner = true layout_mode = 2 -text = "Store in Treasury" [node name="EquipButton" type="Button" parent="Panel/Margin/Root/ButtonRow"] unique_name_in_owner = true layout_mode = 2 -text = "Equip on squad" diff --git a/src/game/engine/scenes/hud/debug_menu.gd b/src/game/engine/scenes/hud/debug_menu.gd index bcabac23..ba7d5a62 100644 --- a/src/game/engine/scenes/hud/debug_menu.gd +++ b/src/game/engine/scenes/hud/debug_menu.gd @@ -16,6 +16,7 @@ var _find_index: int = 0 var _find_results: Array[Vector2i] = [] @onready var _advance_btn: Button = %AdvanceTurnsButton +@onready var _debug_label: Label = %DebugLabel func _ready() -> void: @@ -23,6 +24,8 @@ func _ready() -> void: queue_free() return + _debug_label.text = ThemeVocabulary.lookup("debug_menu_title") + _advance_btn.text = ThemeVocabulary.lookup("debug_menu_advance_10_turns") mouse_filter = Control.MOUSE_FILTER_STOP _apply_panel_style() _advance_btn.pressed.connect(_on_advance_pressed) @@ -40,7 +43,7 @@ func _exit_tree() -> void: func _on_advance_pressed() -> void: _advance_btn.disabled = true - _advance_btn.text = ThemeVocabulary.lookup("processing") + _advance_btn.text = ThemeVocabulary.lookup("action_processing") _local_player = null for player: Variant in GameState.players: @@ -50,7 +53,7 @@ func _on_advance_pressed() -> void: if _local_player == null: _advance_btn.disabled = false - _advance_btn.text = ThemeVocabulary.lookup("advance_turns") + _advance_btn.text = ThemeVocabulary.lookup("debug_menu_advance_10_turns") return _advance_target_turn = GameState.turn_number + 10 @@ -75,7 +78,7 @@ func _on_turn_during_advance(turn_number: int, player_index: int) -> void: EventBus.turn_started.disconnect(_on_turn_during_advance) _advance_btn.disabled = false - _advance_btn.text = ThemeVocabulary.lookup("advance_turns") + _advance_btn.text = ThemeVocabulary.lookup("debug_menu_advance_10_turns") func _apply_panel_style() -> void: diff --git a/src/game/engine/scenes/hud/debug_menu.tscn b/src/game/engine/scenes/hud/debug_menu.tscn index 6ff06417..0d0b30db 100644 --- a/src/game/engine/scenes/hud/debug_menu.tscn +++ b/src/game/engine/scenes/hud/debug_menu.tscn @@ -25,13 +25,12 @@ layout_mode = 2 theme_override_constants/separation = 2 [node name="DebugLabel" type="Label" parent="MarginContainer/VBoxContainer"] +unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 10 theme_override_colors/font_color = Color(0.65, 0.4, 0.25, 0.7) -text = "DEBUG" [node name="AdvanceTurnsButton" type="Button" parent="MarginContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 12 -text = "Advance 10 Turns" diff --git a/src/game/engine/scenes/hud/top_bar.tscn b/src/game/engine/scenes/hud/top_bar.tscn index 0aea8447..b4c76aad 100644 --- a/src/game/engine/scenes/hud/top_bar.tscn +++ b/src/game/engine/scenes/hud/top_bar.tscn @@ -31,7 +31,6 @@ unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 15 theme_override_colors/font_color = Color(1, 1, 1, 1) -text = "Turn 1" [node name="VSeparator" type="VSeparator" parent="MarginContainer/HBoxContainer"] layout_mode = 2 @@ -41,7 +40,6 @@ unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 13 theme_override_colors/font_color = Color(0.85, 0.75, 0.45, 1) -text = "Age of Founding" [node name="Spacer" type="Control" parent="MarginContainer/HBoxContainer"] layout_mode = 2 @@ -108,7 +106,6 @@ unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 13 theme_override_colors/font_color = Color(1.0, 0.92, 0.4, 1) -text = "Golden Age" [node name="GoldenAgeBadgeTurns" type="Label" parent="MarginContainer/HBoxContainer/GoldenAgeBadge"] unique_name_in_owner = true diff --git a/src/game/engine/scenes/magic/mana_panel.gd b/src/game/engine/scenes/magic/mana_panel.gd index 3bbddcdf..64807d7a 100644 --- a/src/game/engine/scenes/magic/mana_panel.gd +++ b/src/game/engine/scenes/magic/mana_panel.gd @@ -23,9 +23,11 @@ const SCHOOL_LABELS: Dictionary = { @onready var _rows: VBoxContainer = $PanelContainer/MarginContainer/VBox/ManaRows @onready var _cap_label: Label = $PanelContainer/MarginContainer/VBox/CapLabel @onready var _casting_label: Label = $PanelContainer/MarginContainer/VBox/CastingLabel +@onready var _title_label: Label = %TitleLabel func _ready() -> void: + _title_label.text = ThemeVocabulary.lookup("power_pool") EventBus.mana_changed.connect(_on_mana_changed) EventBus.turn_started.connect(_on_turn_started) _refresh() diff --git a/src/game/engine/scenes/magic/mana_panel.tscn b/src/game/engine/scenes/magic/mana_panel.tscn index c1f1bdaa..df5d91af 100644 --- a/src/game/engine/scenes/magic/mana_panel.tscn +++ b/src/game/engine/scenes/magic/mana_panel.tscn @@ -26,8 +26,8 @@ layout_mode = 2 theme_override_constants/separation = 4 [node name="TitleLabel" type="Label" parent="PanelContainer/MarginContainer/VBox"] +unique_name_in_owner = true layout_mode = 2 -text = "Mana Pool" theme_override_font_sizes/font_size = 14 [node name="ManaRows" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBox"] @@ -36,10 +36,8 @@ theme_override_constants/separation = 2 [node name="CapLabel" type="Label" parent="PanelContainer/MarginContainer/VBox"] layout_mode = 2 -text = "Cap: 50" theme_override_font_sizes/font_size = 12 [node name="CastingLabel" type="Label" parent="PanelContainer/MarginContainer/VBox"] layout_mode = 2 -text = "Casting Skill: 5" theme_override_font_sizes/font_size = 12 diff --git a/src/game/engine/scenes/magic/spellbook.gd b/src/game/engine/scenes/magic/spellbook.gd index 438e2b82..a68bcf65 100644 --- a/src/game/engine/scenes/magic/spellbook.gd +++ b/src/game/engine/scenes/magic/spellbook.gd @@ -33,13 +33,18 @@ var _current_tab: int = 0 @onready var _spell_cost_label: Label = %SpellCost @onready var _cast_button: Button = %CastButton @onready var _research_button: Button = %ResearchButton -@onready var _close_button: Button = $VBox/TopBar/CloseButton +@onready var _close_button: Button = %CloseButton +@onready var _title_label: Label = %TitleLabel func _ready() -> void: - _tab_bar.add_tab("Researched") - _tab_bar.add_tab("Available") - _tab_bar.add_tab("Enchantments") + _title_label.text = ThemeVocabulary.lookup("spellbook_title") + _close_button.text = ThemeVocabulary.lookup("spellbook_close") + _cast_button.text = ThemeVocabulary.lookup("spellbook_cast_overworld") + _research_button.text = ThemeVocabulary.lookup("spellbook_research") + _tab_bar.add_tab(ThemeVocabulary.lookup("spellbook_tab_researched")) + _tab_bar.add_tab(ThemeVocabulary.lookup("spellbook_tab_available")) + _tab_bar.add_tab(ThemeVocabulary.lookup("spellbook_tab_enchantments")) _close_button.pressed.connect(_on_close) _cast_button.pressed.connect(_on_cast_pressed) _research_button.pressed.connect(_on_research_pressed) diff --git a/src/game/engine/scenes/magic/spellbook.tscn b/src/game/engine/scenes/magic/spellbook.tscn index 695ed850..9965ad9c 100644 --- a/src/game/engine/scenes/magic/spellbook.tscn +++ b/src/game/engine/scenes/magic/spellbook.tscn @@ -47,16 +47,16 @@ layout_mode = 2 custom_minimum_size = Vector2(12, 0) [node name="TitleLabel" type="Label" parent="VBox/TopBar"] +unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 -text = "Spellbook" theme_override_font_sizes/font_size = 22 theme_override_colors/font_color = Color(0.95, 0.82, 0.3, 1) [node name="CloseButton" type="Button" parent="VBox/TopBar"] +unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(90, 38) -text = "Close [S]" theme_override_font_sizes/font_size = 14 [node name="CloseSpacer" type="Control" parent="VBox/TopBar"] @@ -177,7 +177,6 @@ theme_override_colors/font_color = Color(0.75, 0.72, 0.65, 1) unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(180, 38) -text = "Cast (Overworld)" theme_override_font_sizes/font_size = 14 theme_override_colors/font_color = Color(1.0, 0.88, 0.35, 1) visible = false @@ -186,6 +185,5 @@ visible = false unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(180, 38) -text = "Research Spell" theme_override_font_sizes/font_size = 14 visible = false diff --git a/src/game/engine/scenes/menus/credits.tscn b/src/game/engine/scenes/menus/credits.tscn index 2f0170f2..f9e94bbe 100644 --- a/src/game/engine/scenes/menus/credits.tscn +++ b/src/game/engine/scenes/menus/credits.tscn @@ -42,7 +42,6 @@ layout_mode = 2 horizontal_alignment = 1 theme_override_font_sizes/font_size = 34 theme_override_colors/font_color = Color(0.95, 0.82, 0.3, 1) -text = "Credits" [node name="TitleRule" type="ColorRect" parent="MarginContainer/VBox"] layout_mode = 2 @@ -75,4 +74,3 @@ layout_mode = 2 custom_minimum_size = Vector2(200, 48) theme_override_colors/font_color = Color(1.0, 0.92, 0.4, 1) theme_override_font_sizes/font_size = 17 -text = "Back to Menu" diff --git a/src/game/engine/scenes/menus/defeat_screen.gd b/src/game/engine/scenes/menus/defeat_screen.gd index 5de5decf..879ec8ce 100644 --- a/src/game/engine/scenes/menus/defeat_screen.gd +++ b/src/game/engine/scenes/menus/defeat_screen.gd @@ -22,6 +22,8 @@ const STAT_COL_KEYS: Array[String] = [ func _ready() -> void: + _replay_button.text = ThemeVocabulary.lookup("victory_replay_same_seed") + _main_menu_button.text = ThemeVocabulary.lookup("victory_menu_main_menu") _main_menu_button.pressed.connect(_on_main_menu) _replay_button.pressed.connect(_on_replay) EventBus.player_eliminated.connect(_on_player_eliminated) diff --git a/src/game/engine/scenes/menus/defeat_screen.tscn b/src/game/engine/scenes/menus/defeat_screen.tscn index 14d5fda7..981932f0 100644 --- a/src/game/engine/scenes/menus/defeat_screen.tscn +++ b/src/game/engine/scenes/menus/defeat_screen.tscn @@ -42,14 +42,12 @@ layout_mode = 2 horizontal_alignment = 1 theme_override_font_sizes/font_size = 40 theme_override_colors/font_color = Color(0.85, 0.3, 0.3, 1) -text = "DEFEATED" [node name="TopScorerLabel" type="Label" parent="Panel/MarginContainer/VBox"] unique_name_in_owner = true layout_mode = 2 horizontal_alignment = 1 theme_override_font_sizes/font_size = 18 -text = "Leader: Unknown" [node name="TurnLabel" type="Label" parent="Panel/MarginContainer/VBox"] unique_name_in_owner = true @@ -57,7 +55,6 @@ layout_mode = 2 horizontal_alignment = 1 theme_override_font_sizes/font_size = 13 theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1) -text = "Turn 1" [node name="SeedLabel" type="Label" parent="Panel/MarginContainer/VBox"] unique_name_in_owner = true @@ -65,7 +62,6 @@ layout_mode = 2 horizontal_alignment = 1 theme_override_font_sizes/font_size = 12 theme_override_colors/font_color = Color(0.55, 0.5, 0.4, 1) -text = "Seed 000000 — Continents, Standard" [node name="Separator" type="HSeparator" parent="Panel/MarginContainer/VBox"] layout_mode = 2 @@ -90,10 +86,8 @@ theme_override_constants/separation = 24 unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(160, 40) -text = "Replay Same Seed" [node name="MainMenuButton" type="Button" parent="Panel/MarginContainer/VBox/Buttons"] unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(140, 40) -text = "Main Menu" diff --git a/src/game/engine/scenes/menus/load_game.gd b/src/game/engine/scenes/menus/load_game.gd index ea239ec1..1f2ba041 100644 --- a/src/game/engine/scenes/menus/load_game.gd +++ b/src/game/engine/scenes/menus/load_game.gd @@ -12,9 +12,15 @@ var _selected_index: int = -1 @onready var _delete_button: Button = %DeleteButton @onready var _back_button: Button = %BackButton @onready var _empty_label: Label = %EmptyLabel +@onready var _title_label: Label = %TitleLabel func _ready() -> void: + _title_label.text = ThemeVocabulary.lookup("load_game_title") + _empty_label.text = ThemeVocabulary.lookup("load_game_empty") + _load_button.text = ThemeVocabulary.lookup("load_game_load") + _delete_button.text = ThemeVocabulary.lookup("load_game_delete") + _back_button.text = ThemeVocabulary.lookup("load_game_back") _load_button.pressed.connect(_on_load_pressed) _delete_button.pressed.connect(_on_delete_pressed) _back_button.pressed.connect(_on_back_pressed) diff --git a/src/game/engine/scenes/menus/load_game.tscn b/src/game/engine/scenes/menus/load_game.tscn index b67ffb62..f5d9528b 100644 --- a/src/game/engine/scenes/menus/load_game.tscn +++ b/src/game/engine/scenes/menus/load_game.tscn @@ -37,10 +37,10 @@ layout_mode = 2 theme_override_constants/separation = 20 [node name="TitleLabel" type="Label" parent="MarginContainer/VBoxContainer"] +unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 36 theme_override_colors/font_color = Color(0.9, 0.8, 0.4, 1) -text = "Load Game" horizontal_alignment = 1 [node name="HSeparator" type="HSeparator" parent="MarginContainer/VBoxContainer"] @@ -62,7 +62,6 @@ unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 18 theme_override_colors/font_color = Color(0.5, 0.5, 0.5, 1) -text = "No saved games found." horizontal_alignment = 1 visible = false @@ -75,16 +74,13 @@ theme_override_constants/separation = 12 unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(140, 44) -text = "Back" [node name="DeleteButton" type="Button" parent="MarginContainer/VBoxContainer/BottomRow"] unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(140, 44) -text = "Delete" [node name="LoadButton" type="Button" parent="MarginContainer/VBoxContainer/BottomRow"] unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(160, 44) -text = "Load Game" diff --git a/src/game/engine/scenes/menus/loading_screen.gd b/src/game/engine/scenes/menus/loading_screen.gd index 75af16ba..40c0767a 100644 --- a/src/game/engine/scenes/menus/loading_screen.gd +++ b/src/game/engine/scenes/menus/loading_screen.gd @@ -24,8 +24,12 @@ var _tip_index: int = 0 @onready var _stage_label: Label = %StageLabel @onready var _tip_label: Label = %TipLabel @onready var _clan_blurb: Label = %ClanBlurb +@onready var _title_label: Label = %TitleLabel +@onready var _subtitle_label: Label = %SubtitleLabel func _ready() -> void: + _title_label.text = ThemeVocabulary.lookup("game_title") + _subtitle_label.text = ThemeVocabulary.lookup("loading_generating") _overall_progress.value = 0.0 _stage_label.text = ThemeVocabulary.lookup("loading_initialising") _tips = _read_json(TIPS_PATH).get("tips", []) diff --git a/src/game/engine/scenes/menus/loading_screen.tscn b/src/game/engine/scenes/menus/loading_screen.tscn index 52e1eac8..7b836405 100644 --- a/src/game/engine/scenes/menus/loading_screen.tscn +++ b/src/game/engine/scenes/menus/loading_screen.tscn @@ -34,17 +34,17 @@ custom_minimum_size = Vector2(480, 0) theme_override_constants/separation = 24 [node name="TitleLabel" type="Label" parent="CenterContainer/VBoxContainer"] +unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 42 theme_override_colors/font_color = Color(0.95, 0.82, 0.3, 1) -text = "Magic Civilization" horizontal_alignment = 1 [node name="SubtitleLabel" type="Label" parent="CenterContainer/VBoxContainer"] +unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 20 theme_override_colors/font_color = Color(0.65, 0.58, 0.38, 1) -text = "Generating World..." horizontal_alignment = 1 [node name="Spacer" type="Control" parent="CenterContainer/VBoxContainer"] @@ -74,7 +74,6 @@ unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 14 theme_override_colors/font_color = Color(0.62, 0.6, 0.52, 1) -text = "Initialising..." horizontal_alignment = 1 [node name="ClanBlurb" type="Label" parent="CenterContainer/VBoxContainer"] diff --git a/src/game/engine/scenes/menus/settings.gd b/src/game/engine/scenes/menus/settings.gd index 2ca0ad65..a92bbc29 100644 --- a/src/game/engine/scenes/menus/settings.gd +++ b/src/game/engine/scenes/menus/settings.gd @@ -19,9 +19,22 @@ const RESOLUTIONS: Array[Vector2i] = [ @onready var _resolution: OptionButton = %ResolutionOption @onready var _language: OptionButton = %LanguageOption @onready var _back: Button = %BackButton +@onready var _title: Label = %Title +@onready var _master_label: Label = %MasterLabel +@onready var _music_label: Label = %MusicLabel +@onready var _sfx_label: Label = %SfxLabel +@onready var _resolution_label: Label = %ResolutionLabel +@onready var _language_label: Label = %LanguageLabel func _ready() -> void: + _title.text = ThemeVocabulary.lookup("settings_title") + _master_label.text = ThemeVocabulary.lookup("settings_master_volume") + _music_label.text = ThemeVocabulary.lookup("settings_music_volume") + _sfx_label.text = ThemeVocabulary.lookup("settings_sfx_volume") + _resolution_label.text = ThemeVocabulary.lookup("settings_resolution") + _language_label.text = ThemeVocabulary.lookup("settings_language") + _back.text = ThemeVocabulary.lookup("settings_back_to_menu") for res in RESOLUTIONS: _resolution.add_item("%d x %d" % [res.x, res.y]) _language.add_item("English") diff --git a/src/game/engine/scenes/menus/settings.tscn b/src/game/engine/scenes/menus/settings.tscn index 88ef2920..92f0e387 100644 --- a/src/game/engine/scenes/menus/settings.tscn +++ b/src/game/engine/scenes/menus/settings.tscn @@ -31,10 +31,10 @@ layout_mode = 2 theme_override_constants/separation = 18 [node name="Title" type="Label" parent="Margin/VBox"] +unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 34 theme_override_colors/font_color = Color(0.95, 0.82, 0.3, 1) -text = "Settings" horizontal_alignment = 1 [node name="Rule" type="ColorRect" parent="Margin/VBox"] @@ -48,10 +48,10 @@ custom_minimum_size = Vector2(0, 40) theme_override_constants/separation = 12 [node name="MasterLabel" type="Label" parent="Margin/VBox/MasterRow"] +unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(220, 0) theme_override_font_sizes/font_size = 15 -text = "Master Volume" vertical_alignment = 1 [node name="MasterSlider" type="HSlider" parent="Margin/VBox/MasterRow"] @@ -69,7 +69,6 @@ unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(60, 0) theme_override_font_sizes/font_size = 15 -text = "80%" horizontal_alignment = 2 vertical_alignment = 1 @@ -79,10 +78,10 @@ custom_minimum_size = Vector2(0, 40) theme_override_constants/separation = 12 [node name="MusicLabel" type="Label" parent="Margin/VBox/MusicRow"] +unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(220, 0) theme_override_font_sizes/font_size = 15 -text = "Music Volume" vertical_alignment = 1 [node name="MusicSlider" type="HSlider" parent="Margin/VBox/MusicRow"] @@ -100,7 +99,6 @@ unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(60, 0) theme_override_font_sizes/font_size = 15 -text = "70%" horizontal_alignment = 2 vertical_alignment = 1 @@ -110,10 +108,10 @@ custom_minimum_size = Vector2(0, 40) theme_override_constants/separation = 12 [node name="SfxLabel" type="Label" parent="Margin/VBox/SfxRow"] +unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(220, 0) theme_override_font_sizes/font_size = 15 -text = "SFX Volume" vertical_alignment = 1 [node name="SfxSlider" type="HSlider" parent="Margin/VBox/SfxRow"] @@ -131,7 +129,6 @@ unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(60, 0) theme_override_font_sizes/font_size = 15 -text = "70%" horizontal_alignment = 2 vertical_alignment = 1 @@ -141,10 +138,10 @@ custom_minimum_size = Vector2(0, 40) theme_override_constants/separation = 12 [node name="ResolutionLabel" type="Label" parent="Margin/VBox/ResolutionRow"] +unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(220, 0) theme_override_font_sizes/font_size = 15 -text = "Resolution" vertical_alignment = 1 [node name="ResolutionOption" type="OptionButton" parent="Margin/VBox/ResolutionRow"] @@ -158,10 +155,10 @@ custom_minimum_size = Vector2(0, 40) theme_override_constants/separation = 12 [node name="LanguageLabel" type="Label" parent="Margin/VBox/LanguageRow"] +unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(220, 0) theme_override_font_sizes/font_size = 15 -text = "Language" vertical_alignment = 1 [node name="LanguageOption" type="OptionButton" parent="Margin/VBox/LanguageRow"] @@ -180,4 +177,3 @@ custom_minimum_size = Vector2(200, 48) size_flags_horizontal = 4 theme_override_colors/font_color = Color(1.0, 0.92, 0.4, 1) theme_override_font_sizes/font_size = 17 -text = "Back to Menu" diff --git a/src/game/engine/scenes/menus/throne_room.tscn b/src/game/engine/scenes/menus/throne_room.tscn index df90125b..881d04d3 100644 --- a/src/game/engine/scenes/menus/throne_room.tscn +++ b/src/game/engine/scenes/menus/throne_room.tscn @@ -65,13 +65,11 @@ layout_mode = 2 size_flags_horizontal = 3 theme_override_font_sizes/font_size = 22 theme_override_colors/font_color = Color(0.95, 0.82, 0.3, 1) -text = "Throne Room" [node name="CloseButton" type="Button" parent="Header"] unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(110, 52) -text = "Return" theme_override_font_sizes/font_size = 14 [node name="TooltipPanel" type="PanelContainer" parent="."] diff --git a/src/game/engine/scenes/menus/throne_room_spoils.tscn b/src/game/engine/scenes/menus/throne_room_spoils.tscn index b728d296..7ea11f18 100644 --- a/src/game/engine/scenes/menus/throne_room_spoils.tscn +++ b/src/game/engine/scenes/menus/throne_room_spoils.tscn @@ -44,7 +44,6 @@ unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 26 theme_override_colors/font_color = Color(0.95, 0.82, 0.3, 1) -text = "New Acquisitions" horizontal_alignment = 1 [node name="TitleRule" type="ColorRect" parent="CenterContainer/Panel/VBox"] @@ -67,7 +66,6 @@ unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 13 theme_override_colors/font_color = Color(0.65, 0.60, 0.48, 0.8) -text = "Your hall awaits its first glory." horizontal_alignment = 1 visible = false @@ -75,5 +73,4 @@ visible = false unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(0, 48) -text = "Return" theme_override_font_sizes/font_size = 16 diff --git a/src/game/engine/scenes/overviews/end_game_stats.gd b/src/game/engine/scenes/overviews/end_game_stats.gd index c7dd3423..9d643fcb 100644 --- a/src/game/engine/scenes/overviews/end_game_stats.gd +++ b/src/game/engine/scenes/overviews/end_game_stats.gd @@ -13,13 +13,19 @@ var _graph_category_idx: int = 0 @onready var _graph_next: Button = %EndGameGraphNext @onready var _events_list: VBoxContainer = %KeyEventsList @onready var _main_menu_button: Button = %MainMenuButton +@onready var _rankings_header: Label = %RankingsHeader +@onready var _events_header: Label = %EventsHeader func _ready() -> void: + _rankings_header.text = ThemeVocabulary.lookup("end_game_stats_rankings_header") + _events_header.text = ThemeVocabulary.lookup("end_game_stats_events_header") + _graph_prev.text = ThemeVocabulary.lookup("arrow_prev") + _graph_next.text = ThemeVocabulary.lookup("arrow_next") _graph_prev.pressed.connect(_on_graph_prev) _graph_next.pressed.connect(_on_graph_next) _main_menu_button.pressed.connect(_on_main_menu) - _main_menu_button.text = ThemeVocabulary.lookup("main_menu") + _main_menu_button.text = ThemeVocabulary.lookup("victory_menu_main_menu") func setup(winner_index: int, victory_type: String) -> void: diff --git a/src/game/engine/scenes/overviews/end_game_stats.tscn b/src/game/engine/scenes/overviews/end_game_stats.tscn index 30a74678..392d0a99 100644 --- a/src/game/engine/scenes/overviews/end_game_stats.tscn +++ b/src/game/engine/scenes/overviews/end_game_stats.tscn @@ -41,7 +41,6 @@ unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 34 theme_override_colors/font_color = Color(0.95, 0.82, 0.3, 1) -text = "Victory!" horizontal_alignment = 1 [node name="EndGameSubtitle" type="Label" parent="MarginContainer/VBox"] @@ -67,10 +66,10 @@ layout_mode = 2 theme_override_constants/separation = 12 [node name="RankingsHeader" type="Label" parent="MarginContainer/VBox/ContentSplit/LeftPanel"] +unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 18 theme_override_colors/font_color = Color(0.85, 0.75, 0.45, 1) -text = "Final Rankings" [node name="FinalRankingsList" type="VBoxContainer" parent="MarginContainer/VBox/ContentSplit/LeftPanel"] unique_name_in_owner = true @@ -78,10 +77,10 @@ layout_mode = 2 theme_override_constants/separation = 4 [node name="EventsHeader" type="Label" parent="MarginContainer/VBox/ContentSplit/LeftPanel"] +unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 16 theme_override_colors/font_color = Color(0.85, 0.75, 0.45, 1) -text = "Key Events" [node name="EventsScroll" type="ScrollContainer" parent="MarginContainer/VBox/ContentSplit/LeftPanel"] layout_mode = 2 @@ -107,7 +106,6 @@ theme_override_constants/separation = 12 unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(32, 32) -text = "<" [node name="EndGameGraphLabel" type="Label" parent="MarginContainer/VBox/ContentSplit/RightPanel/GraphControls"] unique_name_in_owner = true @@ -115,7 +113,6 @@ layout_mode = 2 custom_minimum_size = Vector2(180, 0) theme_override_font_sizes/font_size = 16 theme_override_colors/font_color = Color(0.85, 0.75, 0.45, 1) -text = "Score" horizontal_alignment = 1 vertical_alignment = 1 @@ -123,7 +120,6 @@ vertical_alignment = 1 unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(32, 32) -text = ">" [node name="EndGameGraphArea" type="Control" parent="MarginContainer/VBox/ContentSplit/RightPanel"] unique_name_in_owner = true @@ -145,4 +141,3 @@ layout_mode = 2 custom_minimum_size = Vector2(200, 48) theme_override_font_sizes/font_size = 17 theme_override_colors/font_color = Color(1.0, 0.92, 0.4, 1) -text = "Main Menu" diff --git a/src/game/engine/scenes/overviews/victory_screen.tscn b/src/game/engine/scenes/overviews/victory_screen.tscn index 819d88bd..2591505e 100644 --- a/src/game/engine/scenes/overviews/victory_screen.tscn +++ b/src/game/engine/scenes/overviews/victory_screen.tscn @@ -38,7 +38,6 @@ unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 56 theme_override_colors/font_color = Color(1.0, 0.85, 0.2, 1) -text = "Victory!" horizontal_alignment = 1 [node name="SubtitleLabel" type="Label" parent="CenterContainer/VBoxContainer"] @@ -69,4 +68,3 @@ custom_minimum_size = Vector2(0, 16) unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(240, 48) -text = "Main Menu" diff --git a/src/game/engine/scenes/treasury/treasury_tab.tscn b/src/game/engine/scenes/treasury/treasury_tab.tscn index 2ea1a1c7..3e22d4c2 100644 --- a/src/game/engine/scenes/treasury/treasury_tab.tscn +++ b/src/game/engine/scenes/treasury/treasury_tab.tscn @@ -42,7 +42,6 @@ layout_mode = 2 unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 -text = "Treasury" theme_override_font_sizes/font_size = 22 [node name="SoftCapHint" type="Label" parent="Panel/Margin/Root/HeaderRow"] @@ -55,7 +54,6 @@ theme_override_font_sizes/font_size = 12 [node name="CloseButton" type="Button" parent="Panel/Margin/Root/HeaderRow"] unique_name_in_owner = true layout_mode = 2 -text = "Close" [node name="HeaderSep" type="HSeparator" parent="Panel/Margin/Root"] layout_mode = 2 @@ -73,7 +71,6 @@ size_flags_stretch_ratio = 2.0 [node name="ItemsHeader" type="Label" parent="Panel/Margin/Root/Columns/ItemsCol"] unique_name_in_owner = true layout_mode = 2 -text = "Finished items" theme_override_font_sizes/font_size = 14 theme_override_colors/font_color = Color(0.82, 0.68, 0.32, 1) @@ -106,7 +103,6 @@ size_flags_stretch_ratio = 1.0 [node name="MaterialsHeader" type="Label" parent="Panel/Margin/Root/Columns/MaterialsCol"] unique_name_in_owner = true layout_mode = 2 -text = "Stockpiled materials" theme_override_font_sizes/font_size = 14 theme_override_colors/font_color = Color(0.62, 0.72, 0.88, 1) diff --git a/src/game/engine/src/modules/ai/ai_turn_bridge.gd b/src/game/engine/src/modules/ai/ai_turn_bridge.gd index a399b985..25d23b27 100644 --- a/src/game/engine/src/modules/ai/ai_turn_bridge.gd +++ b/src/game/engine/src/modules/ai/ai_turn_bridge.gd @@ -16,6 +16,9 @@ const CityScript: GDScript = preload("res://engine/src/entities/city.gd") const CombatResolverScript: GDScript = preload( "res://engine/src/modules/combat/combat_resolver.gd" ) +const CombatUtilsScript: GDScript = preload( + "res://engine/src/modules/combat/combat_utils.gd" +) ## MCTS rollout budgets. 300 keeps per-turn wall time under ~50 ms on the ## 8-core apricot batch host during the early game; 100 protects main-thread @@ -353,14 +356,55 @@ static func _dispatch_move(fields: Dictionary, index_maps: Dictionary) -> bool: var to_hex: Array = fields.get("to_hex", []) if to_hex.size() != 2: return false - var from: Vector2i = unit.position var to: Vector2i = Vector2i(int(to_hex[0]), int(to_hex[1])) + # mc-ai::tactical::movement encodes attacks as MoveUnit onto an enemy + # hex — the Action enum has no AttackHex variant. Mirror the pre-port + # GDScript "move into enemy hex = attack" convention so we don't + # teleport through them. + var enemy_defender: RefCounted = _find_enemy_at(to, int(unit.owner)) + if enemy_defender != null: + return _resolve_move_as_attack(unit, enemy_defender) + var enemy_city: RefCounted = CombatUtilsScript.get_city_at(to) + if enemy_city != null and int(enemy_city.owner) != int(unit.owner): + return _resolve_move_as_attack(unit, enemy_city) + var from: Vector2i = unit.position unit.position = to unit.movement_remaining = maxi(0, unit.movement_remaining - 1) EventBus.unit_moved.emit(unit, from, to) return true +static func _find_enemy_at(pos: Vector2i, attacker_owner: int) -> RefCounted: + var primary: Dictionary = GameState.get_primary_layer() + var all_units: Array = primary.get("units", []) + for u: RefCounted in all_units: + if u == null or not u.is_alive(): + continue + if u.position != pos: + continue + if int(u.owner) == attacker_owner: + continue + return u + return null + + +## Resolve a combat round initiated by MoveUnit onto an enemy-held hex. +## Mirrors `_dispatch_attack` but takes the already-resolved attacker + defender +## RefCounteds directly, skipping the attacker_id / target_id re-lookup. +static func _resolve_move_as_attack(attacker: RefCounted, defender: RefCounted) -> bool: + if attacker.movement_remaining <= 0: + return false + var game_map: RefCounted = GameState.get_game_map() + if game_map == null: + return false + var primary: Dictionary = GameState.get_primary_layer() + var all_units: Array = primary.get("units", []) + var resolver: RefCounted = CombatResolverScript.new() + resolver.resolve(attacker, defender, game_map, all_units) + attacker.movement_remaining = 0 + return true + + static func _dispatch_attack(fields: Dictionary, index_maps: Dictionary) -> bool: var attacker: RefCounted = _resolve_unit( int(fields.get("attacker_id", -1)), index_maps diff --git a/src/game/engine/src/modules/climate/climate.gd b/src/game/engine/src/modules/climate/climate.gd index a8f7fc9d..e96ca9a9 100644 --- a/src/game/engine/src/modules/climate/climate.gd +++ b/src/game/engine/src/modules/climate/climate.gd @@ -47,7 +47,9 @@ func _ensure_rust() -> void: push_warning("Climate: climate_params.json missing — using built-in defaults") _spec = DataLoader.get_climate_spec() if _spec.is_empty(): - push_warning("Climate: climate_spec.json missing — terrain transitions use built-in defaults") + push_warning( + "Climate: climate_spec.json missing — terrain transitions use built-in defaults" + ) var world_flags: Dictionary = DataLoader.get_physics_features() if not world_flags.is_empty(): @@ -93,7 +95,11 @@ func process_turn(game_map: RefCounted, turn: int = 0, seed: int = 42) -> void: LeyResidueScript.tick_residue(game_map) if _physics_flags.get("ecology", true): EcologicalEventsScript.process_events( - game_map, turn, seed, _spec, DataLoader.get_ecological_events(), + game_map, + turn, + seed, + _spec, + DataLoader.get_ecological_events(), GameState.get_max_event_tier() ) AnchorDecayScript.process_decay(game_map, _spec) diff --git a/src/game/engine/src/modules/climate/ecological_event_handlers_b.gd b/src/game/engine/src/modules/climate/ecological_event_handlers_b.gd index a1a2f3d4..2ddf5b84 100644 --- a/src/game/engine/src/modules/climate/ecological_event_handlers_b.gd +++ b/src/game/engine/src/modules/climate/ecological_event_handlers_b.gd @@ -2,16 +2,14 @@ extends RefCounted ## Ecological event handlers: magical, marine, solar, glacial, tsunami, pandemic. ## All functions are static. Called by EcologicalEvents.process_events() dispatch. ## -## Handlers consume the dispatcher's deterministic hash-noise API: every roll -## derives from `EcoUtils.hash_noise(channel, sub, turn_seed)` so reruns of the -## same (turn, seed) reproduce identically without RNG state threading. The -## `channel` float identifies the category (dispatcher assigns +10 per slot); -## handlers pick disjoint sub-channels for each roll they need. +## Handlers consume a `rng: RandomNumberGenerator` supplied by the dispatcher, +## which seeds it deterministically per-category from (turn_seed, channel_idx) +## via EcologicalEvents._category_rng_seed. This preserves reproducibility of +## (turn, seed) reruns while keeping the `rng.randf()/.randi_range()` call +## shape the handlers expect. const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd") -const EcoUtils: GDScript = preload( - "res://engine/src/modules/climate/ecological_event_utils.gd" -) +const EcoUtils: GDScript = preload("res://engine/src/modules/climate/ecological_event_utils.gd") static func _opt_str(cfg: Dictionary, key: String) -> String: @@ -33,14 +31,13 @@ static func process_magical( tier_cfg: Dictionary, w: int, h: int, - turn_seed: float, - channel: float, + rng: RandomNumberGenerator, turn: int, events: Array ) -> void: ## Magical events: T1 grove (positive), T2 chaos storm, T3 wellspring or death rift, ## T4 spirit nexus, T5 arcane cataclysm. - var center: Resource = EcoUtils.pick_land(game_map, w, h, turn_seed, channel) + var center: Resource = EcoUtils.pick_land(game_map, w, h, rng) if center == null or center.is_natural_wonder(): return @@ -49,15 +46,13 @@ static func process_magical( # T3: 50/50 between positive wellspring and negative death rift if tier == 3 and tier_cfg.has("alt_negative"): - if EcoUtils.hash_noise(channel, 5.0, turn_seed) >= 0.5: + if rng.randf() >= 0.5: var alt: Dictionary = tier_cfg["alt_negative"] _apply_magical_negative(game_map, center, alt, turn, "death_rift", events) return if positive: - _apply_magical_positive( - game_map, center, tier_cfg, turn_seed, channel, tier_name, turn, events - ) + _apply_magical_positive(game_map, center, tier_cfg, rng, tier_name, turn, events) else: _apply_magical_negative(game_map, center, tier_cfg, turn, "magical", events) @@ -66,8 +61,7 @@ static func _apply_magical_positive( game_map: Variant, center: Resource, tier_cfg: Dictionary, - turn_seed: float, - channel: float, + rng: RandomNumberGenerator, tier_name: String, turn: int, events: Array @@ -107,9 +101,7 @@ static func _apply_magical_positive( schools.append("nature") if schools.is_empty(): var options: Array[String] = ["life", "nature", "aether"] - var roll: float = EcoUtils.hash_noise(channel, 6.0, turn_seed) - var idx: int = clampi(int(roll * float(options.size())), 0, options.size() - 1) - schools.append(options[idx]) + schools.append(options[rng.randi_range(0, options.size() - 1)]) else: var raw_schools: Array = tier_cfg.get("anchor_schools", []) for s: Variant in raw_schools: @@ -179,13 +171,12 @@ static func process_marine( tier_cfg: Dictionary, w: int, h: int, - turn_seed: float, - channel: float, + rng: RandomNumberGenerator, turn: int, events: Array ) -> void: ## Marine events: T1-T2 positive (reef/fish boost), T3+ negative (die-off, anoxia). - var center: Resource = EcoUtils.pick_tile(game_map, w, h, turn_seed, channel) + var center: Resource = EcoUtils.pick_tile(game_map, w, h, rng) if center == null: return if not BiomeRegistry.has_tag(center.biome_id, "is_water"): @@ -249,8 +240,7 @@ static func process_solar( game_map: Variant, _tier: int, tier_cfg: Dictionary, - turn_seed: float, - channel: float, + rng: RandomNumberGenerator, turn: int, events: Array ) -> void: @@ -259,7 +249,7 @@ static func process_solar( var active_cfg: Dictionary = tier_cfg var alt_chance: float = tier_cfg.get("alt_chance", 0.0) if alt_chance > 0.0 and tier_cfg.has("alt_event"): - if EcoUtils.hash_noise(channel, 7.0, turn_seed) < alt_chance: + if rng.randf() < alt_chance: active_cfg = tier_cfg["alt_event"] var global_heat: float = active_cfg.get("global_heat", 0.0) @@ -283,8 +273,7 @@ static func process_glacial( tier_cfg: Dictionary, w: int, h: int, - turn_seed: float, - channel: float, + rng: RandomNumberGenerator, turn: int, events: Array ) -> void: @@ -322,16 +311,16 @@ static func process_glacial( var tsunami_cfg: Dictionary = DataLoader.get_ecological_events().get("tsunami", {}) var ts_tier_cfg: Dictionary = tsunami_cfg.get("tiers", {}).get(str(ts_tier), {}) if not ts_tier_cfg.is_empty(): - # Derive a disjoint sub-channel so the triggered tsunami doesn't - # alias the sibling tsunami category's own dispatch channel. - process_tsunami( - game_map, ts_tier, ts_tier_cfg, w, h, turn_seed, channel + 8888.0, turn, events - ) + # Derive a disjoint sub-RNG so the triggered tsunami doesn't alias + # the sibling tsunami category's own dispatch channel. + var sub_rng: RandomNumberGenerator = RandomNumberGenerator.new() + sub_rng.seed = rng.seed + 8888 + process_tsunami(game_map, ts_tier, ts_tier_cfg, w, h, sub_rng, turn, events) return # Cold direction: regional temp delta, optional river freeze + tundra expansion var radius: int = tier_cfg.get("radius", 3) - var center: Resource = EcoUtils.pick_land(game_map, w, h, turn_seed, channel) + var center: Resource = EcoUtils.pick_land(game_map, w, h, rng) if center == null: return @@ -374,14 +363,13 @@ static func process_tsunami( tier_cfg: Dictionary, w: int, h: int, - turn_seed: float, - channel: float, + rng: RandomNumberGenerator, turn: int, events: Array ) -> void: ## Tsunami: picks a coast tile, floods inland_reach hexes from coast inward. ## Building/improvement damage and unit damage noted in event for city.gd to process. - var center: Resource = EcoUtils.pick_tile(game_map, w, h, turn_seed, channel) + var center: Resource = EcoUtils.pick_tile(game_map, w, h, rng) if center == null: return if not BiomeRegistry.has_tag(center.biome_id, "is_coast"): @@ -425,8 +413,7 @@ static func process_pandemic( tier_cfg: Dictionary, w: int, h: int, - turn_seed: float, - channel: float, + rng: RandomNumberGenerator, turn: int, events: Array ) -> void: @@ -437,22 +424,19 @@ static func process_pandemic( if target == "wildlife" or target == "livestock": # Ecological tier: reduce lairs and fish stocks in a radius - var center: Resource = EcoUtils.pick_land(game_map, w, h, turn_seed, channel) + var center: Resource = EcoUtils.pick_land(game_map, w, h, rng) if center == null: return var radius: int = tier_cfg.get("radius", 3) var lair_kill_chance: float = tier_cfg.get("lair_kill_chance", 0.0) var fish_stock_loss: int = tier_cfg.get("fish_stock_loss", 0) var killed_lairs: int = 0 - var lair_idx: float = 0.0 for axial: Vector2i in EcoUtils.tiles_in_radius(center, radius): - lair_idx += 1.0 if not game_map.tiles.has(axial): continue var t: Resource = game_map.tiles[axial] if lair_kill_chance > 0.0 and t.lair_type != "": - # Per-tile deterministic kill roll; sub-channel indexed by tile ordinal. - if EcoUtils.hash_noise(channel, 9.0 + lair_idx, turn_seed) < lair_kill_chance: + if rng.randf() < lair_kill_chance: t.lair_type = "" # Convert NPC building to ruin for b: Variant in GameState.get_npc_buildings_at(axial): diff --git a/src/game/engine/src/modules/climate/ecological_events.gd b/src/game/engine/src/modules/climate/ecological_events.gd index e83d16d9..7bd268d4 100644 --- a/src/game/engine/src/modules/climate/ecological_events.gd +++ b/src/game/engine/src/modules/climate/ecological_events.gd @@ -12,9 +12,7 @@ const EcoHandlersA: GDScript = preload( const EcoHandlersB: GDScript = preload( "res://engine/src/modules/climate/ecological_event_handlers_b.gd" ) -const EcoUtils: GDScript = preload( - "res://engine/src/modules/climate/ecological_event_utils.gd" -) +const EcoUtils: GDScript = preload("res://engine/src/modules/climate/ecological_event_utils.gd") ## Ordered category list — channel_idx increments by 10 per slot so each ## category gets a deterministically separate noise channel. @@ -35,8 +33,12 @@ const _CATEGORY_ORDER: Array[String] = [ static func process_events( - game_map: Variant, turn: int, seed: int, spec: Dictionary, - event_cfg: Dictionary = {}, max_tier: int = 10 + game_map: Variant, + turn: int, + seed: int, + spec: Dictionary, + event_cfg: Dictionary = {}, + max_tier: int = 10 ) -> Array: ## Run all ecological event rolls for this turn. ## Returns Array[{ "turn", "type", "col", "row", "description" }]. @@ -80,53 +82,31 @@ static func process_events( rng.seed = _category_rng_seed(turn_seed, channel_idx + 10.0) match category: "volcanic": - EcoHandlersA.process_volcanic( - game_map, tier, tier_cfg, w, h, rng, turn, events - ) + EcoHandlersA.process_volcanic(game_map, tier, tier_cfg, w, h, rng, turn, events) "impact": - EcoHandlersA.process_impact( - game_map, tier, tier_cfg, w, h, rng, turn, events - ) + EcoHandlersA.process_impact(game_map, tier, tier_cfg, w, h, rng, turn, events) "seismic": - EcoHandlersA.process_seismic( - game_map, tier, tier_cfg, w, h, rng, turn, events - ) + EcoHandlersA.process_seismic(game_map, tier, tier_cfg, w, h, rng, turn, events) "wildfire": EcoHandlersA.process_wildfire( game_map, tier, tier_cfg, cat, w, h, rng, turn, events ) "drought": - EcoHandlersA.process_drought( - game_map, tier, tier_cfg, w, h, rng, turn, events - ) + EcoHandlersA.process_drought(game_map, tier, tier_cfg, w, h, rng, turn, events) "plague": - EcoHandlersA.process_plague( - game_map, tier, tier_cfg, w, h, rng, turn, events - ) + EcoHandlersA.process_plague(game_map, tier, tier_cfg, w, h, rng, turn, events) "magical": - EcoHandlersB.process_magical( - game_map, tier, tier_cfg, w, h, rng, turn, events - ) + EcoHandlersB.process_magical(game_map, tier, tier_cfg, w, h, rng, turn, events) "marine": - EcoHandlersB.process_marine( - game_map, tier, tier_cfg, w, h, rng, turn, events - ) + EcoHandlersB.process_marine(game_map, tier, tier_cfg, w, h, rng, turn, events) "solar": - EcoHandlersB.process_solar( - game_map, tier, tier_cfg, rng, turn, events - ) + EcoHandlersB.process_solar(game_map, tier, tier_cfg, rng, turn, events) "glacial": - EcoHandlersB.process_glacial( - game_map, tier, tier_cfg, w, h, rng, turn, events - ) + EcoHandlersB.process_glacial(game_map, tier, tier_cfg, w, h, rng, turn, events) "tsunami": - EcoHandlersB.process_tsunami( - game_map, tier, tier_cfg, w, h, rng, turn, events - ) + EcoHandlersB.process_tsunami(game_map, tier, tier_cfg, w, h, rng, turn, events) "pandemic": - EcoHandlersB.process_pandemic( - game_map, tier, tier_cfg, w, h, rng, turn, events - ) + EcoHandlersB.process_pandemic(game_map, tier, tier_cfg, w, h, rng, turn, events) channel_idx += 10.0 return events