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:
@@ -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.
|
||||
@@ -373,3 +373,14 @@ Added the **Per-player inventory + data-driven items ★** pointer (see [[DR-026
|
||||
- **UITK HUD ([[DR-021_HUD_UITK_BuildPalette]]):** dropped "behind the pause overlay's 100" and "`BuildPreviewMath` = the client mirror of the server check".
|
||||
- **Synty HudTheme ([[DR-024_HUD_Synty_Skin_Theme]]):** dropped "fonts = cached SDF, reset on `SubsystemRegistration`" and the "(NOT Resources)" aside.
|
||||
- **M7 Automation ([[DR-020_M7_Automation_Production_Chains]]):** tightened wording only, no detail removed.
|
||||
|
||||
## 2026-06-12 — CLAUDE.md condensation (EB-2 felt-spend bullet added; paid net-negative)
|
||||
|
||||
Added the **EB-2 felt spend ★** bullet ([[DR-033_EB2_Felt_Spend_Charge_Economy]]) to the *Build / structures / grid* section and clawed the file back below the 1 KB-headroom target (it had been carrying EB-1's ~560 B overage). The detail below was moved here from `CLAUDE.md` — all of it survives in the linked DRs/session notes; the inline one-liners now point here.
|
||||
|
||||
- **Per-player inventory + equipment + items ★ ([[DR-026_Inventory_Equipment_Progression_Foundation]] · [[DR-027_Equipment_Slots_Phase1]]):** the full pre-2026-06-12 bullet (CLAUDE.md now keeps only the region-routing half + a pointer):
|
||||
> Per-player inventory + equipment + items ★: harvest routes by node region (DR-031): **BASE node → shared `ResourceLedger`** (build currency, no `G`-friction), **Expedition/un-tagged → PERSONAL `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.
|
||||
- **EB-1 machines can die ([[DR-032_EB1_Machines_Can_Die]]):** dropped the parenthetical "(Turret/Wall/Pylon)" structure list from the inline bullet (the prefab list is in the DR) — kept the full destroy-gate + fortress-aggro + loss-VFX rules.
|
||||
- **Resource-gated ability tiers / buffs ([[DR-026_Inventory_Equipment_Progression_Foundation]]):** dropped the "(replace/clear-by-SourceId → bounded buffer; folds into … both worlds)" mechanism phrasing → kept "reuse `StatModifier` … `StatRecomputeSystem`→`EffectiveAbilityStats`". (Note: this bullet's `GoalProgress.Charge` is the **goal-meter** charge, unrelated to EB-2's `ResourceId.Charge` turret ammo.)
|
||||
- **PlacedStructure ([[DR-014_M6_Build_Structures_Automation_Foundation]]):** dropped the "(turret reuses `NextTick` as fire cooldown; they're the offline-catch-up linchpin)" + "Data-driven `StructureCatalog` buffer" asides → kept the field layout + the bake-the-tick-fields rule + the DERIVED-occupancy rule.
|
||||
- **M7 Automation ([[DR-020_M7_Automation_Production_Chains]]):** dropped the "server-only `MachineInput`/`MachineOutput`" restatement and noted `Fabricator` is now LIVE on the palette via EB-2 while `Harvester`/`Conveyor` stay trimmed (code intact).
|
||||
|
||||
Reference in New Issue
Block a user