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,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.
|
||||
Reference in New Issue
Block a user