Files
Project-M/Docs/Vault/07_Sessions/2026/2026-06-12_EB2_Felt_Spend.md
T
kronic 3fdac3517b 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>
2026-06-12 19:15:10 -07:00

8.3 KiB
Raw Blame History

date, type, tags, permalink
date type tags permalink
2026-06-12 session
session
combat
structures
economy
automation
netcode
eb-2
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.