EB-2: felt spend - turrets burn a shared Charge pool, ledger-fed Fabricator mints it from Ore

Mined Ore now has an ongoing sink: a ledger-fed Fabricator converts Ore->Charge
(1 Ore -> 3 Charge / 30t) and turrets spend Charge per shot, soft-failing (no
shot, no cooldown burn) when the shared pool runs dry.

- ResourceId.Charge=4 rides the existing [GhostField] StorageEntry ledger (no new wire).
- TurretFireSystem: single ledger resolve + atomic spend / soft-fail / partial-refund.
- Fabricator.InputFromLedger (byte, server-only) feeds input from the shared ledger,
  read live in-loop so two machines split a finite pool; both modes deposit to ledger.
- HudSystem: violet Charge chip + global quiet-turret cue when siege && Charge==0.
- StorageMath.TotalOf backs the affordability read; catalog re-enables the Fabricator (4 entries).

See DR-033.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 19:14:52 -07:00
parent e04cdea44f
commit 2da29783fd
10 changed files with 94 additions and 28 deletions
@@ -28,6 +28,7 @@ namespace ProjectM.Client
static readonly Color AetherCyan = new(0.30f, 0.85f, 1f);
static readonly Color OreAmber = new(1f, 0.72f, 0.35f);
static readonly Color BioGreen = new(0.55f, 0.85f, 0.45f);
static readonly Color ChargeViolet = new(0.80f, 0.45f, 1f);
static readonly Color PanelDark = new(0.08f, 0.11f, 0.15f, 0.90f);
static readonly Color PanelWarm = new(0.16f, 0.09f, 0.09f, 0.88f);
static readonly Color PipDim = new(0.25f, 0.30f, 0.36f, 0.9f);
@@ -58,7 +59,7 @@ namespace ProjectM.Client
readonly List<VisualElement> _pips = new();
// resources
Label _aetherNum, _oreNum, _bioNum;
Label _aetherNum, _oreNum, _bioNum, _chargeNum;
// build palette + hints
VisualElement _paletteRow, _hintBar, _facingArrow;
@@ -190,7 +191,7 @@ namespace ProjectM.Client
}
// ---- Resources (feed palette affordability) ----
int aether = 0, ore = 0, bio = 0;
int aether = 0, ore = 0, bio = 0, charge = 0;
if (SystemAPI.TryGetSingletonEntity<ResourceLedger>(out var ledgerE))
{
var buf = SystemAPI.GetBuffer<StorageEntry>(ledgerE);
@@ -200,11 +201,20 @@ namespace ProjectM.Client
if (en.ItemId == ResourceId.Aether) aether = en.Count;
else if (en.ItemId == ResourceId.Ore) ore = en.Count;
else if (en.ItemId == ResourceId.Biomass) bio = en.Count;
else if (en.ItemId == ResourceId.Charge) charge = en.Count;
}
}
_aetherNum.text = aether.ToString();
_oreNum.text = ore.ToString();
_bioNum.text = bio.ToString();
_chargeNum.text = charge.ToString();
// EB-2 quiet-turret cue (GLOBAL, not per-turret, so the deterministic Charge split never reads as one
// broken turret): a dry base during a siege tells the player to build a Fabricator.
if (siege && charge == 0 && !onExpedition)
{
_locationText.text = "TURRETS OUT OF CHARGE - build a Fabricator (Ore -> Charge)";
_locationText.style.color = new Color(1f, 0.4f, 0.9f);
}
// ---- Threat readout (top-right) — hidden entirely at base with zero husks; its reappearance is the cue ----
bool showThreat = siege || huskCount > 0;
@@ -710,6 +720,7 @@ namespace ProjectM.Client
strip.Add(ResourceChip(theme != null ? theme.AetherIcon : null, AetherCyan, "0", out _aetherNum, 26, 20));
strip.Add(ResourceChip(theme != null ? theme.OreIcon : null, OreAmber, "0", out _oreNum, 30, 22));
strip.Add(ResourceChip(theme != null ? theme.BioIcon : null, BioGreen, "0", out _bioNum, 26, 20));
strip.Add(ResourceChip(null, ChargeViolet, "0", out _chargeNum, 26, 20)); // EB-2 turret ammo (flat violet, no icon)
root.Add(strip);
}