diff --git a/CLAUDE.md b/CLAUDE.md index 50c4adf36..1a7c8e421 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,11 +88,12 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui - **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]]. - **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]`)+a `DamageEvent` buffer+a `Destructible` tag (NO `HitRadius`/NO `EffectiveCharacterStats` → clamp-to-0-then-die); `HealthApplyDamageSystem` destroys a `Destructible` at 0 (NOT bare `PlacedStructure` — M7 machines share it; occupancy auto-frees). `EnemyAISystem` fortress-targets the weighted-nearest of players+structures (snapshot ABOVE the early-return → undefended base razed; `EnemyAIMath.PickWeightedNearest`; `StructureAggroWeight` knob <1 prefers structures, SQUARED). Loss VFX = proximity-gated `StructureFeedbackSystem` (`CombatFeedbackSystem` gated `!isStructure`). 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). Buildable turret = in-region hitscan (no projectile→no tunnelling); `TurretFireSystem` spends `max(1,TurretChargeCostPerShot)`/shot from the ONE `GetSingletonEntity` (NEVER `GetSingleton`): got≥cost→fire+cooldown, else **SOFT-FAIL** (no shot/no cooldown-burn → fires the instant Charge returns; partial→refund). `Fabricator.InputFromLedger`(byte, server-only, NOT `[GhostField]`/enum)→input from the ledger read **LIVE in-loop** (no hoist → 2 machines split a finite pool), output→ledger; re-enabled prefab = Ore→Charge ×3/30t (catalog=**4**; Harvester/Conveyor null). No turret↔Fabricator order edge (≤1-tick); HUD violet Charge chip + global quiet cue. See [[DR-033_EB2_Felt_Spend_Charge_Economy]]. +- **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; region via OPTIONAL `ComponentLookup`); `G`=`InventoryDepositRequest` personal→ledger. 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]]. +- **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]]. ### Presentation / juice / VFX diff --git a/Docs/Vault/07_Sessions/2026/2026-06-12_END1_Losable_Core.md b/Docs/Vault/07_Sessions/2026/2026-06-12_END1_Losable_Core.md new file mode 100644 index 000000000..13821b4a2 --- /dev/null +++ b/Docs/Vault/07_Sessions/2026/2026-06-12_END1_Losable_Core.md @@ -0,0 +1,68 @@ +--- +date: 2026-06-12 +type: session +tags: +- session +- endgame +- netcode +- structures +- persistence +- end-1 +permalink: gamevault/07-sessions/2026/2026-06-12-end1-losable-core +--- + +# END-1 — The base can be lost: a losable Engine Core with integrity + +> Path A milestone (first **Endgame** beat); forks locked in [[DR-029_Path_A_Fork_Locks]] + a 2026-06-12 present-the-forks pass. Full [[dots-dev]] Feature track. Locked → [[DR-034_END1_Losable_Core]]. Continues [[2026-06-12_EB2_Felt_Spend]]. Spec: [[Path_to_Fun#END-1 — The base can be lost: a Core with integrity]]. + +## The hole END-1 closes +EB-1 made structures losable; EB-2 made defense *cost* the harvest. But there was no **aggregate** base-loss condition — a siege could grind machines without ever *taking the base*. END-1 adds the **Engine Core**: a base-integrity meter a breaking-through siege drains. The locked **soft loss** gives the siege teeth without a hard game-over. + +## Forks (present-the-forks ritual, 2026-06-12) +DR-029 had already locked the big END-1 forks (soft loss, Core-bar-only, persist-wounded). Two genuinely-open implementation forks were surfaced + operator-decided: +- **How Husks reach the Core** → **FALLBACK target** (sought only when no live player/structure). Found while reading `EnemyAISystem`: it early-returns to *idle* when no player/structure is alive, so an undefended base would freeze the swarm — the Core must be reachable for the "walks to the Engine" fun-gate. Fallback preserves EB-1 structure stakes (the Core is the *last* thing). +- **Breach resolution** → **despawn remaining Husks** (clean reset-to-recover; the wave disperses). + +## What shipped — ride the global ghost, no new wire + +### `CoreIntegrity` on the GLOBAL CycleDirector ghost +- `{[GhostField] int Current, Max; [GhostField] uint OverrunTick}` — rides the untagged director ghost already carrying `CycleState`/`GoalProgress`/the ledger. **No new ghost, no relevancy.** `Max` baked from `CycleDirectorAuthoring` (100); born-correct `Current` at spawn (full, or persisted wounded). The new `[GhostField]` re-hashes the runtime-spawned ghost — Play-verified server==client (born full 100/100). + +### Two server-only plain-group systems +- `CoreDamageSystem` `[UpdateAfter(EnemyAISystem)]`: a Husk within ~3u of `PlotCenter` drains `CoreDamagePerHusk` and is **consumed** (despawn, at-most-once ECB). Idles at `Current<=0`. +- `CoreRestoreSystem`: +1 every `CoreRegenIntervalTicks` **ONLY in Calm** (deterministic `now % interval`). A chipped Core heals between sieges. + +### SOFT-loss edge — inside `CyclePhaseSystem` (sole Phase writer) +- Checked BEFORE survival: `Current<=0` in Siege → flip **Calm** (**NO** goal reward), `StorageMath.DrainFraction` the shared ledger by `CoreOverrunDrainPct` (0.5), **despawn all remaining Husks** + reset `WaveState`, stamp `OverrunTick`, autosave. Kept inside `CyclePhaseSystem` so it stays the sole Phase/WaveState writer (no second Phase writer → no invisible sort-cycle). New pure `StorageMath.DrainFraction` (floored per row, drops zeroed rows; unit-tested). + +### Core = a FALLBACK target in `EnemyAISystem` +- When no live player/structure remains, Husks seek `PlotCenter` (target `Entity.Null`); both `DamageEvent` strike appends are guarded `!= Entity.Null` (the Core takes damage only via `CoreDamageSystem` proximity). Applies to both the Grunt and Charger passes. + +### Transient overrun pulse, not a latching outcome +- `OverrunTick` is a TRANSIENT pulse (deliberate deviation from the spec's literal `RunOutcome{Overrun}`): a soft loss is **non-terminal**, so a latching terminal outcome is the wrong shape. The HUD edge-detects the tick change → ~3.5s "BASE OVERRUN — resources lost; the Core will recover" flash (overrides the location line, after the EB-2 cue so it wins). The latching `RunOutcome` (Victory) is **END-2's** job. + +### Persistence — SaveData v4 (additive) +- `SaveData` → **v4** adds `CoreCurrent` (`MinLoadableVersion` stays 2). Pre-v4 save lacks the field → JsonUtility 0 → born-correct maps 0 → baked Max (full), reusing EB-1's HP `0→Max` sentinel. Round-tripped through `PendingSave` + both write paths (`SaveWriteSystem` autosave + `WorldLauncher` quit-to-menu). + +### HUD + tuning +- Red Core-integrity bar mirroring the GoalProgress bar (`CoreRed`, danger-lerp as it drops). 3 live `TuningConfig` knobs (`CoreDamagePerHusk`/`CoreRegenIntervalTicks`/`CoreOverrunDrainPct`) + `DebugOverlay` rows; `CoreReachRadius` a structural const. + +## Validation +- **EditMode: 330/330** (318 prior + 12 END-1: 3 `DrainFraction`, 2 SaveData v4 / additive-CoreCurrent, 2 CyclePhase overrun soft-loss [ends siege / drains ledger / despawns husks / no goal charge / resolves-once], 5 CoreSystems [breach drains+consumes / idles-at-0 / regen-in-Calm-only / no-regen-in-Siege / caps-at-Max]). The existing `StructureSave_HP_RoundTrips_And_Writes_V3` was updated for the v4 bump (now asserts `SaveData.CurrentVersion`); TuningConfig golden pin extended with the 3 Core defaults. +- **Play (live netcode, focused editor):** world creation clean (no ordering cycle from the new `[UpdateAfter]` edges); `CoreIntegrity` replicates server==client (born 100/100); both new systems present in `ServerWorld`, absent in `ClientWorld` (server-only filter correct); a live drain test — 12 test Husks at the Core drained 100→0 (all consumed), then `CoreRestoreSystem` regenerated 0→23 in Calm, replicating server(23)→client(22) at the expected ~1-tick interpolation latency. Zero console errors throughout. +- **Operator hands-on fun-gate is OPEN:** in a live Host+client siege, let a Husk breach to the Engine and watch the integrity bar tick down; fail to defend → "BASE OVERRUN", lose resources, regroup in Calm as the Core regenerates. Tune `CoreDamagePerHusk` / regen / drain-pct to taste. + +## Deliberate cuts / notes +- **Latching `RunOutcome` / hard-rollback lose mode** — NOT built (DR-029 locked soft). `RunOutcome` is END-2's (the Victory latch + win banner reuse the `OverrunTick`/banner path). +- **0-integrity-saved → restores full** — a base saved at exactly `Current=0` comes back full (the 0 sentinel = "unset → baked Max", same as EB-1 HP). Negligible: the Core regenerates in Calm anyway, and the autosave-on-breach is immediately followed by Calm regen, so a quit usually saves `Current>0`. + +## Files +- New (Simulation): `World/CoreIntegrity.cs`. New (Server): `World/CoreDamageSystem.cs`, `World/CoreRestoreSystem.cs`. New (Tests): `CoreSystemsTests.cs`. +- Modified (Simulation): `HomeBase/StorageMath.cs` (`DrainFraction`), `Debug/TuningConfig.cs` (+3 Core knobs + wire), `Persistence/SaveData.cs` (v4 + `CoreCurrent`), `Persistence/SaveComponents.cs` (`PendingSave.CoreCurrent`). +- Modified (Server): `World/CycleDirectorSpawnSystem.cs` (born-correct Core), `World/CyclePhaseSystem.cs` (soft-loss edge), `Combat/EnemyAISystem.cs` (Core fallback target + strike guards), `Persistence/SaveWriteSystem.cs` (persist `CoreCurrent`). +- Modified (Authoring/Client): `Authoring/World/CycleDirectorAuthoring.cs` (bake `CoreIntegrityMax`), `Client/UI/WorldLauncher.cs` (stage + quit-save `CoreCurrent`), `Client/Presentation/HudSystem.cs` (Core bar + overrun flash), `Client/Debug/DebugOverlay.cs` (3 knob rows). +- Tests: `SavePersistenceTests.cs` (+2, +v4 fix), `StorageMathTests.cs` (+3), `CyclePhaseSystemTests.cs` (+2), `TuningConfigTests.cs` (golden pin +3). +- Docs: `CLAUDE.md` (END-1 bullet; EB-1/EB-2/inventory condensed net-neutral), `DR-034`, this log. + +## Next-session intent +END-1 gives the base a real lose condition. Next Path A beat is **END-2** — the goal charge arms a final escalating siege and surviving it fires the WIN beat (the latching `RunOutcome{Victory}` + win banner, reusing END-1's pulse/banner path) — which **completes Path A** and triggers the mandatory **Decision Gate**. If defending the Core doesn't feel engaging in the operator's hands, tune the damage/regen/drain knobs before END-2. diff --git a/Docs/Vault/07_Sessions/_Decisions/DR-034_END1_Losable_Core.md b/Docs/Vault/07_Sessions/_Decisions/DR-034_END1_Losable_Core.md new file mode 100644 index 000000000..c15892a39 --- /dev/null +++ b/Docs/Vault/07_Sessions/_Decisions/DR-034_END1_Losable_Core.md @@ -0,0 +1,60 @@ +--- +id: DR-034 +title: END-1 — a losable Engine Core (soft-loss base-integrity meter) +status: accepted +date: 2026-06-12 +tags: +- decision +- design +- endgame +- netcode +- end-1 +permalink: gamevault/07-sessions/decisions/dr-034-end1-losable-core +--- + +# DR-034 — END-1: The Base Can Be Lost (a Core with Integrity) + +> Path A milestone (the first **Endgame** beat); forks locked in [[DR-029_Path_A_Fork_Locks]]. Continues the economy braid [[DR-032_EB1_Machines_Can_Die]] · [[DR-033_EB2_Felt_Spend_Charge_Economy]]. Spec: [[Path_to_Fun#END-1 — The base can be lost: a Core with integrity]]. Session: [[2026-06-12_END1_Losable_Core]]. + +## Context + +EB-1 made structures losable; EB-2 made defense *cost* the harvest. But the base still had no **aggregate** loss condition — a siege could chew machines yet never actually *take the base*. END-1 adds the **Engine Core**: a base-integrity meter a breaking-through siege drains, giving the siege teeth without a hard game-over (the locked **soft-loss** fork). + +## Decision + +**1. `CoreIntegrity` rides the GLOBAL CycleDirector ghost** — `{[GhostField] int Current, Max; [GhostField] uint OverrunTick}`. No new ghost, no relevancy work (the shared-global-state rule — it must never be region-tagged). `Max` is baked from `CycleDirectorAuthoring` (default 100); `Current` is born-correct at spawn (full, or the persisted wounded value). Adding the `[GhostField]` re-hashes the runtime-spawned director ghost — server and client bake the same prefab so the hash matches (Play-verified server==client). + +**2. Two new server-only plain-group systems:** +- `CoreDamageSystem` `[UpdateAfter(EnemyAISystem)]` — a Husk within ~3u (`CoreReachRadius`, structural const) of `BaseGridMath.PlotCenter` BREACHES: drains `CoreDamagePerHusk` and is **consumed** (despawned, at-most-once ECB). Reads each Husk's post-move position. Idles once `Current<=0` (the lose-edge owns resolution). +- `CoreRestoreSystem` — regenerates +1 every `CoreRegenIntervalTicks` **ONLY in Calm** (deterministic server-only `now % interval` gate). A chipped-but-survived Core heals between sieges, so a breach is a setback, not a death spiral. + +**3. The SOFT-loss edge lives INSIDE `CyclePhaseSystem`** (which stays the sole Phase/WaveState writer — no second Phase writer, dodging the documented invisible-cycle hazard). In the Siege branch, checked BEFORE survival: `Current<=0` → +- flip to **Calm** with **NO** `GoalProgress` reward (you were overrun, not survived); +- drain a fraction (`CoreOverrunDrainPct`, default 0.5) of the shared `StorageEntry` ledger via the new pure `StorageMath.DrainFraction`; +- **despawn every remaining Husk** (the locked "siege ends on breach" fork) + reset `WaveState`; +- stamp `OverrunTick` (a TRANSIENT pulse, see decision 5); +- autosave the wounded state. + +**4. The Core is a FALLBACK target in `EnemyAISystem`** (the locked targeting fork — present-the-forks ritual, 2026-06-12). When no living player/structure remains, undefended Husks march on `PlotCenter` (target `Entity.Null`, the `DamageEvent` strike append is guarded — the Core takes damage only via the proximity `CoreDamageSystem`, not the strike path). This guarantees "a Husk the team fails to intercept walks to the Engine" instead of the swarm idling, **without** the Core competing with structures (preserving EB-1's structure-destruction stakes). + +**5. `OverrunTick` is a transient pulse, NOT a latching `RunOutcome`** — a *deliberate deviation* from the spec's literal "sets `RunOutcome{Overrun}`". A SOFT loss is **non-terminal** ("keep playing"), so a latching terminal outcome is the wrong shape; the HUD edge-detects the `OverrunTick` change and flashes a ~3.5s "BASE OVERRUN" banner. The latching `RunOutcome` (Victory) is deferred to **END-2**, where a win genuinely latches. + +**6. Persistence: `SaveData` → v4** adds `CoreCurrent` (additive; `MinLoadableVersion` stays 2). A pre-v4 save lacks the field → JsonUtility defaults it to 0 → born-correct maps 0 → baked Max (full), reusing EB-1's exact HP `0→Max` sentinel convention. A wounded base persists across save/quit (the locked fork). + +**7. Three live `TuningConfig` knobs** (with baked fallbacks): `CoreDamagePerHusk` (10), `CoreRegenIntervalTicks` (18), `CoreOverrunDrainPct` (0.5). `CoreReachRadius` stays a structural const. HUD: a red Core-integrity bar mirroring the GoalProgress bar. + +## Locked forks (END-1) + +Per [[DR-029_Path_A_Fork_Locks]] + the 2026-06-12 present-the-forks pass: +- **Lose-severity = SOFT** (drain ledger + end siege + persist wounded; no rollback). +- **Breach drains the Core bar only** — structure destruction stays EB-1's job. +- **Wounded base persists** in SaveData v4. +- **Core targeting = FALLBACK** (sought only when no live player/structure) — *recommended option, operator-chosen 2026-06-12* (vs. always-weighted vs. proximity-only). +- **Breach resolution = despawn remaining Husks** (clean reset-to-recover) — *operator-chosen 2026-06-12*. + +## Consequences + +- A real lose condition exists: an undefended base gets overrun, loses resources, and ends the siege wounded — the stakes the whole economy is for. +- **END-2 hooks:** `RunOutcome` (latching Victory byte) is END-2's to add on the same director ghost; the soft-loss `OverrunTick`/banner path is reused for the win banner. +- Reversible: lose-severity/damage/regen/drain are live singletons; the soft-vs-hard byte was NOT added (DR-029 locked soft) — a future hard-rollback mode would add it + host reload infra. +- 330/330 EditMode green (+12 END-1); Play-validated server==client + live drain→regen→replicate.