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
@@ -0,0 +1,63 @@
---
date: 2026-06-12
type: session
tags:
- session
- combat
- structures
- economy
- automation
- netcode
- eb-2
permalink: 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] StorageEntry` ledger 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
- `TurretFireSystem` resolves the ledger ONCE (`GetSingletonEntity<ResourceLedger>`+`GetBuffer<StorageEntry>` — never `GetSingleton<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** ⇒ append `DamageEvent` + advance `NextTick`; **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` (a **`byte`**, server-only, **NOT a `[GhostField]`, NOT an enum**): `!=0` ⇒ input from the SHARED ledger; `0` ⇒ legacy `MachineInput` chain. **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 via `TickUtil.NonZero`+`IsNewerThan`).
- `Fabricator.prefab` re-baked to **1 Ore → 3 Charge / 30 ticks, `InputFromLedger=1`**. Subscene `StructureCatalogAuthoring` re-enables ONLY the Fabricator → **4 entries** (Turret/Wall/Pylon/Fabricator); Harvester/Conveyor stay `null` (reserved M7, code intact). The `MachineInput` buffer is **kept** on the prefab (the production query needs it as a column).
- **No `[UpdateBefore/After]` edge** between `TurretFireSystem` and `FabricatorProductionSystem` — 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** `Charge` chip (flat color, no icon — HudTheme has no Charge sprite, null-safe) reads `ItemId==ResourceId.Charge` in the EXISTING single ledger loop. A **GLOBAL** quiet-turret cue overrides the location banner when `siege && 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. `HudSystem` only observes; the cue is derived client-side from already-replicated state.
### No persistence change
- `SaveData` stays **v3**: the recipe + ledger-fed mode are **prefab identity** (`InputFromLedger` baked, not saved). A restored player-built Fabricator Instantiates the baked prefab ⇒ inherits `InputFromLedger=1` for 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 `FabricatorAuthoring` class summary still said "2 Ore → 1 Aether" (doc-only; tooltips were already right) → corrected; (2) the ledger-fed *catch-up* affordance clamp (`CyclesDue>1` bound by the ledger) had no regression pin → added `LedgerFed_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 + bake `InputFromLedger` + 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.
@@ -0,0 +1,54 @@
---
id: DR-033
title: EB-2 = felt spend — turrets burn a shared Charge ammo pool, a ledger-fed Fabricator mints it (Ore→Charge)
status: accepted
date: 2026-06-12
tags:
- decision
- design
- combat
- structures
- economy
- automation
- netcode
- eb-2
permalink: gamevault/07-sessions/decisions/dr-033-eb2-felt-spend-charge-economy
---
# DR-033 — EB-2: Felt Spend (the Ore→Charge→turret-ammo pipe)
## Context
Session [[2026-06-12_EB2_Felt_Spend]] · forks locked [[DR-029_Path_A_Fork_Locks]] · combat thesis [[DR-028_Combat_Primary_Verb_Depth_First]] · base loop [[DR-031_Base_Mining_Loop_Cohesion]] · machines-can-die [[DR-032_EB1_Machines_Can_Die]] · automation [[DR-020_M7_Automation_Production_Chains]] · inventory/ledger [[DR-026_Inventory_Equipment_Progression_Foundation]].
After EB-1, you mine Ore→build→a structure can be lost. But the mined Ore had no ongoing SINK: a turret defended for free, so the economy had no pressure and the harvest loop had no *point* past the first build. EB-2's locked fork (DR-029): **felt spend** — defense must *consume* the mined resource. Operator chose, via the milestone fork, the **ledger-fed Fabricator** route for ammo production (over a per-turret magazine or a raw-tap mint). Ran the standing **pre-code adversarial review** (folded in before coding) and a **post-code** review (CLEAN — 2 quality nits, both fixed in-commit).
## Decision
**1. Turret ammo is a SHARED RESOURCE (`Charge`), not a per-turret magazine.** A new `ResourceId.Charge = 4` (byte id space: None0/Aether1/Ore2/Biomass3/**Charge4**) rides the EXISTING `[GhostField] StorageEntry` ledger buffer — so it replicates to the HUD for free with **no new wire surface** (no `[GhostField]`, no per-turret `AmmoCount`). Ammo being a base-wide pool (not a magazine) is the whole point: every turret draws from the same stockpile, so the player feels the *economy* pressure of a siege, not a per-gun reload minigame. Rejected a per-turret magazine/reload (more replicated state, less economic coupling).
**2. Turrets spend Charge ATOMICALLY with a SOFT-FAIL.** `TurretFireSystem` resolves the ledger ONCE via `GetSingletonEntity<ResourceLedger>()`+`GetBuffer<StorageEntry>()` (NEVER `GetSingleton<StorageEntry>` — a 2nd `StorageEntry` buffer exists on the base container). Per turret with a valid target: `cost = max(1, Tuning.TurretChargeCostPerShot)`; `got = StorageMath.Withdraw(ledger, Charge, cost)`; **`got>=cost`** ⇒ append the `DamageEvent` + advance the `NextTick` cooldown; **`got>0`** (a future cost>1 partial) ⇒ `Deposit` the partial back (never consume Charge without firing); **else** ⇒ soft-fail. A soft-fail burns **no cooldown**, so the turret fires the instant Charge returns — a dry siege reads as "the base is starving," not "the turret is broken." Turrets share the finite pool in query order (deterministic; later turrets soft-fail as it empties).
**3. A LEDGER-FED Fabricator mints the Charge.** `Fabricator.InputFromLedger` (a `byte`, server-only, **NOT a `[GhostField]`, NOT an enum** — dodges the cross-assembly-enum-in-Burst hazard) toggles the input source: `!=0` ⇒ withdraw the input from the SHARED ledger; `0` ⇒ the legacy `MachineInput` conveyor chain. **Both** modes deposit the output to the ledger. The ledger total is read **LIVE inside the per-machine loop** (`StorageMath.TotalOf(ledger,inId)`), never hoisted, so two ledger-fed machines split a finite pool correctly (the 2nd sees the 1st's same-tick withdrawal — a hoisted read would double-spend the pool negative). It reuses the proven input-limited catch-up (`runs = min(ProductionMath.CyclesDue, affordable)`; tick fields via `TickUtil.NonZero` + `NetworkTick.IsNewerThan`). Rejected a dedicated Charge-production system (the Fabricator already IS the input-limited recipe machine). The re-enabled `Fabricator.prefab` bakes **1 Ore → 3 Charge / 30 ticks, `InputFromLedger=1`**; the subscene `StructureCatalog` re-enables ONLY the Fabricator (**4 entries**: Turret/Wall/Pylon/Fabricator) — Harvester/Conveyor stay null (reserved M7, code intact).
**4. No system-ordering edge between the two systems — a ≤1-tick lag is accepted.** `TurretFireSystem` and `FabricatorProductionSystem` are both plain server `SimulationSystemGroup` `[UpdateAfter(...)]`-anchored; adding an `[UpdateBefore/After]` edge between them to make a Fabricator deposit visible to a same-tick turret shot risks a sorter cycle for a one-tick gain. The deposit lands next tick (~16 ms) — imperceptible. (Matches the EB-1 turret/`HealthApplyDamageSystem` same-class trade-off.)
**5. The spend is LEGIBLE on the HUD (observe-only).** A 4th violet `Charge` chip reads `ItemId==ResourceId.Charge` in the EXISTING single ledger loop; a **GLOBAL** quiet-turret cue overrides the location banner when `siege && 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. `HudSystem` only OBSERVES replicated state. No new `[GhostField]`; the cue is derived client-side from the already-replicated ledger + cycle phase.
**6. No SaveData schema bump.** The recipe + ledger-fed mode are **prefab identity** (`InputFromLedger` is baked, not saved); the catalog re-enable + recipe live in the subscene/prefab. A restored player-built Fabricator Instantiates the baked prefab ⇒ inherits `InputFromLedger=1` for free. `SaveData` stays v3.
## Consequences
- The economy now has a **sink**: mined Ore is spent into defense, so the base-mining loop (DR-031) has an ongoing *point* and a siege applies real pressure. An undefended *and* dry base is doubly legible (EB-1's razing + EB-2's "out of charge" banner). **Operator fun-gate OPEN** ("does spending the harvest feel good / does running dry bite?").
- **318/318 EditMode** (313 prior + the 5 EB-2 tests + the post-review catch-up-clamp pin); live netcode Play clean — baked catalog = 4 entries + correct recipe, a live ledger-fed Fabricator converted **47 Ore / +141 Charge** exactly over 47 periods, 0 project console errors.
- Live knobs: `Tuning.TurretChargeCostPerShot` (cost/shot), the prefab recipe ratio (`OutAmount`/`PeriodTicks`/`InAmount`), `FabricatorCostOre` (build cost).
- **Note for END-2:** the Fabricator was repurposed Ore→Aether → **Ore→Charge**, so the default palette no longer has an automated *Aether* source. END-2's win-condition economy must not assume one.
## Alternatives considered (rejected)
- **Per-turret magazine + reload** — more replicated per-turret state, a reload minigame, and it decouples defense from the shared economy (the opposite of "felt spend"). A shared Charge pool couples every turret to the harvest.
- **A new `[GhostField] AmmoCount`** — Charge-in-the-ledger already replicates via the stock `StorageEntry` `[GhostField]`; a parallel field is dead wire surface.
- **A dedicated Charge-production system** — duplicates the Fabricator's input-limited catch-up + ledger resolve; the `InputFromLedger` byte reuses it.
- **Ordering the turret/Fabricator systems for same-tick deposit visibility** — cycle risk for a one-tick (~16 ms) gain; the lag is imperceptible.
- **Minting Charge from a raw Aether/free tap** — would bypass Ore as the sink and break the "mine→spend" pressure; the Fabricator eats *Ore* so the base-mining loop is the driver.
- **A per-turret "out of ammo" tint instead of the global banner** — the deterministic finite-pool split means a single turret can soft-fail while others fire; a per-turret cue would misread as a bug. The global banner states the true (economy) cause.