diff --git a/.project/objectives/README.md b/.project/objectives/README.md
index 8200adeb..6c67412a 100644
--- a/.project/objectives/README.md
+++ b/.project/objectives/README.md
@@ -15,10 +15,10 @@
| Priority | β
| π΅ | π‘ | π΄ | β | β« | Total |
|---|---|---|---|---|---|---|---|
| **P0** | 43 | 0 | 0 | 0 | 0 | 0 | 43 |
-| **P1** | 47 | 4 | 8 | 3 | 9 | 1 | 72 |
-| **P2** | 52 | 1 | 2 | 1 | 6 | 6 | 68 |
+| **P1** | 47 | 4 | 10 | 3 | 7 | 1 | 72 |
+| **P2** | 52 | 1 | 6 | 0 | 3 | 6 | 68 |
| **P3 (oos)** | 3 | 0 | 0 | 0 | 1 | 19 | 23 |
-| **total** | **145** | **5** | **10** | **4** | **16** | **26** | **206** |
+| **total** | **145** | **5** | **16** | **3** | **11** | **26** | **206** |
@@ -116,7 +116,7 @@
| [p1-24](p1-24-windows-path-separator.md) | β
done | ai_personalities.json fails to load from packed builds (all platforms) β pass JSON contents not path | [shipwright](../team-leads/shipwright.md) | 2026-04-25 |
| [p1-25](p1-25-export-script-error-cleanup.md) | β
done | Eliminate parse-error spam in export logs (Unit dup decl + SaveManager stray) | [shipwright](../team-leads/shipwright.md) | 2026-04-25 |
| [p1-26](p1-26-tile-placement-preview-ux.md) | β
done | "Tile-placement UX with effect preview β Civ7-style \\\"where does this go and what changes\\\"" | [shipwright](../team-leads/shipwright.md) | 2026-04-26 |
-| [p1-27](p1-27-mcts-service-extraction.md) | β missing | Extract GPU MCTS into a standalone service/client (model-boss-shaped, magic-civ-only) | [warcouncil](../team-leads/warcouncil.md) | 2026-04-25 |
+| [p1-27](p1-27-mcts-service-extraction.md) | π‘ partial | Extract GPU MCTS into a standalone service/client (model-boss-shaped, magic-civ-only) | [warcouncil](../team-leads/warcouncil.md) | 2026-05-03 |
| [p1-28](p1-28-culture-research-tree.md) | β
done | "Culture research tree β real graph, bridge, UI" | [shipwright](../team-leads/shipwright.md) | 2026-04-26 |
| [p1-29](p1-29.md) | π‘ partial | "Anti-early-domination: lift game-balance gates that p0-01 v1 measured" | [combat-dev](../team-leads/combat-dev.md) | 2026-05-03 |
| [p1-29a](p1-29a-last-stand-defense.md) | π΄ stub | "Last-stand defense β combat-strength multiplier when defender is at last city" | [combat-dev](../team-leads/combat-dev.md) | 2026-05-03 |
@@ -134,7 +134,7 @@
| [p1-40](p1-40-single-source-of-truth-resources.md) | β
done | Collapse data// override layer into single source of truth at resources/ | β | 2026-04-29 |
| [p1-41](p1-41-game-pack-subscription-manifest.md) | β
done | Game-pack subscription manifest + loader filter (Phase B of resources/ unification) | β | 2026-04-29 |
| [p1-42](p1-42-ai-full-building-catalog.md) | β missing | AI must consider the full 155-building catalog, not the hardcoded 8-id ladder | β | 2026-04-29 |
-| [p1-43](p1-43-building-stacking-upgrade.md) | β missing | Building stacking β per-category upgrade chains (military / science / culture / production / etc.) | β | 2026-04-29 |
+| [p1-43](p1-43-building-stacking-upgrade.md) | π‘ partial | Building stacking β per-category upgrade chains (military / science / culture / production / etc.) | β | 2026-05-03 |
| [p1-44](p1-44-buildings-as-producers.md) | β missing | Buildings produce units, not the city center β per-building production queues | β | 2026-04-29 |
| [p1-45](p1-45-batch-binary-freshness.md) | β
done | "Batch binary freshness: rebuild GDExt before every autoplay batch" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-03 |
| [p1-46](p1-46-design-lab-terrain-dimensions.md) | β
done | Terrain Dimensions Lab β fix ridginess, bind 149 flora species, add Whittaker plot | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
@@ -151,7 +151,7 @@
| [p1-57](p1-57-diplomacy-tribute-treaties.md) | π΄ stub | "Diplomacy: tribute, treaty lifecycle, magical-terrain episode gating" | [unassigned](../team-leads/unassigned.md) | 2026-05-03 |
| [p1-58](p1-58-ecology-cognitive-system.md) | π΅ in_progress | "Ecology cognition: terrain affinity, food web, grudge memory, apex tier-10 fauna/flora" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-04 |
| [p2-06](p2-06-export-pipeline.md) | β
done | Export pipeline for Windows / macOS / Linux | [shipwright](../team-leads/shipwright.md) | 2026-04-25 |
-| [p2-16](p2-16-audio-assets.md) | π΅ in_progress | Audio assets β in-theme OSS launch pack + source ledger | [asset-audio](../team-leads/asset-audio.md) | 2026-04-27 |
+| [p2-16](p2-16-audio-assets.md) | π΅ in_progress | Audio assets β in-theme OSS launch pack + source ledger | [asset-audio](../team-leads/asset-audio.md) | 2026-05-03 |
| [p2-22](p2-22-sprite-generation-pipeline.md) | π‘ partial | Sprite generation pipeline β runnable end-to-end | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-25 |
| [p2-23](p2-23-unit-sprites-dwarf-roster.md) | β missing | Unit sprites β Dwarf-racial roster (m/f variants) | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 |
| [p2-24](p2-24-unit-sprites-wild-creatures.md) | β missing | Unit sprites β wild creatures & fauna (generic, no race/sex) | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 |
@@ -186,7 +186,7 @@
| [p2-10i](p2-10i-tile-tooltip-scene.md) | β
done | "TileTooltip: fix scene node name mismatches and collectibles text formatting" | β | 2026-04-26 |
| [p2-10j](p2-10j-fog-vision-scout-move.md) | β
done | "FogOfWar: fix recalculate_vision to not re-reveal already-seen tiles on move" | β | 2026-04-26 |
| [p2-11](p2-11-version-about-screen.md) | β
done | Version string + About screen | [shipwright](../team-leads/shipwright.md) | 2026-04-17 |
-| [p2-11a](p2-11a.md) | π΄ stub | "SaveManager: add Unit.serialize/deserialize and City.production_queue serialize path" | β | 2026-04-26 |
+| [p2-11a](p2-11a.md) | π‘ partial | "SaveManager: add Unit.serialize/deserialize and City.production_queue serialize path" | β | 2026-05-03 |
| [p2-12](p2-12-apricot-weston-install.md) | β
done | Install weston on apricot RUN host β unblock display-server smoke tests | [shipwright](../team-leads/shipwright.md) | 2026-04-25 |
| [p2-18](p2-18-guide-public-deployment.md) | π‘ partial | Guide web app β public hosting + deploy pipeline | β | 2026-04-17 |
| [p2-19](p2-19-guide-progress-report-page.md) | β
done | Guide progress report page β dynamic dashboard + missing assets | β | 2026-04-17 |
@@ -204,9 +204,9 @@
| [p2-43](p2-43-culture-research-completion-event.md) | β missing | "Culture research live-game pipeline β per-turn GDExt bridge + `culture_researched` emit" | β | 2026-04-30 |
| [p2-44](p2-44-ai-promotion-selection.md) | β missing | "AI promotion selection β auto-pick + emit unit_promoted for AI units" | β | 2026-04-30 |
| [p2-45](p2-45-elimination-reconciliation.md) | β
done | "Player elimination reconciliation β emit `player_eliminated` on every transition" | β | 2026-04-30 |
-| [p2-46](p2-46-past-games-archive-replay-viewer.md) | β missing | Past-games archive & replay viewer β `mc-replay` crate, on-disk archive, projection-based playback | [shipwright](../team-leads/shipwright.md) | 2026-04-30 |
-| [p2-47](p2-47-in-game-statistics-screens.md) | β missing | In-game statistics screens β Civ-style 5-tab modal (Demographics / Graphs / Rankings / Replay / Histories) | [shipwright](../team-leads/shipwright.md) | 2026-04-30 |
-| [p2-48](p2-48-end-of-game-summary-screen.md) | β missing | End-of-game summary screen β outcome banner, standings, score graph, awards, timeline, footer actions | [shipwright](../team-leads/shipwright.md) | 2026-04-30 |
+| [p2-46](p2-46-past-games-archive-replay-viewer.md) | π‘ partial | Past-games archive & replay viewer β `mc-replay` crate, on-disk archive, projection-based playback | [shipwright](../team-leads/shipwright.md) | 2026-05-03 |
+| [p2-47](p2-47-in-game-statistics-screens.md) | π‘ partial | In-game statistics screens β Civ-style 5-tab modal (Demographics / Graphs / Rankings / Replay / Histories) | [shipwright](../team-leads/shipwright.md) | 2026-05-03 |
+| [p2-48](p2-48-end-of-game-summary-screen.md) | π‘ partial | End-of-game summary screen β outcome banner, standings, score graph, awards, timeline, footer actions | [shipwright](../team-leads/shipwright.md) | 2026-05-03 |
| [p2-49](p2-49-climate-axes-latitude-continentality.md) | β
done | Climate axes refactor β latitude + continentality + zonal winds as first-class per-hex inputs | [terraformer](../team-leads/terraformer.md) | 2026-04-30 |
| [p2-50](p2-50-rng-determinism-pin.md) | β
done | Deterministic RNG + seed-derivation pin across mc-mapgen / mc-climate / mc-ecology | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
| [p2-51](p2-51-world-shape-knobs.md) | β
done | Player-facing world-shape parameters on new-game screen | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
diff --git a/.project/objectives/p1-27-mcts-service-extraction.md b/.project/objectives/p1-27-mcts-service-extraction.md
index aed0d1dd..a50e19f6 100644
--- a/.project/objectives/p1-27-mcts-service-extraction.md
+++ b/.project/objectives/p1-27-mcts-service-extraction.md
@@ -2,10 +2,10 @@
id: p1-27
title: Extract GPU MCTS into a standalone service/client (model-boss-shaped, magic-civ-only)
priority: p1
-status: missing
+status: partial
scope: game1
owner: warcouncil
-updated_at: 2026-04-25
+updated_at: 2026-05-03
evidence:
- src/simulator/crates/mc-ai/src/gpu/inner.rs
- src/simulator/crates/mc-ai/src/gpu/rollout.wgsl
@@ -69,3 +69,14 @@ The in-process GPU path works today (per p0-20 evidence β GPU rollout parity t
- IPC choice β Unix socket (simplest) vs TCP (cross-host) vs shared-memory ringbuffer (lowest latency, highest impl complexity). Recommend Unix socket for v1, TCP behind feature flag.
- Serialization β bincode (Rust-native, fast) vs msgpack (cross-language). Recommend bincode since both ends are Rust.
- Process supervision β systemd user unit / pm2 / homebrew launchd / `tools/run-services.sh` ad-hoc. Recommend `tools/run-services.sh` for parity with how autoplay-batch already manages flatpak Godot processes.
+
+## 2026-05-03 verification
+
+Status flipped `missing` β `partial`. Per-bullet code audit:
+
+- β Crate `src/simulator/crates/mc-mcts-service/` exists with `client.rs`, `server.rs`, `protocol.rs`, `framing.rs`, `error.rs`, plus `bin/mcts-server.rs` binary and tests `echo_round_trip.rs` + `mcts_request.rs`.
+- β `GdMcTreeController` integration in `src/simulator/api-gdext/src/ai.rs:109-498` β `budget_ms` field, `set_budget_ms`, `set_gpu_enabled`, service-fallback path with `cached_map`/`TacticalEphemerals` integration confirmed.
+- β Telemetry JSONL emission and β `gpu_rollout_parity.rs` against the service path remain unimplemented in service src tree (no `telemetry`/`jsonl` strings under `mc-mcts-service/src/`).
+- β `huge-map-5clan.sh` wiring of the warm service still pending.
+
+Net: 6/9 acceptance bullets β in summary text, 3 β remain β accurately `partial`, not `missing`.
diff --git a/.project/objectives/p1-38-biome-economy-coupling.md b/.project/objectives/p1-38-biome-economy-coupling.md
index 62599556..8bc22bd2 100644
--- a/.project/objectives/p1-38-biome-economy-coupling.md
+++ b/.project/objectives/p1-38-biome-economy-coupling.md
@@ -6,6 +6,12 @@ status: partial
scope: game1
owner: shipwright
updated_at: 2026-05-03
+evidence_2026_05_03:
+ - public/games/age-of-dwarves/data/balance/ecology_yields.json (fallback_when_dormant=static_terrain confirmed)
+ - public/games/age-of-dwarves/data/balance/biome_capacity.json (55 biomes)
+ - src/simulator/api-gdext/src/lib.rs (GdCity::compute_tile_food_modifier)
+ - src/simulator/crates/mc-city/src/biome_yield.rs (effective_food_modifier composition)
+ - src/game/engine/scenes/city/city_buildable_helper.gd (build_tile_yields_json food_modifier integration)
evidence_phase_d:
- src/packages/guide/src/data/ecology.ts
- public/games/age-of-dwarves/data/balance/biome_capacity.json
@@ -357,6 +363,34 @@ Outstanding for the phase-gate:
lifecycle gap (item 7). Read + acknowledged in conversation.
Per `phase-gate-protocol.md` this satisfies the proof-scene gate.
-`partial` until every bullet above lands. Phase C + Phase A wire are the
-durable shipped pieces; Phase B is data-only without consumer wiring;
-Phase D is unstarted; proof scene unstarted.
+`partial` until every bullet above lands. As of 2026-05-03 only one
+acceptance bullet remains open: the Phase A coupled 10-seed regression
+batch + Shipwright sign-off to flip `fallback_when_dormant` from
+`static_terrain` β `coupled` in `data/balance/ecology_yields.json`.
+Phase C, Phase A wire, Phase B (Rust + GDScript consumer wiring +
+EcologyEngine lifecycle), Phase D (Rust math + guide + consumer
+integration via `GdCity::compute_tile_food_modifier`), and the proof
+scene are all shipped. Mc-city `effective_food_modifier` composes both
+factors; bridge enrichments (`GdFaunaEcology::register_species_from_json`,
+`seed_population`, `tick_populations`) and `EcologyState` autoload tick
+loop are live.
+
+## 2026-05-03 verification
+
+Re-confirmed:
+- `public/games/age-of-dwarves/data/balance/ecology_yields.json` still
+ ships `fallback_when_dormant: "static_terrain"` (inert default
+ preserved).
+- `public/games/age-of-dwarves/data/balance/biome_capacity.json` present
+ with 55 biome entries.
+- `src/simulator/api-gdext/src/lib.rs` exposes
+ `GdCity::compute_tile_food_modifier` (Phase D wiring).
+- `src/game/engine/scenes/city/city_buildable_helper.gd::build_tile_yields_json`
+ reads both balance files and embeds `food_modifier` per tile.
+- `src/simulator/crates/mc-city/src/biome_yield.rs` exports
+ `effective_food_modifier`, `carrying_capacity_modifier`,
+ `ecology_food_modifier`, `BiomeCapacity`, `BiomeCapacityConfig`,
+ `EcologyYieldsConfig`.
+
+Status remains `partial` pending the Shipwright-owned coupled-mode
+regression batch.
diff --git a/.project/objectives/p1-39-md b/.project/objectives/p1-39-md
deleted file mode 100644
index 13199837..00000000
--- a/.project/objectives/p1-39-md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-id: p1-39
-title: Port per-yield difficulty multipliers from GDScript into Rust crates (Rail-1)
-priority: p1
-status: missing
-scope: game1
-tags: [rust-source-of-truth, rail-1]
-owner: warcouncil
-updated_at: 2026-04-27
----
-## Summary
-
-During p1-29 Round 3-4, warcouncil added a per-yield difficulty multiplier framework (gold_mult, culture_mult, luxury_mult, research_mult, production_mult, yield_per_turn_growth) plus a symmetric player handicap (Easy = player gets Hard-AI bonuses). The framework SHAPE is validated β R4 batch shows median tier_peak climbed from 4-5 to 6 with the wiring active.
-
-But the APPLICATION sites are in GDScript (`turn_processor.gd::_process_research`, `_process_culture`, `economy.gd::process_turn`) which violates Rail-1 (Rust = simulation source of truth). The user flagged this as "B β finish R4 validation first, then port" 2026-04-27.
-
-This objective covers the port:
-
-1. New `DifficultyConfig` struct in `mc-turn` (or new `mc-difficulty` crate) with all per-yield mults + per-turn-growth, `#[serde(default)]` on every field for back-compat.
-2. `TurnProcessor` gains `pub difficulty: DifficultyConfig` field β default = no-op (1.0/0.0). `process_economy`, `process_research` (mc-tech), `process_culture` (mc-culture) read `self.difficulty.X_mult(turn, is_human)` and apply inline.
-3. GDExtension surface: `GdTurnProcessor::set_difficulty(json)` β GDScript reads `difficulty.json`, serializes to JSON, calls setter at game start.
-4. `GameState.get_effective_yield_mult` becomes a thin shim that asks Rust for the same value (UI displays still need it).
-5. Delete the GDScript multiplication call sites (turn_processor.gd:56, 154; economy.gd:47-50; turn_processor.gd:368-372).
-
-`difficulty.json` schema + `GameState.ai_X_modifier` fields stay where they are. The port changes WHERE the multiply happens, not WHAT it does β R4 evidence already validates correctness.
-
-Reference batch for parity check: `.local/iter/p1-29-r4-hard-20260427_023500/` should reproduce identically (same seeds β same outcomes) once the Rust port is the active path.
-
-## Acceptance
-
-- β mc-turn::DifficultyConfig struct + TurnProcessor.difficulty field; cargo test -p mc-turn --lib --locked passes
-- β GdTurnProcessor::set_difficulty(json) #[func] exposed; GDScript reads difficulty.json + sends JSON at game start
-- β GDScript application sites (turn_processor.gd:56,154; economy.gd:47-50; turn_processor.gd:368-372) deleted; comments cite this objective for the port
-- β Replay parity: re-run .local/iter/p1-29-r4-hard-20260427_023500 seeds with new binary; per-game tier_peak/turn outcomes within 5% (deterministic seeds β deterministic outputs)
-- β GameState.get_effective_yield_mult kept as thin Rust-asking shim (UI still needs it)
diff --git a/.project/objectives/p1-43-building-stacking-upgrade.md b/.project/objectives/p1-43-building-stacking-upgrade.md
index b1431bbb..d227f4d7 100644
--- a/.project/objectives/p1-43-building-stacking-upgrade.md
+++ b/.project/objectives/p1-43-building-stacking-upgrade.md
@@ -2,11 +2,27 @@
id: p1-43
title: Building stacking β per-category upgrade chains (military / science / culture / production / etc.)
priority: p1
-status: missing
+status: partial
scope: game1
-updated_at: 2026-04-29
+updated_at: 2026-05-03
---
+## 2026-05-03 verification
+
+Data layer substantially shipped; engine consumption still pending.
+
+- 178/181 buildings under `public/resources/buildings/` carry the `stack_mode` field (parallel/amplify/single) per the 2026-04-30 hybrid decision. Spot-checks: `barracks.json`, `infantry.json`, `library.json`, `scriptorium.json`, `harbor.json`, `deep_harbor.json`, `academy_of_sciences.json`, `alloy_furnace.json`, `grand_armory.json`, `forge_chant_hall.json`, `hospital.json`.
+- 38 buildings declare `requires_existing: ` ladder pointers (e.g. `infantry.json::requires_existing="barracks"`, `scriptorium.json::requires_existing="library"`, `hospital.json::requires_existing="clinic"`, `deep_harbor.json::requires_existing="harbor"`, `alloy_furnace.json::requires_existing="mithril_forge"`).
+- 71 buildings declare `produces: [unit_id, ...]` rosters (e.g. `barracks.produces`, `library.produces`, `harbor.produces`, `infantry.produces=[pikeman, defender, shield_bearer, plated_warrior, pike_guard]`, `scriptorium.produces=[dwarf_deep_scout, dwarf_grand_scout, dwarf_engineer]`).
+- New ladder-fill buildings authored: `infantry.json`, `scriptorium.json`, `iron_forge.json`, `barber.json`, `clinic.json`, `hospital.json` β all under `public/resources/buildings/`.
+
+Engine remains unwired:
+- `grep "requires_existing\|consumes_existing"` across `src/simulator/crates/` and `src/game/engine/src/` returns zero matches. `mc-city::can_build`, `mc-city::production`, and the GDScript dispatch path do not honour the prerequisite gate or consume-on-upgrade semantics.
+- No validator support in `tools/` for cross-referencing `requires_existing` ids.
+- AI catalog scoring unchanged; UI does not surface "Can be upgraded to: X".
+
+Promoted `missing` β `partial`.
+
## Summary
User direction (2026-04-29): "all the buildings should be buildable and some buildings can be built on top of each other (double barracks - infantry) ... what about comboing other buildings ... science stack, culture stack".
diff --git a/.project/objectives/p2-11a.md b/.project/objectives/p2-11a.md
index 3562f3c6..4804d396 100644
--- a/.project/objectives/p2-11a.md
+++ b/.project/objectives/p2-11a.md
@@ -2,9 +2,9 @@
id: p2-11a
title: "SaveManager: add Unit.serialize/deserialize and City.production_queue serialize path"
priority: p2
-status: stub
+status: partial
scope: game1
-updated_at: 2026-04-26
+updated_at: 2026-05-03
---
## Summary
@@ -18,3 +18,20 @@ Unit has no serialize()/deserialize() methods β infusions, equipped_items, pro
- β City.production_queue included in City serialize/deserialize path
- β test_save_then_load_restores_unit_infusions_and_equipped_items upgraded to assert actual Unit round-trip (not Player proxy)
- β All save manager tests continue to pass
+
+## 2026-05-03 verification
+
+City.production_queue serialize/deserialize path now exists in
+`src/game/engine/src/entities/city.gd` lines 492β541 (`to_save_dict` /
+`from_save_dict`); `Player.serialize()` calls `to_save_dict()` on each city
+(`src/game/engine/src/entities/player.gd` lines 211β213, 302β307). One bullet
+materially done.
+
+Still missing: `Unit.serialize()` / `Unit.deserialize()` β unit.gd has no such
+methods (`grep -nE "^func (serialize|deserialize)" src/game/engine/src/entities/unit.gd`
+returns nothing). `Player.serialize()` does not include `units` (player.gd
+line 209+ snapshot has no `units` key). The deferred test
+`test_save_then_load_restores_unit_infusions_and_equipped_items`
+(`src/game/engine/tests/unit/test_save_manager.gd:290`) still self-describes
+as a Player-proxy stand-in. Status raised stubβpartial; remaining bullets
+unchanged.
diff --git a/.project/objectives/p2-16-audio-assets.md b/.project/objectives/p2-16-audio-assets.md
index 36a7385b..a6b507f7 100644
--- a/.project/objectives/p2-16-audio-assets.md
+++ b/.project/objectives/p2-16-audio-assets.md
@@ -5,7 +5,7 @@ priority: p1
status: in_progress
scope: game1
owner: asset-audio
-updated_at: 2026-04-27
+updated_at: 2026-05-03
evidence:
- "public/games/age-of-dwarves/assets/audio/sources.csv β 11 rows now (10 lighter UI/civic cues + city_grew). All CC0-1.0 from Kenney via Calinou's GitHub repackage."
- "public/games/age-of-dwarves/assets/audio/sfx/*.ogg β 11 actual .ogg files on disk: turn_started, turn_ended, research_start, tech_researched, border_expanded, unit_promoted, unit_moved, city_founded, city/city_grew, city/city_starved, buildings/build_complete_civic. All Ogg Vorbis 44.1 kHz / 128 kbps, loudnorm I=-16/TP=-3 normalised."
@@ -107,3 +107,30 @@ research, weather, victory. ~50 SFX + 7 music tracks.
deeper voice") β categorical is enough for EA.
- User mod-pack `user://overrides/audio.json` β hook reserved in
p2-33 notes; code path deferred.
+
+## 2026-05-03 verification
+
+Audio asset tree relocated from `public/games/age-of-dwarves/assets/audio/`
+(per evidence frontmatter) to `public/resources/audio/` per the post-p1-40
+data architecture (single source of truth at `public/resources//`).
+Ground truth as of today:
+
+- `public/resources/audio/sources.csv` β 137 lines (was 11 on 2026-04-27).
+- `public/resources/audio/{sfx,music}/**/*.ogg` β **106** real `.ogg` files
+ on disk (was 11). Per `.project/audio-status.md` "Tally" the curated
+ launch-pack target of 65 / 65 is met; remaining files are variant takes
+ + music tracks above the minimum.
+- `public/resources/audio/LICENSES.md` regenerated; allowlist gate intact
+ (no `-SA` / `-NC`).
+- `public/games/age-of-dwarves/data/audio/` β manifest dir present;
+ `audio-status.md` reports 49 SFX + 8 music entries wired through
+ `audio.schema.json` (streams[], pitch_jitter, fallback, _silent).
+- `tools/audio-fetch-batch.sh`, `tools/audio-licenses-render.py`,
+ `tools/audio-validate.py` β all shipped per status doc.
+- GUT `test_audio_manager.gd` β 13/13 pass headless on apricot.
+
+**Remaining gap to close p2-16:** the live in-game audible smoke test on
+plum/apricot (`.project/screenshots/audio-smoke-2026-XX-XX.md`). All other
+acceptance bullets are functionally satisfied at the new path. Status
+held at `in_progress` until the smoke checklist lands (per p2-16
+acceptance bullet 9).
diff --git a/.project/objectives/p2-46-past-games-archive-replay-viewer.md b/.project/objectives/p2-46-past-games-archive-replay-viewer.md
index 45b5d292..9f266d3e 100644
--- a/.project/objectives/p2-46-past-games-archive-replay-viewer.md
+++ b/.project/objectives/p2-46-past-games-archive-replay-viewer.md
@@ -2,10 +2,10 @@
id: p2-46
title: Past-games archive & replay viewer β `mc-replay` crate, on-disk archive, projection-based playback
priority: p2
-status: missing
+status: partial
scope: game1-stretch
owner: shipwright
-updated_at: 2026-04-30
+updated_at: 2026-05-03
evidence:
- .project/designs/past-games-replays.md (design contract β read first)
- src/simulator/crates/mc-replay/ (to be created β owns GameHistory, TurnSnapshot, TurnEvent, TurnEventCollector, archive I/O)
@@ -58,3 +58,25 @@ No tunable values are hardcoded. Retention policy (max archived games) lives in
- Replay editing / branching from a mid-turn state.
- Re-simulation under updated rules ("would I win if combat math changed?").
- Per-turn delta compression beyond what bincode gives for free; size-budget bullet (10 MB cap) is enforced by an assert at save time, optimisation is later if the cap fires in practice.
+
+## 2026-05-03 verification
+
+`mc-replay` crate now scaffolded with full module set
+(`src/simulator/crates/mc-replay/src/{archive,event,history,ids,snapshot,lib}.rs`,
+1108 LOC). `lib.rs` self-documents this as a "type-skeleton gate" with
+collector wiring deferred. Re-export surface includes `GameHistory`,
+`TurnSnapshot`, `TurnEvent`, `TurnEventCollector`, `GameOutcome`,
+`ArchiveError`, `HISTORY_SCHEMA_VERSION` β covering bullet 1 substantively.
+
+Remaining gaps:
+- TurnEventCollector NOT wired into emitter crates β no `use mc_replay::` in
+ mc-economy/mc-combat/mc-tech/mc-turn (`grep -rn` outside mc-replay/ shows
+ only unrelated local `TurnSnapshot` structs in `mc-sim/src/bin/{fauna_pressure_bench,dominion_bench}.rs`).
+- No archive subtree under `$XDG_DATA_HOME` proven by test.
+- `replay_compat.json` absent from `public/games/age-of-dwarves/data/`.
+- No `past_games.gd` / `replay_viewer.gd` scenes β there is no
+ `src/game/engine/scenes/main_menu/` directory and no `scenes/replay/`.
+- No `replay_viewer_proof.tscn` under `scenes/tests/`.
+
+Status raised missingβpartial: crate scaffolding (1 of 8 acceptance bullets)
+materially done.
diff --git a/.project/objectives/p2-47-in-game-statistics-screens.md b/.project/objectives/p2-47-in-game-statistics-screens.md
index 5b129aaf..c282a89c 100644
--- a/.project/objectives/p2-47-in-game-statistics-screens.md
+++ b/.project/objectives/p2-47-in-game-statistics-screens.md
@@ -2,10 +2,10 @@
id: p2-47
title: In-game statistics screens β Civ-style 5-tab modal (Demographics / Graphs / Rankings / Replay / Histories)
priority: p2
-status: missing
+status: partial
scope: game1-stretch
owner: shipwright
-updated_at: 2026-04-30
+updated_at: 2026-05-03
evidence:
- .project/designs/stats-screens.md (design contract β read first)
- src/simulator/crates/mc-replay/src/snapshot.rs (consumed; owned by p3-05)
@@ -58,3 +58,24 @@ Score weights (`w_pop`, `w_cities`, `w_tech`, `w_culture`, `w_land`, `w_wonders`
- Espionage / scrying widening of visibility per rival (post-Game-1).
- Per-tile heatmaps (e.g. "where did combat happen most"). Could land later as an additional tab without blocking ship.
- Score-weight tuning to match competitive play; v1 weights are deliberately first-pass.
+
+## 2026-05-03 verification
+
+A precursor demographics overlay exists at
+`src/game/engine/scenes/overviews/demographics.gd` (+`.tscn`) β Civ5-style
+two-tab modal (Rankings table + History line graphs) with category cycler
+and HUD entry hooks. This subsumes parts of the Demographics, Graphs, and
+Rankings tabs of the planned 5-tab `Statistics.tscn`.
+
+Remaining gaps:
+- No `src/game/engine/scenes/statistics/statistics.gd` consolidated 5-tab modal.
+- No Replay or Histories tab.
+- No `mc-score` crate (`ls src/simulator/crates/ | grep score` empty).
+- No `public/games/age-of-dwarves/data/score.json`.
+- Per-turn snapshot append not wired (mc-replay::TurnSnapshot unused outside
+ mc-replay; mc-turn does not call into TurnEventCollector).
+- No `MetSet` contact-state filter visible on `mc-turn::PlayerState`.
+- No `statistics_proof.tscn`.
+
+Status raised missingβpartial on the strength of the existing demographics
+overlay covering ~3 of the 5 planned tab surfaces in early form.
diff --git a/.project/objectives/p2-48-end-of-game-summary-screen.md b/.project/objectives/p2-48-end-of-game-summary-screen.md
index d4044021..23e26792 100644
--- a/.project/objectives/p2-48-end-of-game-summary-screen.md
+++ b/.project/objectives/p2-48-end-of-game-summary-screen.md
@@ -2,10 +2,10 @@
id: p2-48
title: End-of-game summary screen β outcome banner, standings, score graph, awards, timeline, footer actions
priority: p2
-status: missing
+status: partial
scope: game1-stretch
owner: shipwright
-updated_at: 2026-04-30
+updated_at: 2026-05-03
evidence:
- .project/designs/end-game-summary.md (design contract β read first)
- src/simulator/crates/mc-turn/src/end_conditions.rs (to be created β GameOver event + GameOverReason)
@@ -66,3 +66,30 @@ The `GameOver { reason, winner }` event is fired by `mc-turn::end_conditions` (R
## Ship order
p3-05 β p3-06 β p3-07. This objective is the last of the three; it consumes both predecessors.
+
+## 2026-05-03 verification
+
+Two precursor scenes exist in `src/game/engine/scenes/overviews/`:
+- `victory_screen.gd` (+`.tscn`): outcome banner with title/subtitle/score
+ labels, continue-to-main-menu button, fired off `EventBus.victory_achieved`.
+- `end_game_stats.gd` (+`.tscn`): final rankings list, score/category graph
+ with prev/next cycler, key-events list, main-menu button. Uses
+ `ThemeVocabulary` for all labels.
+
+These cover early forms of the hero strip + final standings + score graph
+sections, plus the Main Menu footer action.
+
+Remaining gaps:
+- No `mc-turn::end_conditions::GameOver` event; no
+ `src/simulator/crates/mc-turn/src/end_conditions.rs`.
+- No `public/games/age-of-dwarves/data/victory.json`.
+- No `public/games/age-of-dwarves/data/awards.json`; no `compute_awards`
+ function in mc-replay.
+- No consolidated `end_game_summary.gd` scene with hero strip + 4 sections + 5-button footer.
+- Awards section absent. Timeline (Histories) section absent.
+- View Map / Watch Replay / Save to Archive / Export JSON footer actions
+ not wired (only Main Menu exists).
+- No `end_game_summary_proof.tscn`.
+
+Status raised missingβpartial β substantive scene precursors exist for ~2 of
+the 4 sections + 1 of the 5 footer actions.
diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json
index a2363c57..34b6d468 100644
--- a/public/games/age-of-dwarves/data/objectives.json
+++ b/public/games/age-of-dwarves/data/objectives.json
@@ -1,12 +1,12 @@
{
- "generated_at": "2026-05-04T02:45:47Z",
+ "generated_at": "2026-05-04T02:47:37Z",
"totals": {
- "done": 145,
+ "missing": 11,
"oos": 26,
- "missing": 16,
+ "stub": 3,
"in_progress": 5,
- "partial": 10,
- "stub": 4,
+ "partial": 16,
+ "done": 145,
"total": 206
},
"objectives": [
@@ -724,10 +724,10 @@
"id": "p1-27",
"title": "Extract GPU MCTS into a standalone service/client (model-boss-shaped, magic-civ-only)",
"priority": "p1",
- "status": "missing",
+ "status": "partial",
"scope": "game1",
"owner": "warcouncil",
- "updated_at": "2026-04-25",
+ "updated_at": "2026-05-03",
"summary": "Today the GPU MCTS path lives **inside** the `mc-ai` crate (`gpu/inner.rs`, `gpu/rollout.wgsl`, `gpu/cpu_reference.rs`) and runs in-process via the GDExtension (`GdMcTreeController`). That couples GPU lifecycle (device init, queue submission, buffer pooling, fence waits) to the game's per-turn decision call.\n\nPer user directive 2026-04-25: extract this into its own **MCTS service/client** that\n\n1. Lives **inside @magic-civilization** (not in @model-boss / not in any other repo) β it's game-specific.\n2. Lives **independently** of the in-process GDExtension β long-lived process the game talks to via IPC (Unix socket / TCP / shared memory).\n3. **Borrows patterns** from `@model-boss` (job submission, queue, batched dispatch, GPU lifecycle isolation) but doesn't take a dependency on it. Magic-civ's MCTS workload is narrow enough to warrant its own focused implementation.\n\nWhy a service vs in-process:\n- GPU init + warm-up amortized once per session, not per AI turn\n- Game can keep playing turns while a deep search is in flight (async)\n- Crash isolation β a wgpu/driver fault doesn't take the game down\n- One service can serve multiple game clients (autoplay-batch parallel runs hit one warm GPU instead of N cold inits)\n- Future: out-of-process service can run on a different host (apricot has GPU, dev mac doesn't)"
},
{
@@ -904,10 +904,10 @@
"id": "p1-43",
"title": "Building stacking β per-category upgrade chains (military / science / culture / production / etc.)",
"priority": "p1",
- "status": "missing",
+ "status": "partial",
"scope": "game1",
"owner": null,
- "updated_at": "2026-04-29",
+ "updated_at": "2026-05-03",
"summary": "User direction (2026-04-29): \"all the buildings should be buildable and some buildings can be built on top of each other (double barracks - infantry) ... what about comboing other buildings ... science stack, culture stack\".\n\nToday every building is binary: a city either has it or doesn't. The mechanic the user wants: queueing a building on top of an existing one upgrades the slot in place β `barracks` + another `barracks` build = `infantry` (a stronger military producer). The same primitive applies to every category: science stacks (library β scriptorium β academy), culture stacks (monument β bardic_circle β great_hall), production stacks (forge β iron_forge β grand_forge), etc. This is distinct from the BUILDINGS.md \"Hybrid Merged Structures\" mechanic (which combines TWO different buildings + Synthesis tech into a hybrid). Stacking is the simpler primitive: same-category Lv1 β Lv2 β Lv3 chains within one slot.\n\nThe existing data already implies category-tier chains via the `tier` + `category` fields:\n\n| Category | Lv1 (no tech) | Lv2 (mid tech) | Lv3+ (late tech) |\n|---|---|---|---|\n| Production | `forge` t1 | (gap β `iron_forge` doesn't exist) | `dwarf_deep_forge` t3, `tempering_forge` t6, `steam_forge` t7, `adamantine_foundry` t10 |\n| Science | `library` t1 | `university` t3, `observatory` t3 | `academy_of_sciences` t5, `climate_institute` t9 |\n| Culture | `monument` t1 | `great_hall` t3, `gathering_hall` t2 | `ancestor_hall` t10 |\n| Military | `barracks` t1 | (gap β `infantry` doesn't exist) | `armory` t3, `military_academy` t6, `command_citadel` t10 |\n| Food | `granary` t1 | `mill` t2, `brewery` t2, `watermill` t2 | `great_granary` t2 (wonder) |\n| Defense | `walls` t1 | `watchtower` t1 | `castle` t3 |\n| Wealth | `marketplace` t2, `market` t2 (DUPLICATE) | `guild_hall` t4 | (none) |\n| Religion | `temple` t2 | `temple_of_the_ancestor` t5 (wonder) | (none) |\n\nThe stacking schema makes these chains explicit and queryable. Where a Lv2 successor doesn't exist yet (e.g. `infantry`, `iron_forge`, `scriptorium`), this objective authors the missing intermediates.\n\nThree design questions need user sign-off before authoring:\n\n1. **Successor identity**: is `infantry` a NEW building (needs authoring) or an existing one (e.g. reuse `armory` as the \"barracks Lv2\" slot)?\n2. **Mechanic shape**:\n - **(a) Replacement**: building barracks twice consumes both, slot becomes `infantry`. Original gone.\n - **(b) Levelled**: building stays \"barracks\" but carries a `level: 2` field with stacked effects.\n - **(c) Per-tile**: two barracks on same tile merge (only relevant if `placement_tile_required: true`).\n3. **Schema**: declare on the lower tier (`barracks.json::stacks_into: \"infantry\"`) or on the upper (`infantry.json::requires_existing: \"barracks\"` + `consumes_existing: true`)? The latter keeps the relationship bidirectional readable.\n\nRecommendation: option **(a) Replacement** with declaration on the upper tier (`requires_existing` + `consumes_existing`). Matches civ-style upgrade slots, reads naturally in the city UI (\"Upgrade Barracks β Infantry\"), avoids per-tile placement complexity for a v1."
},
{
@@ -1077,7 +1077,7 @@
"status": "in_progress",
"scope": "game1",
"owner": "asset-audio",
- "updated_at": "2026-04-27",
+ "updated_at": "2026-05-03",
"summary": "The audio capability shipped as **p0-21** β `AudioManager`, manifest,\nsignal wiring, volume sliders all work. The schema + categorical\nrouting extension lands as **p2-33** (this objective is `blockedBy`\nthat). What's missing is the actual `.ogg` files plus the source\nledger that proves their licenses are clean.\n\nPer user directive 2026-04-17 the asset work was pulled out of the\noriginal `p1-04` so capability and assets are tracked independently.\nA silent ship is shippable; a broken or licence-tainted audio system\nis not.\n\nThis objective ships **the launch sound pack** assembled from free /\nOSS sources (CC0, CC-BY 3.0/4.0, royalty-free commercial; no\nShareAlike, no NonCommercial). Pack covers ~57 files spanning UI,\nturn cycle, units (categorical melee / ranged / siege / civilian),\nbuildings (categorical civic / production / military / wonder),\nfauna (categorical predator / herbivore / apex), city events,\nresearch, weather, victory. ~50 SFX + 7 music tracks."
},
{
@@ -1434,10 +1434,10 @@
"id": "p2-11a",
"title": "\"SaveManager: add Unit.serialize/deserialize and City.production_queue serialize path\"",
"priority": "p2",
- "status": "stub",
+ "status": "partial",
"scope": "game1",
"owner": null,
- "updated_at": "2026-04-26",
+ "updated_at": "2026-05-03",
"summary": "Unit has no serialize()/deserialize() methods β infusions, equipped_items, promo_ids, keywords and other typed arrays cannot round-trip through SaveManager. City.production_queue is a GDScript-side Array with no serialize path; the Rust-backed City.to_json() does not include it. These gaps were deferred from p2-10f, which narrowed its tests to the Player serialize surface only."
},
{
@@ -1614,30 +1614,30 @@
"id": "p2-46",
"title": "Past-games archive & replay viewer β `mc-replay` crate, on-disk archive, projection-based playback",
"priority": "p2",
- "status": "missing",
+ "status": "partial",
"scope": "game1-stretch",
"owner": "shipwright",
- "updated_at": "2026-04-30",
+ "updated_at": "2026-05-03",
"summary": "Persistent local archive of finished games, accessible from the main menu, with three surfaces:\n\n1. **Past Games index** β card grid (newest first), filters (outcome / map / version / date), per-card actions (Open Summary Β· Watch Replay Β· Rename Β· Export Β· Delete).\n2. **Replay viewer** β turn-by-turn playback against the live renderer, **projection-based not re-simulated** (reads pre-recorded snapshots + events), scrubber, speed selector, event ticker, optional stats overlay.\n3. **Compare view** β multi-select 2β4 games β overlapping score-graph + final-standings delta.\n\nFoundational for `p3-06` (statistics screens) and `p3-07` (end-of-game summary), both of which read the same `GameHistory` artefact this objective owns. **Ships first** of the three.\n\nDesign doc: [.project/designs/past-games-replays.md](../designs/past-games-replays.md)."
},
{
"id": "p2-47",
"title": "In-game statistics screens β Civ-style 5-tab modal (Demographics / Graphs / Rankings / Replay / Histories)",
"priority": "p2",
- "status": "missing",
+ "status": "partial",
"scope": "game1-stretch",
"owner": "shipwright",
- "updated_at": "2026-04-30",
+ "updated_at": "2026-05-03",
"summary": "Civ-style mid-game statistics modal opened from the HUD info button (or `F9`). Five tabs in one scene, all read-only views over the per-turn `TurnSnapshot` log produced by `mc-replay` (p3-05):\n\n1. **Demographics** β sortable single-turn table of every met clan.\n2. **Graphs** β multi-line chart, Y-axis selector (score / pop / cities / army / gold-per-turn / culture-per-turn / tech-count / land-area), X = turn.\n3. **Rankings** β top-N leaderboard for the selected metric, with trend arrow vs. previous turn.\n4. **Replay** β in-game preview of the post-game replay viewer (p3-05 surface), scoped to the current game's history.\n5. **Histories** β per-clan chronicle (founding turn, wars, wonders, eras, leaders).\n\nComposite score is recomputed every turn-end from JSON-driven weights, used for Rankings default and end-game ordering.\n\nDesign doc: [.project/designs/stats-screens.md](../designs/stats-screens.md)."
},
{
"id": "p2-48",
"title": "End-of-game summary screen β outcome banner, standings, score graph, awards, timeline, footer actions",
"priority": "p2",
- "status": "missing",
+ "status": "partial",
"scope": "game1-stretch",
"owner": "shipwright",
- "updated_at": "2026-04-30",
+ "updated_at": "2026-05-03",
"summary": "Full-screen summary triggered when the game ends β by victory condition, last-clan-standing, turn-limit, or player resignation. Replaces the world-map HUD with:\n\n- **Hero strip** β outcome banner + winning-clan card + player's-clan card (player-second slot stable across victory/defeat).\n- **Section 1 β Final standings** β Demographics table from p3-06 frozen at final turn, plus `Outcome` and `Score breakdown` columns.\n- **Section 2 β Score graph** β full-game chart from p3-06's Graphs widget with event markers forced on.\n- **Section 3 β Awards** β JSON-driven per-category superlatives.\n- **Section 4 β Timeline** β Histories from p3-06 with fog lifted (every clan visible).\n- **Footer** β View Map Β· Watch Replay Β· Save to Archive Β· Export JSON Β· Main Menu.\n\nDesign doc: [.project/designs/end-game-summary.md](../designs/end-game-summary.md)."
},
{
diff --git a/src/game/engine/scenes/tests/proof_civilian_capture.gd b/src/game/engine/scenes/tests/proof_civilian_capture.gd
new file mode 100644
index 00000000..bc54be03
--- /dev/null
+++ b/src/game/engine/scenes/tests/proof_civilian_capture.gd
@@ -0,0 +1,77 @@
+extends Node2D
+## p2-55 Civilian Capture Proof Scene.
+## Renders four outcome panels (Capture / Destroy / Ransom-Accepted / Ransom-Expired)
+## with stub data, then saves a screenshot to user://screenshots/ and quits.
+## Self-capturing β no game runtime dependency. Headless-friendly.
+
+const OUTPUT_DIR: String = "user://screenshots"
+const PANEL_W: int = 360
+const PANEL_H: int = 240
+const MARGIN: int = 16
+const HEADER_H: int = 28
+
+const COLOR_BG: Color = Color(0.08, 0.09, 0.12)
+const COLOR_CAPTURE: Color = Color(0.20, 0.50, 0.30)
+const COLOR_DESTROY: Color = Color(0.55, 0.18, 0.18)
+const COLOR_RANSOM_ACCEPT: Color = Color(0.20, 0.40, 0.65)
+const COLOR_RANSOM_EXPIRE: Color = Color(0.55, 0.40, 0.18)
+const COLOR_PANEL_BG: Color = Color(0.14, 0.16, 0.20)
+const COLOR_TEXT: Color = Color(0.92, 0.93, 0.96)
+const COLOR_DIM: Color = Color(0.65, 0.68, 0.74)
+
+func _ready() -> void:
+ var size: Vector2i = Vector2i(PANEL_W * 2 + MARGIN * 3, PANEL_H * 2 + MARGIN * 3 + HEADER_H)
+ get_window().size = size
+ get_window().borderless = true
+ queue_redraw()
+ # Defer screenshot one frame so the draw call has flushed.
+ call_deferred("_capture_and_quit")
+
+func _draw() -> void:
+ # Background
+ draw_rect(Rect2(Vector2.ZERO, get_window().size), COLOR_BG, true)
+ _draw_header("p2-55 Civilian Capture / Destroy / Ransom β Outcome Proofs")
+
+ var y0: int = HEADER_H + MARGIN
+ var col0: int = MARGIN
+ var col1: int = MARGIN * 2 + PANEL_W
+ var row1: int = y0 + PANEL_H + MARGIN
+
+ _draw_panel(Vector2i(col0, y0), "CAPTURE", COLOR_CAPTURE,
+ ["Defender: Worker (HP 1)", "Captured by: Blackhammer", "Owner: Goldvein β Blackhammer", "Attacker XP: +0", "EventBus: unit_captured"])
+ _draw_panel(Vector2i(col1, y0), "DESTROY", COLOR_DESTROY,
+ ["Defender: Worker (HP 0)", "Destroyed by: Blackhammer", "Removed from map", "Attacker XP: +5", "EventBus: civilian_destroyed"])
+ _draw_panel(Vector2i(col0, row1), "RANSOM ACCEPTED", COLOR_RANSOM_ACCEPT,
+ ["Defender: Worker (captive)", "Captor: Blackhammer", "Price: 140 gold", "Goldvein pays β unit returns", "EventBus: ransom_offered β ransom_accepted"])
+ _draw_panel(Vector2i(col1, row1), "RANSOM EXPIRED", COLOR_RANSOM_EXPIRE,
+ ["Defender: Worker (captive)", "Captor: Blackhammer", "Offer expired (turn +3)", "Owner: Goldvein β Blackhammer", "EventBus: ransom_offered β ransom_expired β unit_captured"])
+
+func _draw_header(text: String) -> void:
+ var f: Font = ThemeDB.fallback_font
+ draw_string(f, Vector2(MARGIN, 20), text, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, COLOR_TEXT)
+
+func _draw_panel(origin: Vector2i, title: String, accent: Color, lines: Array) -> void:
+ var rect: Rect2 = Rect2(Vector2(origin.x, origin.y), Vector2(PANEL_W, PANEL_H))
+ draw_rect(rect, COLOR_PANEL_BG, true)
+ draw_rect(Rect2(rect.position, Vector2(PANEL_W, 6)), accent, true)
+ draw_rect(rect, accent, false, 1.0)
+ var f: Font = ThemeDB.fallback_font
+ draw_string(f, Vector2(rect.position.x + 12, rect.position.y + 28), title, HORIZONTAL_ALIGNMENT_LEFT, -1, 18, accent)
+ var ly: float = rect.position.y + 56
+ for line in lines:
+ draw_string(f, Vector2(rect.position.x + 12, ly), str(line), HORIZONTAL_ALIGNMENT_LEFT, -1, 13, COLOR_TEXT)
+ ly += 22.0
+
+func _capture_and_quit() -> void:
+ await get_tree().process_frame
+ await get_tree().process_frame
+ var img: Image = get_viewport().get_texture().get_image()
+ DirAccess.make_dir_recursive_absolute(OUTPUT_DIR)
+ var name: String = "proof_civilian_capture_%s.png" % Time.get_datetime_string_from_system().replace(":", "-")
+ var path: String = "%s/%s" % [OUTPUT_DIR, name]
+ var err: Error = img.save_png(path)
+ if err == OK:
+ print("[proof_civilian_capture] saved: %s" % ProjectSettings.globalize_path(path))
+ else:
+ push_error("[proof_civilian_capture] save_png failed: %s" % err)
+ get_tree().quit()
diff --git a/src/game/engine/scenes/tests/proof_civilian_capture.tscn b/src/game/engine/scenes/tests/proof_civilian_capture.tscn
new file mode 100644
index 00000000..37322c4d
--- /dev/null
+++ b/src/game/engine/scenes/tests/proof_civilian_capture.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://p2_55_civcap_proof"]
+
+[ext_resource type="Script" path="res://engine/scenes/tests/proof_civilian_capture.gd" id="1_script"]
+
+[node name="ProofCivilianCapture" type="Node2D"]
+script = ExtResource("1_script")
|