From 6769fc3de9c24eb86242a81e1116ff18accb2693 Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Mon, 15 Jun 2026 12:38:46 -0700 Subject: [PATCH] Docs: END-2 session log + DR-036; Backlog/Path_to_Fun/Milestones; CLAUDE.md END-2 line Path A spine COMPLETE (14/14): Backlog SL-3 blocker cleared + marked done; Path_to_Fun END-2 done + banner; Milestones END-2 row. CLAUDE.md gains the END-2 gotcha line (replicate the outcome, don't client-derive; SiegeTimeout off during the final), net-zero via EB-1/EB-2/END-1/M7/inventory/build-grid condensations (40,445 then 40,510 w/ history note, under the 40,960 limit). DR-036 + session log capture the design, the operator forks (halt+banner, Target=4, SaveData v5), and the pre-coding + post-impl adversarial reviews. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 19 ++--- Docs/Vault/06_Roadmap/Backlog.md | 5 +- Docs/Vault/06_Roadmap/Milestones.md | 3 +- Docs/Vault/06_Roadmap/Path_to_Fun.md | 8 ++- ...026-06-13_SL3_END2_Final_Siege_Win_Lose.md | 71 +++++++++++++++++++ .../DR-036_END2_Final_Siege_Win_Lose.md | 63 ++++++++++++++++ 6 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 Docs/Vault/07_Sessions/2026/2026-06-13_SL3_END2_Final_Siege_Win_Lose.md create mode 100644 Docs/Vault/07_Sessions/_Decisions/DR-036_END2_Final_Siege_Win_Lose.md diff --git a/CLAUDE.md b/CLAUDE.md index 1a7c8e421..69722d4e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ Multiplayer game on **Unity DOTS (Entities) + Netcode for Entities** — server- - **Size check** — bash: `wc -c CLAUDE.md` · PowerShell: `(Get-Item CLAUDE.md).Length`. Must be `< 40960`. - **Archive, don't delete.** When trimming, append the verbose / least-hot detail to the obsidian reference note `Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md` under a **new dated heading** (never overwrite an older snapshot), and leave a one-line pointer + the relevant `[[DR-###]]` link here. Design rationale already lives in the per-milestone DRs (`Docs/Vault/07_Sessions/_Decisions/DR-###`). - **Net-zero rule:** every addition is paid for by a condensation elsewhere. Keep only the hottest, highest-recurrence operational rules inline (flag them **★**); depth lives in the archive + DRs. -- Condensation history: 2026-06-04 (first pass, M1–M6 long-form → archive) · 2026-06-07 (second pass, M7+/HUD/animation tightened) · 2026-06-08 (inventory pointer net-zero; persistence + world-collision-rim detail → archive). +- Condensation history: 2026-06-04 (first pass, M1–M6 long-form → archive) · 2026-06-07 (M7+/HUD/animation tightened) · 2026-06-08 (inventory pointer; persistence + world-collision detail → archive) · 2026-06-13 (END-2 line added net-zero; EB-1/EB-2/END-1/M7/inventory/build-grid trimmed). ## Stack — Unity 6.4.7 (`6000.4.7f1`, stable) as of 2026-05-30 @@ -86,15 +86,16 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui ### Build / structures / grid - **Build-grid math must be deterministic + integer-stable:** corner-origin, center-returning, **half-open** cell bounds, `math.floor`. Lock `CellSize`/`PlotSize` as a coordinate space once (`BaseGridMath`) — changing them invalidates placed structures. -- **`PlacedStructure{[GhostField] byte Type; int2 Cell (server-only); uint NextTick; uint LastProcessedTick}`** on an ownerless interpolated ghost. **Bake the tick fields** (offline-catch-up linchpin). Only `Type` replicates (client derives `Cell` via `BaseGridMath.WorldToCell`). **Occupancy is DERIVED** by scanning live ghosts into a Temp `NativeHashSet`, never a baked buffer. See [[DR-014_M6_Build_Structures_Automation_Foundation]]. +- **`PlacedStructure{[GhostField] byte Type; int2 Cell (server-only); uint NextTick; uint LastProcessedTick}`** on an ownerless interpolated ghost. **Bake the tick fields** (catch-up linchpin); only `Type` replicates (client derives `Cell`). **Occupancy is DERIVED** by scanning live ghosts into a Temp `NativeHashSet`, never baked. See [[DR-014_M6_Build_Structures_Automation_Foundation]]. - **Co-op placement atomicity:** commit `StorageMath.Withdraw` + cell-reservation **in-place in the RPC foreach** (only `Instantiate` via ECB) so two same-tick requests for one cell can't both pass. -- **EB-1 machines can die ★ (DR-032):** structures bake `Health`(`[GhostField]`)+`DamageEvent` buffer+a `Destructible` tag; `HealthApplyDamageSystem` destroys a `Destructible` at 0 (NOT bare `PlacedStructure`; occupancy auto-frees). `EnemyAISystem` fortress-targets weighted-nearest players+structures (`EnemyAIMath.PickWeightedNearest`; snapshot ABOVE the early-return; `StructureAggroWeight`<1 prefers structures, SQUARED). Loss VFX = `StructureFeedbackSystem`. See [[DR-032_EB1_Machines_Can_Die]]. -- **EB-2 felt spend ★ (DR-033):** turret ammo = shared `Charge`(`ResourceId` **4**) on the existing `[GhostField] StorageEntry` ledger (no new wire). `TurretFireSystem` spends from the ONE `GetSingletonEntity` (NEVER `GetSingleton`): afford→fire+cooldown, else **SOFT-FAIL** (no cooldown-burn; partial→refund). `Fabricator.InputFromLedger`(byte, server-only) reads the ledger **LIVE in-loop** (no hoist → machines split a finite pool); Ore→Charge ×3/30t. HUD violet Charge chip + global quiet cue. See [[DR-033_EB2_Felt_Spend_Charge_Economy]]. -- **END-1 losable Core ★ (DR-034):** `CoreIntegrity{[GhostField] int Current,Max; uint OverrunTick}` on the GLOBAL CycleDirector ghost (no new ghost). `CoreDamageSystem` (server, after `EnemyAISystem`): Husk within ~3u of `PlotCenter` drains+despawns; `CoreRestoreSystem` regens ONLY in Calm. SOFT-loss edge IN `CyclePhaseSystem` (sole Phase writer): `Current<=0` in Siege → Calm (**NO** reward)+`DrainFraction` ledger+despawn Husks+stamp `OverrunTick` (transient flash, not latching). Core = an `EnemyAISystem` **FALLBACK** target. SaveData **v4** `CoreCurrent` (0→full); 3 live knobs. See [[DR-034_END1_Losable_Core]]. -- **Resource-gated ability tiers/buffs reuse `StatModifier`** (`StatRecomputeSystem`→`EffectiveAbilityStats` both worlds). `GoalProgress{[GhostField] int Charge,Target}` (goal meter — ≠ EB-2 `ResourceId.Charge` ammo) rides the CycleDirector ghost. -- **M7 Automation (server-only) ★:** `Harvester`/`Conveyor` TRIMMED from the palette (code intact), `Fabricator` LIVE (EB-2); on `PlacedStructure`; plain server group; catch-up `ProductionMath.CyclesDue` (**lower-bound 0**); `RuntimePlacedTag` = player-built. See [[DR-020_M7_Automation_Production_Chains]]. -- **Per-player inventory + equipment + items ★:** harvest routes by node region (DR-031) — **BASE→shared `ResourceLedger`**, **Expedition/un-tagged→PERSONAL `InventorySlot`** (`[GhostField]` `OwnerSendType.All`, spill→ledger); `G`=`InventoryDepositRequest`. Items=`ItemDatabase` blob, equip=`EquipSystem`; session-only — **full detail in gotchas archive (2026-06-12)**. See [[DR-026_Inventory_Equipment_Progression_Foundation]] · [[DR-027_Equipment_Slots_Phase1]]. -- **Disk persistence (`SaveData`, single-slot atomic JSON, versioned/additive) ★:** **born-correct load** — `CycleDirectorSpawnSystem` stages `PendingSave` AT SPAWN; `BaseRestoreSystem` replays structures charge-free + REMAINING-tick cooldowns + **EB-1 v3** per-structure HP, SAME-ECB. `SaveService.Load` = additive floor `[MinLoadableVersion=2, Current]` (old saves load; missing field 0-defaults). See [[DR-019_Frontend_Menu_Settings_Saves_Build]] · [[DR-032_EB1_Machines_Can_Die]]. +- **EB-1 machines can die ★ (DR-032):** structures bake `Health`(`[GhostField]`)+`DamageEvent`+a `Destructible` tag; `HealthApplyDamageSystem` destroys a `Destructible` at 0 (NOT bare `PlacedStructure`; occupancy auto-frees). `EnemyAISystem` fortress-targets weighted-nearest players+structures (`EnemyAIMath.PickWeightedNearest`; snapshot ABOVE the early-return; `StructureAggroWeight`<1, SQUARED). See [[DR-032_EB1_Machines_Can_Die]]. +- **EB-2 felt spend ★ (DR-033):** turret ammo = shared `Charge`(`ResourceId` **4**) on the `[GhostField] StorageEntry` ledger. `TurretFireSystem` spends from the ONE `GetSingletonEntity` (NEVER `GetSingleton`): afford→fire+cooldown, else **SOFT-FAIL** (no cooldown-burn). `Fabricator.InputFromLedger` reads the ledger **LIVE in-loop** (no hoist → machines split a finite pool). See [[DR-033_EB2_Felt_Spend_Charge_Economy]]. +- **END-1 losable Core ★ (DR-034):** `CoreIntegrity{[GhostField] int Current,Max; uint OverrunTick}` on the GLOBAL CycleDirector ghost. `CoreDamageSystem`/`CoreRestoreSystem` (server): a Husk near `PlotCenter` drains+despawns; regen ONLY in Calm. SOFT-loss edge IN `CyclePhaseSystem` (sole Phase writer): `Current<=0` in Siege → Calm (**NO** reward; drain+despawn; transient `OverrunTick`, NOT latching). Core = `EnemyAISystem` **FALLBACK** target. SaveData **v4**. See [[DR-034_END1_Losable_Core]]. +- **END-2 win/lose ★ (DR-036):** terminal run on the CycleDirector — server-only `RunPhase` (writer `GoalReachedSystem`, after CyclePhase) + **REPLICATED `RunOutcome{[GhostField] byte}`** (writer `CyclePhaseSystem`; replicate for the banner, do NOT client-derive). `GoalReached` arms a final siege ×`FinalSiegeMultiplier` at `Charge>=Target` (once)+FinalDefense; latch Victory/Loss+halt; **SiegeTimeout OFF in the final**; SaveData **v5**. See [[DR-036_END2_Final_Siege_Win_Lose]]. +- **`GoalProgress{[GhostField] int Charge,Target}`** (the goal meter — ≠ EB-2 `ResourceId.Charge` ammo) rides the CycleDirector ghost. Resource-gated ability tiers/buffs reuse `StatModifier` (`StatRecomputeSystem`→`EffectiveAbilityStats`). +- **M7 Automation (server-only) ★:** `Harvester`/`Conveyor` TRIMMED from the palette (code intact), `Fabricator` LIVE (EB-2); plain server group; catch-up `ProductionMath.CyclesDue` (**lower-bound 0**); `RuntimePlacedTag`=player-built. See [[DR-020_M7_Automation_Production_Chains]]. +- **Harvest routes by node region (DR-031) ★; inventory/equipment PAUSED:** BASE→shared `ResourceLedger`, Expedition/un-tagged→PERSONAL `InventorySlot` (`[GhostField] OwnerSendType.All`, spill→ledger); `G`=deposit. Items/equip (`ItemDatabase` blob, `EquipSystem`) — **full detail in the gotchas archive (2026-06-12)**. See [[DR-026_Inventory_Equipment_Progression_Foundation]] · [[DR-031_Base_Mining_Loop_Cohesion]]. +- **Disk persistence (`SaveData`, single-slot atomic JSON, versioned/additive) ★:** **born-correct load** — `CycleDirectorSpawnSystem` stages `PendingSave` AT SPAWN; `BaseRestoreSystem` replays structures charge-free + REMAINING-tick cooldowns + per-structure HP. `SaveService.Load` = additive floor `[MinLoadableVersion=2, Current]` (old saves load; missing field 0-defaults). See [[DR-019_Frontend_Menu_Settings_Saves_Build]]. ### Presentation / juice / VFX - **All juice/HUD = client-only observe-only `SystemBase` in `PresentationSystemGroup`** (once/frame, no rollback double-fire), never mutates the sim. Read ECS via `SystemAPI.Query` + `EntityManager.CompleteDependencyBeforeRO()` — NOT MonoBehaviour `LateUpdate` (job-safety throw). `Entity` = a stable client dict key per ghost lifetime — **prune the cache each frame** (a pruned ghost = a kill/loss → death VFX); **never `DestroyEntity` a ghost client-side** (`GhostDespawnSystem` owns despawn). Hit-stop = camera punch, **never `Time.timeScale`**. diff --git a/Docs/Vault/06_Roadmap/Backlog.md b/Docs/Vault/06_Roadmap/Backlog.md index 7505e4be3..233db5d58 100644 --- a/Docs/Vault/06_Roadmap/Backlog.md +++ b/Docs/Vault/06_Roadmap/Backlog.md @@ -19,7 +19,7 @@ Last decluttered 2026-06-08 (removed all shipped `[x]` items; their context is p **Already shipped & code-verified (do NOT rebuild — 13/14 slice systems):** movement, dash (MC-1, fun-gate PASSED 2026-06-10), melee combo (MC-4), ranged poke; base-local Ore mining (DR-031); Turret/Wall/Fabricator palette + UITK build HUD (DR-021); Ore→Fabricator→Charge→turret economy (EB-2 / DR-033); destructible structures (EB-1 / DR-032); soft-loss Engine Core integrity + overrun flash (END-1 / DR-034); SaveData v4 + frontend menu / Continue / build menu (DR-019). The audit: 9-agent grounded gap-analysis, 2026-06-13. -**The ONE blocker is END-2 (final siege + terminal win) — SL-3 below.** Today `GoalProgress.Charge` increments past `Target` forever with no clamp/consequence; there is a clear *loss* (Core overrun) but **no terminal *win***. END-2 must land by **June 26**. **Charge cadence LOCKED 2026-06-13: siege-survived-only** (goal +1 per survived siege; Path_to_Fun's "Both"/Aether-deposit cadence deferred post-slice). See [[DR-035_End_Of_Month_Slice_Adoption#Locked fork — END-2 charge cadence (2026-06-13)]]. +**END-2 (SL-3) is CODE-COMPLETE + validated (2026-06-13) — the Path-A blocker is CLEARED.** The goal meter now arms a visibly-larger final siege at `Charge>=Target` (=4) and latches a terminal **Victory** (survive it) / **Loss** (Core breached during it), with a HUD banner + run-halt. 342/342 EditMode green; Play-validated server==client + no ordering cycle; pre-coding + post-impl adversarial reviews. The **fun-gate** (does the climax land?) is the one open item → folds into SL-5. See [[DR-036_END2_Final_Siege_Win_Lose]] · [[2026-06-13_SL3_END2_Final_Siege_Win_Lose]]. **Charge cadence LOCKED 2026-06-13: siege-survived-only** (goal +1 per survived siege; "Both"/Aether-deposit deferred post-slice). See [[DR-035_End_Of_Month_Slice_Adoption#Locked fork — END-2 charge cadence (2026-06-13)]]. **Critical path:** SL-1 → SL-2 → **SL-3 (END-2)** → SL-5 → SL-6 → SL-7; SL-4 (visual) runs parallel, done by Jun 23. Every milestone ends with a **falsifiable play/fun-gate**, not a test count. Full vision + visual direction + cohesion checklist + Done Definition: [[End_Of_Month_Game_Jam_Slice]]. @@ -35,7 +35,8 @@ Last decluttered 2026-06-08 (removed all shipped `[x]` items; their context is p - **Fun-gate:** a fresh non-designer plays 5 min uncoached and reads mining as purposeful, building as a "prep" beat, waves as recognizable pressure, a siege death as "try again"; competent player finishes a run in 5–8 min. - **Deps:** SL-1. -### SL-3 — END-2: Final Siege & Win/Lose (PATH-A BLOCKER) `Jun 16-23 build, Jun 24 validate` · risk MED · **review-gated** ★ +### SL-3 — END-2: Final Siege & Win/Lose (PATH-A BLOCKER) ✅ CODE-COMPLETE + validated 2026-06-13 (fun-gate → SL-5) · **review-gated** ★ +> **Done 2026-06-13:** `RunPhase` (server-only) + `RunOutcome` (`[GhostField]`) on the CycleDirector ghost; new `GoalReachedSystem` arms the final siege (×live `FinalSiegeMultiplier`); `CyclePhaseSystem` latches Victory (final cleared) / Loss (Core breached) + halts the director; SaveData **v5** (a won/lost run loads finished, no re-arm); HUD terminal banner; `Target` 10→4. 342/342 EditMode green; Play-validated server==client + no ordering cycle. Spec/decisions [[DR-036_END2_Final_Siege_Win_Lose]] · session [[2026-06-13_SL3_END2_Final_Siege_Win_Lose]]. Open: operator fun-gate (climax feel) + SL-5's retry/quit-on-banner overlay + rich pre-final telegraph. - **Goal:** the goal meter means something — at `GoalProgress.Charge >= Target` arm a visibly-larger **final siege**; surviving it latches a **Victory**, breaching the Core during it latches a **Loss**; both fire a clear HUD banner. Spec: [[Path_to_Fun#END-2 — The charge means something: the cap arms a final siege, win or lose]]. - **Charge cadence — LOCKED 2026-06-13: siege-survived-only** (goal +1 per survived siege, the existing single-writer; no Aether-deposit writer — "Both" deferred post-slice). [[DR-035_End_Of_Month_Slice_Adoption#Locked fork — END-2 charge cadence (2026-06-13)]]. - **Tasks:** add `RunPhase{byte Normal|FinalDefense}` + `RunOutcome{byte InProgress|Victory|Loss}` on the CycleDirector (server-only single-writer, **NOT `[GhostField]`** — avoid the ghost re-hash; derive client-side) · new server-only `GoalReachedSystem` `[UpdateAfter(CyclePhaseSystem)]`: on the `Charge>=Target` edge (guarded `RunPhase==Normal` → exactly-once) **clamp `Charge`**, arm the final siege via the existing `ThreatState.PendingSiegeSize` (×`FinalSiegeMultiplier`), set `FinalDefense` · in `CyclePhaseSystem` (keep it the **sole** `WaveState`/`Phase` writer — no second writer, DR-017 cycle hazard): final-`DefendCleared`→`Victory`, final-overrun→`Loss` · add live `FinalSiegeMultiplier` (~2–3×) to `TuningConfig` · client-only HUD victory/loss banner reusing END-1's `OverrunTick` edge-detect pattern · **run the adversarial design review BEFORE coding** (netcode/determinism/single-writer) per [[validate-netcode-design-before-coding]] · 4 EditMode tests (arms once, Victory edge, Loss edge, Charge clamp). diff --git a/Docs/Vault/06_Roadmap/Milestones.md b/Docs/Vault/06_Roadmap/Milestones.md index b43bed5cb..e9bdc1686 100644 --- a/Docs/Vault/06_Roadmap/Milestones.md +++ b/Docs/Vault/06_Roadmap/Milestones.md @@ -41,5 +41,6 @@ permalink: gamevault/06-roadmap/milestones | **— 2026-06-11 EB-1 — machines can die (structure loss-state)** | Structures bake `Health`(`[GhostField]`)+`DamageEvent`+`Destructible`; `HealthApplyDamageSystem` destroys at 0 (occupancy auto-frees); `EnemyAISystem` fortress-targets weighted-nearest players+structures; loss VFX via `StructureFeedbackSystem`. SaveData v3 per-structure HP. | ✅ Done 2026-06-11 — [[DR-032_EB1_Machines_Can_Die]] · [[2026-06-11_EB1_Machines_Can_Die]] | | **— 2026-06-12 EB-2 felt spend + END-1 losable Core** | **EB-2:** turret ammo = shared `Charge` (`ResourceId` 4) on the existing ledger; `TurretFireSystem` soft-fail spend; ledger-fed `Fabricator` mints Ore→Charge live-in-loop. **END-1:** `CoreIntegrity{[GhostField] Current,Max; uint OverrunTick}` on the CycleDirector; `CoreDamageSystem`/`CoreRestoreSystem`; soft-loss edge in `CyclePhaseSystem` (transient overrun flash, NO latching win yet); Core = `EnemyAISystem` fallback target. SaveData **v4**. | ✅ Done 2026-06-12 — 330/330 EditMode. [[DR-033_EB2_Felt_Spend_Charge_Economy]] · [[2026-06-12_EB2_Felt_Spend]] · [[DR-034_END1_Losable_Core]] · [[2026-06-12_END1_Losable_Core]] | | **— 2026-06-13 End-of-Month Game Jam Slice (ACTIVE TARGET)** | Answer the Path A Decision Gate early as **ship the minimum**: "Awakening Engine Last Stand" — a compact ARPG / base-defense demo, 5–8 min win-or-lose in **one arena**, June 30. Code audit (9-agent): **13/14 slice systems shipped**; **END-2** (final siege + latching win) is the one blocker. Single-arena scope-down + visual cohesion + loop tuning + packaging. Full milestone breakdown (SL-1…SL-7) in [[Backlog#NEXT — Awakening Engine Last Stand (End-of-Month Slice) ★]]. | 🧭 **Direction set 2026-06-13** — [[End_Of_Month_Game_Jam_Slice]] · [[DR-035_End_Of_Month_Slice_Adoption]] | +| **— 2026-06-13 END-2 — final siege + win/lose (slice SL-3)** | The slice's one blocker + the last Path A spine mechanic: at `Charge>=Target` (=4) `GoalReachedSystem` arms a larger final siege (×live `FinalSiegeMultiplier`); `CyclePhaseSystem` latches **Victory** (survive) / **Loss** (Core breached) + halts the director; `RunOutcome` (`[GhostField]`, server-only `RunPhase`) drives a HUD terminal banner; SaveData **v5** persists the outcome (a won/lost run loads finished). End-behavior locked **halt + banner** (retry/quit UX → SL-5). | ✅ Done 2026-06-13 — 342/342 EditMode; Play-validated server==client + no ordering cycle; pre-coding + post-impl adversarial reviews. **Path A spine COMPLETE.** [[DR-036_END2_Final_Siege_Win_Lose]] · [[2026-06-13_SL3_END2_Final_Siege_Win_Lose]] | -Promote items from [[Backlog]] here when committed. **The active target is the [[End_Of_Month_Game_Jam_Slice]]** (Path A spine ~done; END-2 the one blocker — see [[Backlog]]); the long-term plan remains [[Path_to_Fun]], answered at its [[Path_to_Fun#The Decision Gate (MANDATORY STOP after END-2)|Decision Gate]] *after* the slice ships. [[DR-035_End_Of_Month_Slice_Adoption]] \ No newline at end of file +Promote items from [[Backlog]] here when committed. **The active target is the [[End_Of_Month_Game_Jam_Slice]]** (Path A spine **COMPLETE** — END-2 shipped 2026-06-13 as SL-3; the rest of the slice is tuning + polish, not code — see [[Backlog]]); the long-term plan remains [[Path_to_Fun]], answered at its [[Path_to_Fun#The Decision Gate (MANDATORY STOP after END-2)|Decision Gate]] *after* the slice ships. [[DR-035_End_Of_Month_Slice_Adoption]] \ No newline at end of file diff --git a/Docs/Vault/06_Roadmap/Path_to_Fun.md b/Docs/Vault/06_Roadmap/Path_to_Fun.md index eb31e3456..48e3ee8e9 100644 --- a/Docs/Vault/06_Roadmap/Path_to_Fun.md +++ b/Docs/Vault/06_Roadmap/Path_to_Fun.md @@ -13,7 +13,7 @@ permalink: gamevault/06-roadmap/path-to-fun # Path to Fun — the north-star roadmap -> **[ACTIVE JUNE 13–30 → the slice]** Path A's spine is built — a 2026-06-13 code audit found **13/14 systems shipped** (MC-0/1/4 · EB-1/2 · END-1). The operator answered the [Decision Gate](#the-decision-gate-mandatory-stop-after-end2) **early, as *ship the minimum***: the immediate target is the **[[End_Of_Month_Game_Jam_Slice]]** ("Awakening Engine Last Stand"), a compact single-arena base-defense demo due June 30. The one un-built spine mechanic — **END-2** (final siege + latching win, spec'd [below](#end-2--the-charge-means-something-the-cap-arms-a-final-siege-win-or-lose)) — is the slice's critical path. This doc stays the **north-star for the full game**; the slice scopes Path A to a deliverable. Milestone breakdown: [[Backlog#NEXT — Awakening Engine Last Stand (End-of-Month Slice) ★]] · adoption: [[DR-035_End_Of_Month_Slice_Adoption]]. *(Path B is untouched and provisional; the formal ship-vs-continue note is logged after June 30.)* +> **[ACTIVE JUNE 13–30 → the slice]** Path A's spine is **COMPLETE** — all **14/14 systems shipped** (MC-0/1/4 · EB-1/2 · END-1 · **END-2 built 2026-06-13 as slice SL-3**, [[DR-036_END2_Final_Siege_Win_Lose]]). The operator answered the [Decision Gate](#the-decision-gate-mandatory-stop-after-end2) **early, as *ship the minimum***: the immediate target is the **[[End_Of_Month_Game_Jam_Slice]]** ("Awakening Engine Last Stand"), a compact single-arena base-defense demo due June 30. With END-2 in, the slice is **winnable end-to-end**; the remaining slice work is **tuning + polish, not code** (SL-1/2/4/5/6/7) — and the final-siege **fun-gate** is the one open operator validation. This doc stays the **north-star for the full game**; the slice scopes Path A to a deliverable. Milestone breakdown: [[Backlog#NEXT — Awakening Engine Last Stand (End-of-Month Slice) ★]] · adoption: [[DR-035_End_Of_Month_Slice_Adoption]]. *(Path B is untouched and provisional; the formal ship-vs-continue note is logged after June 30.)* > The plan to turn an engineering-complete foundation into a game that's fun to play. Direction locked in [[DR-028_Combat_Primary_Verb_Depth_First]]. This is the **forward** plan; [[Milestones]] stays the historical record, [[Backlog]] the loose pool. Living doc: the [Path A contract table](#path-a--the-proven-path-to-a-point-committed) is the only committed scope; everything in [Path B](#path-b--the-forever-track-provisional-not-scheduled) is provisional and re-derived after Path A's fun-gates pass. @@ -70,7 +70,7 @@ Green EditMode + server==client stay **necessary, not sufficient** — they were | **EB-1** | Machines can die: the structure loss-state | Economy | MED · review-gated | ~1.5–2.5 wk | a base you can lose; END-1's Core hook | | **EB-2** | The felt spend: output → depletable combat resource | Economy | MED | ~1–1.5 wk | factory→defense pipe; co-op shared spend | | **END-1** | The base can be lost: a Core with integrity | Endgame | MED | ~4–6 d | a real lose condition | -| **END-2** | The charge means something: final siege, win/lose | Endgame | MED | ~2–4 d | a win beat; the minimum point | +| **END-2** ✅ | The charge means something: final siege, win/lose | Endgame | MED | ~2–4 d | a win beat; the minimum point — **DONE 2026-06-13 (SL-3)** | *Estimates are solo + Claude **coding-time only**. They are wider than the prior draft because several of these milestones are secretly 2–3 slices each (see the [secretly-multi note](#secretly-multi-milestones-why-the-estimates-widened)). **Fun-tuning is the unestimated, unbounded cost** — every milestone's real schedule risk is the playtest→tune→replaytest loop, not the code (see [Risk register](#risk-register) R1/R11). That is why every feel-critical value is a live server singleton, not a baked const (see [Tuning-knob surface](#tuning-knob-surface)), and why the [calendar conversion](#calendar-time-the-play-budget-assumption) below turns these into months.* @@ -184,7 +184,9 @@ Depth = a **dialogue**. Enemies ask distinct, readable questions (a committed lu - **Claude:** the component, the 3 server systems, the HUD bar, the lose-resolution wiring (rollback or soft-drain per the fork), EditMode coverage (Core drains under siege, regens in Calm, lose-edge fires once). **Operator:** the fun-gate playtest (is defending the Core engaging?), the diegetic Core/Engine placement, the lose-severity decision. - **Dependencies:** EB-1 (the structure loss-state is the natural sibling; both make the siege threatening). **Kill-risk:** if defending the Core isn't fun, nothing downstream matters — a losable base only amplifies an already-good fight; shipping it before the fight earns it actively hurts. -### END-2 — The charge means something: the cap arms a final siege, win or lose `~2–4 d` · risk MEDIUM +### END-2 — The charge means something: the cap arms a final siege, win or lose `~2–4 d` · risk MEDIUM · ✅ DONE (2026-06-13, as slice SL-3) +> **Status (2026-06-13):** ✅ **CODE-COMPLETE + validated** — built as slice **SL-3**, COMPLETING Path A's spine. `RunPhase` (server-only) + `RunOutcome` (`[GhostField]`) on the CycleDirector ghost; new `GoalReachedSystem` arms the final siege at `Charge>=Target` (=4) ×live `FinalSiegeMultiplier`; `CyclePhaseSystem` latches **Victory** (final cleared) / **Loss** (Core breached) + halts the director; SaveData **v5** persists the outcome (a won/lost run loads finished). End-behavior LOCKED **halt + banner** (operator chose the clean terminal end over the spec's "keep playing"; the retry/quit overlay → SL-5, the pause menu is the in-session hatch). 342/342 EditMode green; Play-validated server==client + no ordering cycle; pre-coding + post-impl adversarial reviews. The **fun-gate** is the open operator item. See [[DR-036_END2_Final_Siege_Win_Lose]] · [[2026-06-13_SL3_END2_Final_Siege_Win_Lose]]. + **Goal:** at `GoalProgress.Charge>=Target` the Engine begins opening the Wellspring — a final, larger escalating siege — and surviving it fires the WIN beat. The meter is no longer a number that stops at 10. **This is the minimum "the game has a point."** - **Scope:** a server-only `GoalReachedSystem` that, on the `Charge>=Target` edge (currently UNHANDLED — `CyclePhaseSystem` increments past it forever with no clamp), arms a FINAL siege via the existing `ThreatState.PendingSiegeSize` entry point (bigger size + a distinct telegraph) and sets a replicated `RunPhase{byte=FinalDefense}`. The WIN-edge — surviving the final siege during `RunPhase.FinalDefense` sets `RunOutcome{byte=Victory}`, fires the ending event (subtitle/banner first), and for the minimum simply flips into "keep playing, the base is yours" (the endless/NG+ curve is END-5, Path B). A single client-only WIN/LOSS banner in `HudSystem` (observe `RunOutcome`; reused by END-1's overrun banner). **Zero net-new writing** for the minimum (placeholder banner text), **zero new ghosts.** - **Build notes:** arm the final siege through the EXISTING single entry point — do NOT add a parallel siege path (`CyclePhaseSystem` stays the sole `WaveState` writer, DR-017's atomic Calm→Siege seed). `RunPhase`/`RunOutcome` are BYTES, single-writer, server-decided. The banner is client-only observe-only. Guard the `GoalReached` edge so it arms EXACTLY ONCE (a `RunPhase!=Normal` guard) — and **clamp the currently-uncapped `Charge`.** If cadence moves to Aether-deposited (the fork below), keep it a server-only single writer (avoid co-op double-count). diff --git a/Docs/Vault/07_Sessions/2026/2026-06-13_SL3_END2_Final_Siege_Win_Lose.md b/Docs/Vault/07_Sessions/2026/2026-06-13_SL3_END2_Final_Siege_Win_Lose.md new file mode 100644 index 000000000..c0ac0740d --- /dev/null +++ b/Docs/Vault/07_Sessions/2026/2026-06-13_SL3_END2_Final_Siege_Win_Lose.md @@ -0,0 +1,71 @@ +--- +date: 2026-06-13 +type: session +tags: +- session +- endgame +- netcode +- persistence +- slice +- end-2 +permalink: gamevault/07-sessions/2026/2026-06-13-sl3-end2-final-siege-win-lose +--- + +# SL-3 / END-2 — The charge means something: final siege, win or lose + +> The slice's one **critical-path blocker** ([[End_Of_Month_Game_Jam_Slice]] · [[Backlog#SL-3 — END-2: Final Siege & Win/Lose (PATH-A BLOCKER)]]); the last un-built Path A spine mechanic. Full [[dots-dev]] Feature track, **review-gated** (pre-coding + post-impl adversarial reviews). Locked → [[DR-036_END2_Final_Siege_Win_Lose]]. Continues [[2026-06-12_END1_Losable_Core]] (reuses its ghost + banner pattern). Charge cadence locked in [[DR-035_End_Of_Month_Slice_Adoption]]. Spec: [[Path_to_Fun#END-2 — The charge means something: the cap arms a final siege, win or lose]]. + +## The hole END-2 closes +END-1 gave a clear *loss* (soft Core overrun → transient flash); the goal meter still incremented past `Target` forever with no clamp and no consequence — **no terminal win, no climax**. END-2 makes the meter mean something: at the cap it arms a visibly-larger **final siege**; surviving it **latches Victory**, a Core breach during it **latches Loss**; both show a HUD banner and the run **halts**. This is the minimum "the game has a point" — it completes Path A. + +## Operator forks (asked up front, present-the-forks ritual) +The docs were explicit on most of END-2 (locked cadence, single-writer, clamp, arm-once, banner). Two genuine forks the source docs left open / conflicted on, plus one the review surfaced: +- **End behavior → HALT + banner** (chosen over "keep-playing sandbox" and "retry/quit overlay now"). `Path_to_Fun`'s minimum said "keep playing"; the slice's Done Definition wants "a run with an end." Operator picked the clean terminal end; the existing **pause menu** (Esc → Quit-to-Menu/Desktop, autosave) is the escape hatch, the dedicated retry/quit overlay is **SL-5**. +- **Run length → `Target = 4`** survived sieges → a 5th, larger final siege (was baked 10; too long for a 5–8 min run). +- **Persist the outcome now (SaveData v5)** — the review showed that without it, a *won* run on Continue replays the final siege (the 4th-siege autosave already records `Charge==Target`), erasing the win. Operator approved the +1-byte bump rather than deferring to SL-5. + +## Reviews earned their keep (the design changed twice) +The written SL-3 note said *"server-only bytes, derive client-side, no save bump."* Both halves were overturned by review: +- **Pre-coding** (3 lenses — netcode/determinism/reuse, all GO-with-changes): the outcome must **replicate** (`[GhostField]`) — the spec literally says "observe `RunOutcome`", and client-side derivation has late-join/late-focus blind spots; split into **two single-writer components** (one combined struct = two systems RMW-ing one component across an ordering boundary); fix the arming handoff + clamp + **SiegeTimeout-during-final** (a timeout cull would fake a Victory, F5). +- **Post-impl** (4 lenses over the diff — netcode/determinism GO, persistence/regression GO-with-changes): caught **M-1** — my `RunOutcome` born-correct insert had landed *inside* the `if (HasComponent(prefab))` guard (my insertion line was the guard's closing brace). Latent (CoreIntegrity is always baked) but it would skip the outcome restore if the Core were ever dropped off the prefab → a finished run re-arms on Continue. Fixed, plus 5 test gaps + 3 nits (all addressed). + +## What shipped — reuse the global ghost, split by writer + +### Two new run-state components (CycleDirector ghost) +- `RunPhase { byte Value }` — **server-only** (added at spawn like `CycleRuntime`/`ThreatState`, never on the ghost serializer). `RunPhaseId{Normal,FinalDefense}`. SINGLE writer: `GoalReachedSystem`. +- `RunOutcome { [GhostField] byte Value }` — **REPLICATED** (baked on the prefab → part of the ghost → one re-bake, folded with the `Target=4` re-bake). `RunOutcomeId{InProgress,Victory,Loss}`. SINGLE writer: `CyclePhaseSystem`. The HUD shows the banner by observing it directly. + +### `GoalReachedSystem` (new, server, `[UpdateAfter(CyclePhaseSystem)]`) +On `Charge>=Target` ∧ `RunPhase==Normal` ∧ `RunOutcome==InProgress` (exactly once): arms `ThreatState.PendingSiegeSize = max(1,(int)((SizeBase+ScheduleSizePerWave*wave) * FinalSiegeMultiplier))` + `ArmTick=TickUtil.NonZero(now+delay)`, flips `RunPhase=FinalDefense`. Never writes `Phase`/`WaveState`/`Charge`. + +### `CyclePhaseSystem` Siege branch split on `RunPhase==FinalDefense` (still sole Phase/WaveState/RunOutcome writer) +- final `DefendCleared` → `RunOutcome=Victory`, Calm, **no** goal increment. +- final `Core<=0` → `RunOutcome=Loss` (terminal: **no** ledger drain, **no** `OverrunTick` stamp → the dedicated Loss banner, not the soft flash; husks despawned). +- Normal-phase paths **byte-unchanged** (END-1 soft loss + survived increment, now `Charge=min(Charge+1,Target)` clamped at the source → `GoalProgress` stays single-writer). + +### Halt + the rest +- Halt authority `RunOutcome != InProgress`: `ThreatDirectorSystem` arming sources gated `RunPhase==Normal && RunOutcome==InProgress`; **SiegeTimeout disabled during FinalDefense** (F5); `CoreRestoreSystem` + `CoreDamageSystem` skip once decided (Core freezes at its terminal value). +- HUD: a dedicated latched `_runBanner` (NOT the per-player `_downed` overlay) — "THE ENGINE HOLDS / VICTORY" or "OVERRUN / THE FINAL STAND FELL" + an "Esc — menu" hint. +- `FinalSiegeMultiplier` live `TuningConfig` knob (default 2.5, floored ≥1) across all 6 touch-points + the `DebugTuningReport` wire. +- **SaveData v5** adds `RunOutcome` (additive; `MinLoadableVersion` stays 2; old saves 0-default to InProgress). Persisted at both write sites + staged through `PendingSave` + born-correct at spawn; a won/lost run loads finished + halted (the guard keeps `GoalReachedSystem` inert). Continue also clamps `Target` to the baked run-length so a pre-v5 `Target=10` save still reaches the final siege. + +## Validation +- **EditMode: 342/342** (330 prior + 12: 7 END-2 [arms-once+enter, Charge clamp, Victory edge, Loss edge no-soft-effects, **normal-overrun-stays-soft regression**, restored-Victory-no-rearm, SiegeTimeout-not-culling-final] + the review's 5: full-pipeline arm-not-stomped, FinalSiegeMultiplier override + sub-1 floor, SaveData v5 RunOutcome round-trip + pre-v5-defaults-InProgress). Existing CyclePhase/Threat/Tuning/Core/Save suites stay green (no regression). +- **Play (live netcode, focused editor):** world creation clean — **no ordering cycle** from `GoalReachedSystem [UpdateAfter(CyclePhaseSystem)]`; the ghost **re-bake** is clean (`RunOutcome [GhostField]` replicates server==client — ClientWorld read `RunOutcome=InProgress`; `RunPhase` server-only — ClientWorld count 0); `Target=4` born-correct; `Goal` already climbing (2/4 via survived scheduled sieges → whole pipeline ticking). Zero console errors. +- **Operator fun-gate is OPEN** (the slice's SL-5 work): grind the meter to full, read the final-siege escalation + telegraph, win/lose; tune `FinalSiegeMultiplier`. The retry/quit-on-banner overlay + the rich pre-final telegraph are SL-5. + +## Deliberate cuts / notes +- **Retry/quit-on-banner overlay** — NOT built (SL-5). The pause menu (Quit-to-Menu/Desktop, autosave) is the in-session escape hatch; the banner shows "Esc — menu". +- **Rich final-siege telegraph** (≥3–5 s hum/sky shift/"FINAL SIEGE INCOMING") — SL-5. SL-3 reuses the existing arm-delay telegraph (PhaseEndTick countdown). +- **Client-derive-the-banner / no-rehash** — REJECTED by review (fragile late-join). Replicating one byte + one deliberate re-bake is the correct trade. + +## Files +- New (Simulation): `World/RunStateComponents.cs` (RunPhase + RunOutcome + byte-const classes). New (Server): `World/GoalReachedSystem.cs`. New (Tests): `EndgameWinLoseTests.cs`. +- Modified (Server): `World/CyclePhaseSystem.cs` (FinalDefense Victory/Loss branches + Charge clamp), `World/ThreatDirectorSystem.cs` (arming gate + SiegeTimeout-off-during-final), `World/CoreRestoreSystem.cs` + `World/CoreDamageSystem.cs` (terminal-halt guards), `World/CycleDirectorSpawnSystem.cs` (RunPhase at spawn + born-correct RunOutcome + Target clamp), `Persistence/SaveWriteSystem.cs` (persist RunOutcome). +- Modified (Simulation): `Debug/TuningConfig.cs` (+FinalSiegeMultiplier knob + wire), `Persistence/SaveData.cs` (v5 + RunOutcome), `Persistence/SaveComponents.cs` (PendingSave.RunOutcome). +- Modified (Authoring/Client): `Authoring/World/CycleDirectorAuthoring.cs` (Target 10→4 + bake RunOutcome), `Client/UI/WorldLauncher.cs` (stage + quit-save RunOutcome), `Client/Presentation/HudSystem.cs` (terminal banner). +- Tests: `SavePersistenceTests.cs` (+2 RunOutcome, v5 comment fixes), `TuningConfigTests.cs` (FinalSiegeMultiplier default pin). +- Docs: `DR-036`, this log, `Backlog` (SL-3 done), `Path_to_Fun` (END-2 built), `Milestones`. + +## Next-session intent +END-2 completes Path A's spine — the slice is now winnable. The remaining slice work is **tuning + polish, not code**: SL-1 (arena/camera lock), SL-2 (loop tuning, incl. the `Target=4` run-length + `FinalSiegeMultiplier`), SL-4 (visual cohesion), SL-5 (final-siege climax tuning + the retry/quit-on-banner overlay + rich telegraph + autosave-the-outcome continue/retry flow), SL-6 (polish/cut), SL-7 (package + the formal post-slice Decision-Gate note). The one open END-2 item is the **operator fun-gate**: does the climax read as distinct, prompt deliberate prep, and land the win/loss banner? diff --git a/Docs/Vault/07_Sessions/_Decisions/DR-036_END2_Final_Siege_Win_Lose.md b/Docs/Vault/07_Sessions/_Decisions/DR-036_END2_Final_Siege_Win_Lose.md new file mode 100644 index 000000000..7992d46c8 --- /dev/null +++ b/Docs/Vault/07_Sessions/_Decisions/DR-036_END2_Final_Siege_Win_Lose.md @@ -0,0 +1,63 @@ +--- +id: DR-036 +title: END-2 — the final siege + a latching win/lose (SL-3) +status: accepted +date: 2026-06-13 +tags: +- decision +- design +- endgame +- netcode +- end-2 +- slice +permalink: gamevault/07-sessions/decisions/dr-036-end2-final-siege-win-lose +--- + +# DR-036 — END-2: The Charge Means Something (final siege, win or lose) + +> The last un-built Path A spine mechanic and the **critical-path blocker** for the [[End_Of_Month_Game_Jam_Slice]] (slice milestone **SL-3**). Answers the slice's "no terminal win" gap. Spec: [[Path_to_Fun#END-2 — The charge means something: the cap arms a final siege, win or lose]]. Continues [[DR-034_END1_Losable_Core]] (reuses its ghost + banner pattern). Charge cadence locked in [[DR-035_End_Of_Month_Slice_Adoption#Locked fork — END-2 charge cadence (2026-06-13)]]. Session: [[2026-06-13_SL3_END2_Final_Siege_Win_Lose]]. + +## Context + +Path A's spine was built except for the **win**: `GoalProgress.Charge` incremented past `Target` forever with no clamp and no consequence. END-1 gave a clear *loss* (soft Core overrun → transient flash) but there was no terminal *win* and no climax. END-2 makes the goal meter mean something — at the cap it arms a **bigger final siege**; surviving it **latches Victory**, a Core breach during it **latches Loss**; both show a HUD banner and the run **halts**. + +## Operator decisions (present-the-forks ritual, 2026-06-13) + +- **Charge cadence = siege-survived-only** (re-confirmed from DR-035): goal +1 per survived siege, the existing single writer; no Aether-deposit writer. +- **End behavior = HALT + banner** (chosen over "keep-playing sandbox" and "retry/quit overlay now"). On Victory/Loss: latch, stop the siege director, the banner is the end-state. The existing **pause menu** (Esc → Quit-to-Menu/Desktop, with autosave) is the escape hatch; the dedicated retry/quit-on-banner overlay is deferred to **SL-5**. This is a deliberate divergence from `Path_to_Fun`'s literal "keep playing, the base is yours" minimum. +- **`GoalProgress.Target` = 4** (was baked 10) → survive 4 sieges, then a larger 5th *final* siege. A live-tunable run-length; baked default re-baked. +- **Persist the outcome now (SaveData v5)** rather than deferring save-durability to SL-5 — otherwise a *won* run, on Continue, replays the final siege (the 4th-siege autosave already records `Charge==Target`), erasing the win and undercutting the terminal-end the operator chose. + +## Decision + +**1. Two new components on the global CycleDirector ghost, split by writer** (the pre-impl review's BLOCKER — a single combined struct would have two systems read-modify-writing one component across an ordering boundary): +- `RunPhase { byte Value }` — **server-only** (added at spawn like `CycleRuntime`/`ThreatState`, never on the ghost serializer). Consts `RunPhaseId{Normal=0, FinalDefense=1}`. SINGLE writer: `GoalReachedSystem`. +- `RunOutcome { [GhostField] byte Value }` — **REPLICATED** (baked on the prefab → part of the ghost → one re-bake). Consts `RunOutcomeId{InProgress=0, Victory=1, Loss=2}`. SINGLE writer: `CyclePhaseSystem`. The client HUD shows the banner by **observing** it directly — the spec's "observe `RunOutcome`" — rather than a fragile client-side reconstruction (the rejected "derive from Charge+Phase+Core + a `_sawFinalSiege` latch", which had late-join/late-focus blind spots). + +**2. New `GoalReachedSystem`** (server, `[BurstCompile]`, plain group, `[UpdateAfter(CyclePhaseSystem)]`). On the `Charge>=Target` rising edge — guarded `RunPhase==Normal && RunOutcome==InProgress` ⇒ **exactly once** — it arms a bigger final siege through the existing `ThreatState.PendingSiegeSize` entry point (`= max(1,(int)((SizeBase + ScheduleSizePerWave*wave) * FinalSiegeMultiplier))`, telegraphed via `ArmTick = TickUtil.NonZero(now+delay)`) and flips `RunPhase=FinalDefense`. It **never** writes `CycleState.Phase`/`WaveState` (CyclePhaseSystem stays sole writer) nor `GoalProgress.Charge`. + +**3. `CyclePhaseSystem` Siege branch split on `RunPhase==FinalDefense`** (it remains the sole Phase/WaveState writer and is now the sole `RunOutcome` writer): +- final `DefendCleared` → `RunOutcome=Victory`, Phase=Calm, **no** goal increment; +- final Core `Current<=0` → `RunOutcome=Loss` (terminal: **no** ledger drain, **no** `OverrunTick` stamp — so the client shows the dedicated terminal Loss banner, not the soft "the Core will recover" flash; husks despawned; Phase=Calm); +- **Normal-phase paths are byte-unchanged** (END-1 soft loss + survived-siege increment) — the highest-risk regression, pinned by a dedicated test. +- The survived-siege increment now **clamps at the source**: `goal.Charge = math.min(goal.Charge + 1, goal.Target)`, keeping `GoalProgress` single-writer and the persisted value bounded regardless of system order. + +**4. The halt authority is `RunOutcome != InProgress`.** Once decided: `GoalReachedSystem` + `ThreatDirectorSystem`'s two arming sources stop (gated `RunPhase==Normal && RunOutcome==InProgress`); `CoreRestoreSystem` stops regen (Core freezes at its terminal value). **`SiegeTimeout` is disabled during `FinalDefense`** (review F5 — a timeout-cull would trip `DefendCleared` and fake a Victory). + +**5. Client HUD: a dedicated latched `_runBanner`** (NOT reusing the per-player `_downed` overlay) that observes the replicated `RunOutcome` — "THE ENGINE HOLDS / VICTORY" or "OVERRUN / THE FINAL STAND FELL", with an "Esc — menu" hint (the SL-5-deferred retry/quit hatch). + +**6. `FinalSiegeMultiplier`** is a new live `TuningConfig` knob (default **2.5**, floored ≥1 in the `ClampKnob` default bucket + at the use-site) — added across all six touch-points incl. the `DebugTuningReport` IRpcCommand wire (unconditional type → RpcCollection hash matches across peers; safe). + +**7. Persistence: `SaveData` → v5** adds `RunOutcome` (additive; `MinLoadableVersion` stays 2 → old saves 0-default to InProgress). Persisted at both write sites (`SaveWriteSystem` + `WorldLauncher.TrySaveFromServer`), staged through `PendingSave`, and born-correct at spawn (`CycleDirectorSpawnSystem`). A won/lost run loads **finished + halted** (the `RunOutcome` guard keeps `GoalReachedSystem` inert), so Continue never replays the climax. + +## Why the design changed from the SL-3 note (the review earned its keep) + +The written SL-3 note said "server-only bytes, derive client-side, no save bump." The mandatory pre-coding adversarial review (3 lenses) + a post-impl review (4 lenses) overturned both: (a) the outcome must **replicate** (`[GhostField]`) — the spec literally says "observe `RunOutcome`" and client-derivation has late-join blind spots; (b) the terminal halt + replicated outcome means Continue must persist it (**v5**) or a win is erased on reload. Net: one CycleDirector re-bake (folded with the `Target=4` re-bake) and a one-byte save field — a deliberate, logged trade. + +## Consequences + +- **The game has a point**: a grind-the-meter → climactic final siege → win/lose run, the minimum shippable Path-A loop. The slice's one blocker (SL-3) is cleared. +- **Validation**: 342/342 EditMode green (+12 END-2/save tests — 7 core + the post-impl review's 5: full-pipeline arm-not-stomped, FinalSiegeMultiplier override + sub-1 floor, SaveData v5 RunOutcome round-trip + pre-v5-defaults-InProgress); Play-validated server==client (RunOutcome `[GhostField]` replicates; RunPhase stays server-only; no system-ordering cycle; `Target=4` born-correct; clean console). Two adversarial reviews (pre-coding 3-lens + post-impl 4-lens over the diff) — the post-impl pass caught a real born-correct nesting bug (M-1) since fixed. +- **Reversible / tunable**: `FinalSiegeMultiplier` is live; `Target` is a baked run-length knob; halt-vs-keep-playing is the operator's logged choice. +- **SL-5 hooks**: the retry/quit-on-banner overlay, a richer pre-final telegraph (≥3–5 s hum/sky shift), and `FinalSiegeMultiplier` tuning all build on this. The banner element + the replicated `RunOutcome` are the surfaces SL-5 dresses. +- **Not done here** (deferred, logged): the dedicated retry/quit overlay (SL-5), the rich final-siege telegraph (SL-5), and the fun-gate playtest (operator).