fix(@projects/@magic-civilization): 🐛 update bunker status and acceptance criteria

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-06-09 23:11:43 -07:00
parent d8288b2a9d
commit da5f138c23
2 changed files with 75 additions and 9 deletions

View file

@ -2,9 +2,9 @@
id: p2-76
title: Bunker improvement — deposit-destroying fortified subterranean chamber
priority: p2
status: missing
status: partial
scope: game1
updated_at: 2026-06-06
updated_at: 2026-06-09
blocked_by: [p2-75, p2-80]
---
@ -57,13 +57,40 @@ contents and *contaminate* a tile.
## Acceptance
- ◻ Bunker is buildable only on `hills` / `mountains` and only with `pneumatic_construction` researched (gate read from JSON, per Rail 2).
- ◻ On completion, the bunker applies `defense_bonus: 100` and `concealed_from_surface: true` via the `p2-75` subsystem.
- ◻ `destroys_deposit`: completion records the tile's flat index in a new `destroyed_deposits: BTreeSet<u32>` (or equivalent) overlay; `mc-city/yield_fold.rs` consults it and yields zero collectible from that tile thereafter. The underlying `CollectiblesIndex` is **not** mutated.
- ◻ `surface_contamination`: completion records the tile in a contamination overlay with `contamination_turns = max(min_turns, destroyed_deposit_tier × turns_per_tier)`; while active the tile is `yields_zeroed_and_unworkable`. Overlay decays per turn; self-heals at 0 (fixed-duration model, no class engine yet — that is `p2-77`).
- ◻ **River-gap guard (INITIAL)**: the bunker is forbidden on a tile that would dam a river (a river-edge tile bridging upstream/downstream) until the runtime hydrology re-solve (`p2-78`) lands. Document this as a temporary build restriction, removed by `p2-78`.
- ◻ Serde round-trip: a `GameState` with a completed bunker (destroyed deposit + active contamination) round-trips cleanly; both overlays `#[serde(default)]`.
- ◻ `cargo test` green (build-gate, deposit-destruction overlay, contamination-duration, yield-zeroing, serde round-trip), headless GUT green, proof-scene screenshot of a built bunker (defense + concealment + dead surface tile) reviewed.
- ◐ **Bunker buildable only on `hills` / `mountains` + `pneumatic_construction`** (gate read from JSON, per Rail 2). The terrain/tech gate is in `bunker.json` (`valid_terrain`, `requires_tech`) and read by the existing GDScript build-validity path. **River-gap guard added in Rust** (see below). The terrain/tech build-validity enforcement is a GDScript-presentation concern not re-verified this pass — partial.
- ✓ **Defense + concealment via p2-75 — DONE (2026-06-09).** `complete_improvement` writes the bunker anchor mirroring `effects` (`defense_bonus: 100`, `concealed_from_surface: true`); the p2-75 combat/vision path consumes them. Verified by `bunker_completion_records_destroyed_deposit_and_pending_terraform` (asserts the mirrored effects) + the gdext `tile_improvement_defense_bonus`/`tile_improvement_concealed` bridges.
- ✓ **`destroys_deposit` overlay + yield-zeroing — DONE (2026-06-09).** `ImprovementEffects` extended with `destroys_deposit: bool` (`#[serde(default)]`). On completion, `GameState::complete_improvement` records the tile's flat index (`col * grid_height + row`) in the new global `destroyed_deposits: BTreeSet<u32>` overlay; `mc-city/yield_fold.rs::tile_yields_from_collectibles_suppressed` consults a suppressed-coords set and yields zero collectible. The `CollectiblesIndex` is never mutated. **Verified:** `bunker_completion_records_destroyed_deposit_and_pending_terraform`, `non_destroying_improvement_records_no_overlay`, `p2_76_suppressed_tile_yields_zero_collectible` (all green on apricot).
- ✓ **`surface_contamination` overlay + decay + self-heal — DONE (2026-06-09).** `SurfaceContaminationSpec` (+ `duration_for_tier(tier) = max(min_turns, tier × turns_per_tier)`) parsed into `ImprovementEffects.surface_contamination`. Completion enqueues a `TerraformEvent` (tier SNAPSHOTTED from the persisted tile `quality` — never re-derived from seed). `WorldSim::step` sub-step 1b seeds a `TileContamination { remaining_turns, source_tier }` on the `WorldSim` `contamination_map` overlay + chronicles it; sub-step 4b decays every active contamination and removes (self-heals) at 0, rebuilding the derived `GameState::unworkable_tiles` mirror. **Verified:** `contamination_duration_scales_with_destroyed_tier`, `terraform_seeds_contamination_then_decays_and_self_heals` (seeds tier-3 → 30 turns, decays to self-heal, asserts the unworkable mirror + chronicle entry), green on apricot.
- ✓ **River-gap guard (INITIAL) — DONE (2026-06-09).** `GameState::bunker_river_gap_blocked(col, row)` returns true when the tile carries any `river_edges` (a river course). Bridged as `GdGameState::bunker_river_gap_blocked` for the build-validity path. Documented as temporary — removed by `p2-78`. **Verified:** `bunker_river_gap_guard_blocks_river_course_tiles`.
- ✓ **Serde round-trip — DONE (2026-06-09).** `destroyed_deposits` is `#[serde(default)]` on `GameState` and survives a full `GameState` serde round-trip (`destroyed_deposits_overlay_round_trips_serde`). The contamination overlay round-trips losslessly via the `worldsim_state`-envelope pairs form + `restore_contamination_map` (`contamination_map_round_trips_serde`). `pending_terraform` is `#[serde(skip)]` (drained each turn, design Q3) and is empty on load.
- ◐ **`cargo test` green + headless GUT + proof-scene screenshot.** Cargo DONE — `cargo test -p mc-core -p mc-state -p mc-city -p mc-turn -p mc-worldsim` green on apricot (incl. all 6 new bunker tests + 3 worldsim contamination tests + the yield-suppression test); `validate-game-data` 1103/0. The **proof-scene screenshot of a built bunker (defense + concealment + dead surface tile) is NOT yet captured** — blocks `done`. (Determinism preserved: `p2_76_substeps_are_noop_without_terraform` confirms 1b/4b are no-ops without a pending terraform, so the worldsim golden vector is unperturbed.)
## Verification note (2026-06-09, apricot — p2-76 bridgehead built)
Built per the design `.project/designs/p2-76-79-terraforming-cascade-design.md`
Increment 1, with the RECOMMENDED defaults for the open questions:
- **Q1 (overlay home):** split by lifetime — `destroyed_deposits` (permanent,
one-shot) on `GameState`; contamination (per-turn-evolving) on the `WorldSim`
side-structure; derived `unworkable_tiles` mirror on `GameState` for the yield
seam. Implemented as recommended.
- **Q3 (pending_terraform persistence):** `#[serde(skip)]` — completion + 1b
application happen in the same `WorldSim::step`, so the queue never crosses a
save boundary. Implemented as recommended.
**Files:** `mc-core/src/improvement.rs` (`destroys_deposit` +
`SurfaceContaminationSpec`), `mc-state/src/game_state.rs` (`destroyed_deposits`,
`pending_terraform`, `unworkable_tiles`, `TerraformEvent`, deposit-destruction in
`complete_improvement`, `tile_flat_index`, `bunker_river_gap_blocked`),
`mc-city/src/yield_fold.rs` (`tile_yields_from_collectibles_suppressed`),
`mc-ecology/src/tile.rs` (`TileContamination`), `mc-worldsim/src/lib.rs`
(`contamination_map`, 1b `apply_pending_terraform`, 4b `tick_contamination`,
`restore_contamination_map`), `api-gdext/src/lib.rs` (`is_deposit_destroyed`,
`is_tile_unworkable`, `bunker_river_gap_blocked` bridges).
**Remaining for `done`:** the proof-scene screenshot (phase-gate). The Rust path
+ overlays + determinism are complete and tested; the proof scene + the GDScript
build-validity enforcement of the terrain/tech gate are the open presentation
work.
## Non-goals

