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 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 12:38:46 -07:00
parent aac1813a93
commit 6769fc3de9
6 changed files with 154 additions and 15 deletions
+10 -9
View File
@@ -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, M1M6 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, M1M6 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<int2>`, 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<int2>`, 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<ResourceLedger>` (NEVER `GetSingleton<StorageEntry>`): 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<ResourceLedger>` (NEVER `GetSingleton<StorageEntry>`): 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<T>()` — 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`**.