feat(@projects/@magic-civilization): update climate system and localization

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-17 17:28:56 -07:00
parent 87938ed602
commit f3e469315f
38 changed files with 229 additions and 180 deletions

View file

@ -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<String, _>``BTreeMap<String, _>` 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`). `<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
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]

View file

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

View file

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

View file

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

View file

@ -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<WonderId, u8>` (`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<String, _>` — 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<String, _>` → `BTreeMap<String, _>` 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 89 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",

View file

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

View file

@ -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)

View file

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

View file

@ -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()

View file

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

View file

@ -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)

View file

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

View file

@ -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:

View file

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

View file

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

View file

@ -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()

View file

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

View file

@ -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)

View file

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

View file

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

View file

@ -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)

View file

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

View file

@ -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)

View file

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

View file

@ -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", [])

View file

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

View file

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

View file

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

View file

@ -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="."]

View file

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

View file

@ -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:

View file

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

View file

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

View file

@ -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)

View file

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

View file

@ -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)

View file

@ -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):

View file

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