View file

@ -5470,6 +5470,45 @@ impl GdGameState {
}
}
/// p2-76 — true iff the deposit at `(col, row)` has been permanently
/// destroyed (by a bunker). Reads the `destroyed_deposits` overlay flat
/// index. The yield path consults this so a destroyed-deposit tile yields no
/// collectible. False when there is no grid or the tile is off-map.
#[func]
pub fn is_deposit_destroyed(&self, col: i64, row: i64) -> bool {
let Ok(c) = u16::try_from(col) else { return false };
let Ok(r) = u16::try_from(row) else { return false };
match self.inner.tile_flat_index(c, r) {
Some(flat) => self.inner.destroyed_deposits.contains(&flat),
None => false,
}
}
/// p2-76 — true iff the tile at `(col, row)` is currently
/// yields-zeroed-and-unworkable due to active surface contamination. Reads
/// the derived `unworkable_tiles` mirror (rebuilt each worldsim step).
#[func]
pub fn is_tile_unworkable(&self, col: i64, row: i64) -> bool {
let Ok(c) = u16::try_from(col) else { return false };
let Ok(r) = u16::try_from(row) else { return false };
match self.inner.tile_flat_index(c, r) {
Some(flat) => self.inner.unworkable_tiles.contains(&flat),
None => false,
}
}
/// p2-76 **temporary** river-gap build guard: true when a bunker (or other
/// deposit-destroying improvement) must be FORBIDDEN at `(col, row)` because
/// the tile carries a river course (damming it needs the `p2-78` hydrology
/// re-solve). The build-validity path consults this before allowing a bunker.
/// Removed by `p2-78`.
#[func]
pub fn bunker_river_gap_blocked(&self, col: i64, row: i64) -> bool {
let Ok(c) = u16::try_from(col) else { return false };
let Ok(r) = u16::try_from(row) else { return false };
self.inner.bunker_river_gap_blocked(c, r)
}
/// Queue a bombard request for the turn processor to drain.
///
/// `indirect_fire` must be `true` for units with the `arcing` keyword