Docs: EB-2 session log + DR-033; CLAUDE.md felt-spend bullet

DR-033 records the felt-spend design (shared Charge ammo, atomic soft-fail,
ledger-fed Fabricator, no-ordering-edge trade-off, global HUD cue, no SaveData
bump). CLAUDE.md adds the EB-2 ★ bullet net-zero (trimmed bullets archived to
the gotchas archive under a 2026-06-12 heading).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 19:15:10 -07:00
parent 44da26cdf6
commit 3fdac3517b
4 changed files with 137 additions and 9 deletions
+9 -9
View File
@@ -85,15 +85,15 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui
- **Hit/area tests must be SWEPT, not point checks** — a point check tunnels when the per-tick step exceeds the target radius (high speed *or* tick-batching); test the segment traversed this tick. **In a PLAIN `SimulationSystemGroup` system do NOT use `SystemAPI.Time.DeltaTime`** (wall-frame delta, not the fixed step) — store the per-tick step on the projectile (`Projectile.LastStep`, written in the fixed-step group) and rebuild the segment as `cur - dir*LastStep`. `ecb.DestroyEntity` **at-most-once** per tick (destroyed-bitset; double destroy throws at Playback). **TWO target types in one pass: UNIFY into one best-target loop + one shared bitset** (separate sweeps double-destroy a projectile overlapping both — DR-018). **A per-hit yield `(int)` cast that also gates despawn is an immortal-sink** (sub-1.0→0→no deposit, shot still consumed): guard `math.max(1,(int)yield)` + `[Min(1f)]` authoring.
### Build / structures / grid
- **Build-grid math must be deterministic + integer-stable:** corner-origin, center-returning, **half-open** cell bounds, `math.floor` (not truncation — negatives). Lock `CellSize`/`PlotSize` as a coordinate space once (`BaseGridMath`, EditMode-tested) — changing them invalidates placed structures.
- **`PlacedStructure{[GhostField] byte Type; int2 Cell (server-only); uint NextTick; uint LastProcessedTick}`** on an ownerless interpolated ghost. **Bake the two tick fields** (turret reuses `NextTick` as fire cooldown; they're the offline-catch-up linchpin). Only `Type` replicates (client derives `Cell` via `BaseGridMath.WorldToCell`). Data-driven `StructureCatalog` buffer. **Occupancy is DERIVED** by scanning live structure ghosts into a Temp `NativeHashSet<int2>`, never a mutable buffer on the baked `BaseAnchor`. See [[DR-014_M6_Build_Structures_Automation_Foundation]].
- **Co-op placement atomicity:** commit the `StorageMath.Withdraw` + cell-reservation **in-place inside the RPC foreach** (only `Instantiate` goes through the ECB) so two same-tick requests for one cell can't both pass.
- **Buildable turret = hitscan:** nearest living Husk in-region within Range, on `NextTick` cooldown appends a direct `DamageEvent{SourceNetworkId=-1}` → reuses `HealthApplyDamageSystem`. No projectile → no tunnelling.
- **EB-1 machines can die ★ (DR-032):** structures (Turret/Wall/Pylon) 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 → an undefended base is razed; `EnemyAIMath.PickWeightedNearest`; `StructureAggroWeight` knob, <1 prefers structures, SQUARED). Loss VFX = proximity-gated `StructureFeedbackSystem` (`CombatFeedbackSystem` gated `!isStructure`). See [[DR-032_EB1_Machines_Can_Die]].
- **Resource-gated ability tiers / buffs reuse `StatModifier`** (replace/clear-by-SourceId → bounded buffer; `StatRecomputeSystem` folds it into `EffectiveAbilityStats` both worlds). `GoalProgress{[GhostField] int Charge, Target}` rides the global CycleDirector ghost.
- **M7 Automation (server-only; TRIMMED from the live palette, code intact) ★:** `Harvester`/`Conveyor`/`Fabricator` on `PlacedStructure`; server-only `MachineInput`/`MachineOutput`; 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 node → shared `ResourceLedger`** (build currency, no `G`-friction), **Expedition/un-taggedPERSONAL `InventorySlot`** (`[GhostField]` `OwnerSendType.All`, spill→ledger) — BOTH projectile (`ResourceHarvestSystem`) + melee (`MeleeComboSystem` server block, `Remaining` write-back), region via OPTIONAL `ComponentLookup<RegionTag>` (not a query column → no fixture-drop). `G`-key `InventoryDepositRequest` deposits personal→ledger. Items = `ItemDatabase` blob (`ushort ItemId` subsumes `ResourceId`; mods INLINE on `ItemDefBlob`, NOT nested — by-value `TryGetItem` reads nested empty). Equip = event-driven `EquipSystem` (weapon→`AbilityRef.Id`; gear→`StatModifier`s by slot-`SourceId`). Session-only. 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 set SAME-ECB born-correct). `SaveService.Load` = additive floor `[MinLoadableVersion=2, Current]` (old saves load; a missing field 0-defaults). See [[DR-019_Frontend_Menu_Settings_Saves_Build]] · [[DR-032_EB1_Machines_Can_Die]].
- **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]].
- **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<ResourceLedger>` (NEVER `GetSingleton<StorageEntry>`): 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]].
- **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) **BASEshared `ResourceLedger`**, **Expedition/un-taggedPERSONAL `InventorySlot`** (`[GhostField]` `OwnerSendType.All`, spill→ledger; region via OPTIONAL `ComponentLookup<RegionTag>`); `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]].
- **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
- **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`**.