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>
8.3 KiB
date, type, tags, permalink
| date | type | tags | permalink | |||||||
|---|---|---|---|---|---|---|---|---|---|---|
| 2026-06-12 | session |
|
gamevault/07-sessions/2026/2026-06-12-eb2-felt-spend |
EB-2 — felt spend (turrets burn a shared Charge pool; a ledger-fed Fabricator mints it from Ore)
Path A milestone; forks locked in DR-029_Path_A_Fork_Locks (EB-2 = the mined resource must be spent on defense). Operator picked the ledger-fed Fabricator route. Full dots-dev Feature track under ultracode: mandatory pre-code adversarial review (folded in before coding) + post-code adversarial review (CLEAN — 2 quality nits, both fixed in-commit). Locked → DR-033_EB2_Felt_Spend_Charge_Economy. Continues 2026-06-11_EB1_Machines_Can_Die.
The hole EB-2 closes
EB-1 gave a structure stakes (it can be lost). But mined Ore had no ongoing sink — a turret defended for free, so the harvest loop had no point past the first build. EB-2 makes defense consume the harvest: mine Ore → a ledger-fed Fabricator converts it to Charge → turrets spend Charge per shot and soft-fail when the base runs dry.
What shipped — reuse the ledger + the recipe machine
Charge = a shared ammo RESOURCE (no new wire surface)
ResourceId.Charge = 4(byte id space None0/Aether1/Ore2/Biomass3/Charge4). It rides the EXISTING[GhostField] StorageEntryledger buffer — replicates to the HUD for free, no new[GhostField], no per-turret AmmoCount. Ammo is a base-wide pool (not a magazine) so a siege applies economy pressure, not a reload minigame.
Turrets spend Charge atomically, soft-fail when dry
TurretFireSystemresolves the ledger ONCE (GetSingletonEntity<ResourceLedger>+GetBuffer<StorageEntry>— neverGetSingleton<StorageEntry>, a 2nd buffer exists on the base container). Per turret with a target:cost=max(1,Tuning.TurretChargeCostPerShot);got=StorageMath.Withdraw(ledger,Charge,cost); got≥cost ⇒ appendDamageEvent+ advanceNextTick; got>0 (future cost>1 partial) ⇒ refund the partial (never consume without firing); else ⇒ soft-fail. A soft-fail burns no cooldown → the turret fires the instant Charge returns. Turrets share the finite pool in query order (deterministic; later turrets soft-fail as it empties).- New
StorageMath.TotalOf(buffer,itemId)(sums matching rows; 0 if itemId==0) backs the affordability read.
A ledger-fed Fabricator mints the Charge
Fabricator.InputFromLedger(abyte, server-only, NOT a[GhostField], NOT an enum):!=0⇒ input from the SHARED ledger;0⇒ legacyMachineInputchain. Both deposit the output to the ledger. The ledger total is read LIVE inside the per-machine loop (StorageMath.TotalOf) so two ledger-fed machines split a finite pool — the 2nd sees the 1st's same-tick withdrawal (a hoisted read would double-spend negative). Reuses the input-limited catch-up (runs=min(ProductionMath.CyclesDue, affordable); ticks viaTickUtil.NonZero+IsNewerThan).Fabricator.prefabre-baked to 1 Ore → 3 Charge / 30 ticks,InputFromLedger=1. SubsceneStructureCatalogAuthoringre-enables ONLY the Fabricator → 4 entries (Turret/Wall/Pylon/Fabricator); Harvester/Conveyor staynull(reserved M7, code intact). TheMachineInputbuffer is kept on the prefab (the production query needs it as a column).- No
[UpdateBefore/After]edge betweenTurretFireSystemandFabricatorProductionSystem— a Fabricator deposit lands next tick (~16 ms); ordering for same-tick visibility risks a sorter cycle for no felt gain.
The spend is legible (HUD, observe-only)
- A 4th violet
Chargechip (flat color, no icon — HudTheme has no Charge sprite, null-safe) readsItemId==ResourceId.Chargein the EXISTING single ledger loop. A GLOBAL quiet-turret cue overrides the location banner whensiege && ledger Charge==0 && !onExpedition→ "TURRETS OUT OF CHARGE — build a Fabricator (Ore→Charge)". Global (not per-turret) so the deterministic finite-pool split never reads as one broken turret.HudSystemonly observes; the cue is derived client-side from already-replicated state.
No persistence change
SaveDatastays v3: the recipe + ledger-fed mode are prefab identity (InputFromLedgerbaked, not saved). A restored player-built Fabricator Instantiates the baked prefab ⇒ inheritsInputFromLedger=1for free.
Adversarial reviews (both workflowed, ultracode)
- Pre-code (multi-lens → synthesis): set the contract folded in before coding — Charge as a ledger resource (no new GhostField), the live in-loop read, the single ledger resolve, the atomic soft-fail/refund, the no-ordering-edge trade-off, the global (not per-turret) HUD cue, no SaveData bump.
- Post-code (3 lenses → adversarial verify → synthesis): CLEAN — 2 of 4 candidate findings confirmed, both quality nits (no correctness bug): (1) a stale
FabricatorAuthoringclass summary still said "2 Ore → 1 Aether" (doc-only; tooltips were already right) → corrected; (2) the ledger-fed catch-up affordance clamp (CyclesDue>1bound by the ledger) had no regression pin → addedLedgerFed_Runs_Are_Clamped_To_Affordable_Ledger_Under_CatchUp. Both folded into this commit for zero debt.
Validation
- EditMode: 318/318 (313 prior + 5 EB-2: turret soft-fail / consume-one-Charge / two-turrets-share-finite-pool; ledger-fed withdraw-from-ledger / two-machines-split-via-live-read; + the post-review catch-up-clamp pin). The existing 4 turret tests were updated for the new
RequireForUpdate<ResourceLedger>(they now seed a Charge pool — else they'd silently soft-fail and fail). Zero console errors at every compile. - Play (live netcode, unfocused editor, runInBackground ticking): world creation clean (no ordering cycle); baked
StructureCatalog= 4 entries, Fabricator row Prefab≠Null; baked Fabricator recipe = In=Ore(2)×1, Out=Charge(4)×3, Period=30, InputFromLedger=1; a live instantiated ledger-fed Fabricator converted Ore 100→53 (−47) / Charge 0→141 (+47×3) exactly over 47 periods — the mined-Ore→turret-ammo pipe is live and deterministic. Only console error = a benign FMOD audio-device init (machine audio, unrelated). - Operator hands-on fun-gate is OPEN: mine Ore → build a Fabricator (Ore→Charge) → survive a Siege as turrets burn the pool → feel the base run dry ("TURRETS OUT OF CHARGE"). Tune
TurretChargeCostPerShot+ the recipe ratio to taste.
Deliberate cuts / notes
- Per-turret magazine/reload, a
[GhostField] AmmoCount, a dedicated Charge system, and ordering the two systems — all rejected (see DR-033_EB2_Felt_Spend_Charge_Economy); the shared-pool + Fabricator-reuse path is leaner and more economically coupled. - END-2 heads-up: the Fabricator was repurposed Ore→Aether → Ore→Charge, so the default palette no longer has an automated Aether source — END-2's win economy must not assume one.
Files
- Modified (Simulation):
Economy/ResourceNode.cs(ResourceId.Charge),HomeBase/StorageMath.cs(TotalOf),Automation/AutomationComponents.cs(Fabricator.InputFromLedger),Tuning.cs(TurretChargeCostPerShot). - Modified (Server):
Automation/FabricatorProductionSystem.cs(ledger-fed branch, live in-loop read),Building/TurretFireSystem.cs(single ledger resolve + atomic Charge spend / soft-fail / partial-refund). - Modified (Authoring/Client):
Authoring/Automation/FabricatorAuthoring.cs(recipe defaults + bakeInputFromLedger+ corrected summary),Client/Presentation/HudSystem.cs(violet Charge chip + global quiet-turret cue). - Assets:
Prefabs/Fabricator.prefab(recipe 1 Ore→3 Charge/30t, ledger-fed),Subscenes/Gameplay.unity(catalog re-enables Fabricator → 4 entries). - Tests:
FabricatorProductionSystemTests.cs(+3 ledger-fed cases),TurretFireSystemTests.cs(seed Charge pool + 3 spend/soft-fail cases).
Next-session intent
EB-2 closes the factory→defense pipe (mined Ore is now spent). The next Path A braid is END-1 (a losable Core integrity bar) — the aggregate base-health meter EB-1/EB-2 deliberately kept out — then END-2 (explicit win/lose + run resolution). If the spend doesn't bite in the operator's hands, tune TurretChargeCostPerShot ↑ / the recipe ratio ↓ before moving on; if turrets feel starved too easily, the inverse.