From 2da29783fd92974b5898267c20c0aaa55eb61cc7 Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Fri, 12 Jun 2026 19:14:52 -0700 Subject: [PATCH] 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 --- Assets/_Project/Prefabs/Fabricator.prefab | 9 ++--- .../Automation/FabricatorAuthoring.cs | 21 +++++++----- .../Scripts/Client/Presentation/HudSystem.cs | 15 +++++++-- .../Automation/FabricatorProductionSystem.cs | 17 ++++++---- .../Server/Building/TurretFireSystem.cs | 33 +++++++++++++++---- .../Automation/AutomationComponents.cs | 4 +++ .../Simulation/Economy/ResourceNode.cs | 3 ++ .../Simulation/HomeBase/StorageMath.cs | 13 ++++++++ Assets/_Project/Scripts/Simulation/Tuning.cs | 5 +++ Assets/_Project/Subscenes/Gameplay.unity | 2 +- 10 files changed, 94 insertions(+), 28 deletions(-) diff --git a/Assets/_Project/Prefabs/Fabricator.prefab b/Assets/_Project/Prefabs/Fabricator.prefab index 45b83cc45..214dfbdde 100644 --- a/Assets/_Project/Prefabs/Fabricator.prefab +++ b/Assets/_Project/Prefabs/Fabricator.prefab @@ -144,7 +144,8 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.FabricatorAuthoring InResourceId: 2 - InAmount: 2 - OutResourceId: 1 - OutAmount: 1 - PeriodTicks: 90 + InAmount: 1 + OutResourceId: 4 + OutAmount: 3 + PeriodTicks: 30 + InputFromLedger: 1 diff --git a/Assets/_Project/Scripts/Authoring/Automation/FabricatorAuthoring.cs b/Assets/_Project/Scripts/Authoring/Automation/FabricatorAuthoring.cs index f80e9faa3..8f393a743 100644 --- a/Assets/_Project/Scripts/Authoring/Automation/FabricatorAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Automation/FabricatorAuthoring.cs @@ -6,19 +6,23 @@ namespace ProjectM.Authoring { /// /// Authoring for a Fabricator machine ghost prefab. Bakes {Type=Fabricator} + - /// recipe + an empty buffer (a conveyor fills it; the - /// fabricator deposits its output directly into the GLOBAL ledger, so it needs no output buffer). Default - /// recipe: 2 Ore -> 1 Aether (both existing resources — the "auto-gather existing resources" terminal). + /// recipe + a (kept) empty buffer — the production query + /// needs the buffer as a column, but a ledger-fed Fabricator ignores it. It deposits its output directly into + /// the GLOBAL ledger, so it needs no output buffer. Default recipe (EB-2): 1 Ore -> 3 Charge, ledger-fed + /// ( != 0) — mints the turret-ammo Charge that TurretFireSystem + /// spends per shot. /// public class FabricatorAuthoring : MonoBehaviour { [Tooltip("Input resource id consumed per run (1=Aether, 2=Ore, 3=Biomass).")] public byte InResourceId = 2; // Ore - [Min(1)] public int InAmount = 2; - [Tooltip("Output resource id deposited to the global ledger.")] - public byte OutResourceId = 1; // Aether - [Min(1)] public int OutAmount = 1; - [Min(1)] public int PeriodTicks = 90; + [Min(1)] public int InAmount = 1; + [Tooltip("Output resource id deposited to the global ledger (4 = EB-2 Charge / turret ammo).")] + public byte OutResourceId = 4; // Charge + [Min(1)] public int OutAmount = 3; + [Min(1)] public int PeriodTicks = 30; + [Tooltip("EB-2: 1 = ledger-fed (consume the input from the shared ledger; no conveyor). 0 = M7 MachineInput chain.")] + public byte InputFromLedger = 1; private class FabricatorBaker : Baker { @@ -39,6 +43,7 @@ namespace ProjectM.Authoring OutResourceId = authoring.OutResourceId, OutAmount = authoring.OutAmount, PeriodTicks = authoring.PeriodTicks, + InputFromLedger = authoring.InputFromLedger, }); AddBuffer(entity); } diff --git a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs index 0a16c7bcc..3aa41b9c4 100644 --- a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs +++ b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs @@ -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 _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(out var ledgerE)) { var buf = SystemAPI.GetBuffer(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); } diff --git a/Assets/_Project/Scripts/Server/Automation/FabricatorProductionSystem.cs b/Assets/_Project/Scripts/Server/Automation/FabricatorProductionSystem.cs index 8cb103f61..631bae9f4 100644 --- a/Assets/_Project/Scripts/Server/Automation/FabricatorProductionSystem.cs +++ b/Assets/_Project/Scripts/Server/Automation/FabricatorProductionSystem.cs @@ -75,16 +75,21 @@ namespace ProjectM.Server byte inId = fab.ValueRO.InResourceId; int inAmount = fab.ValueRO.InAmount; - // Input-limited: never produce more than the buffered input affords (no mint-from-nothing). A - // zero/negative recipe input amount is treated as unsatisfiable rather than dividing by zero. - int affordable = inAmount > 0 - ? MachineSlotMath.TotalOf(input, inId) / inAmount - : 0; + // Input-limited: never produce more than the available input affords (no mint-from-nothing). EB-2: + // a ledger-fed Fabricator (InputFromLedger != 0) sources its input from the SHARED ledger (read LIVE + // here so a 2nd ledger-fed Fabricator sees the 1st's same-tick withdrawal) instead of MachineInput; + // both modes deposit the output to the ledger. A zero/negative input amount is unsatisfiable. + bool fromLedger = fab.ValueRO.InputFromLedger != 0; + int available = fromLedger ? StorageMath.TotalOf(ledger, inId) : MachineSlotMath.TotalOf(input, inId); + int affordable = inAmount > 0 ? available / inAmount : 0; int runs = math.min(cycles, affordable); if (runs > 0) { - MachineSlotMath.Withdraw(input, inId, inAmount * runs); + if (fromLedger) + StorageMath.Withdraw(ledger, inId, inAmount * runs); + else + MachineSlotMath.Withdraw(input, inId, inAmount * runs); StorageMath.Deposit(ledger, (ushort)fab.ValueRO.OutResourceId, fab.ValueRO.OutAmount * runs); } diff --git a/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs b/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs index 150ff7da8..1e8259446 100644 --- a/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs +++ b/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs @@ -28,6 +28,7 @@ namespace ProjectM.Server public void OnCreate(ref SystemState state) { state.RequireForUpdate(); + state.RequireForUpdate(); state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly())); } @@ -59,6 +60,12 @@ namespace ProjectM.Server return; } + // EB-2: resolve the shared ledger ONCE (NEVER GetSingleton — a 2nd StorageEntry buffer + // exists on the base container). Turrets withdraw Charge from it sequentially (a finite pool split in + // query order; later turrets soft-fail when it empties). + var ledgerEntity = SystemAPI.GetSingletonEntity(); + var ledger = SystemAPI.GetBuffer(ledgerEntity); + var ecb = new EntityCommandBuffer(Allocator.Temp); foreach (var (ps, turret, xform, region) in @@ -92,14 +99,26 @@ namespace ProjectM.Server if (best >= 0) { - ecb.AppendToBuffer(huskEntities[best], new DamageEvent + // EB-2 felt spend: a shot costs Charge from the shared ledger. Gate BOTH the damage AND the + // cooldown advance on a SUCCESSFUL withdraw — out of Charge = SOFT-FAIL (no shot, no cooldown + // burn, so the turret fires the instant Charge returns). Refund a partial (cost>1 underflow). + int cost = math.max(1, Tuning.TurretChargeCostPerShot); + int got = StorageMath.Withdraw(ledger, ResourceId.Charge, cost); + if (got >= cost) { - Amount = turret.ValueRO.Damage, - SourceNetworkId = -1, - SourceTick = TickUtil.NonZero(now), - }); - uint cd = (uint)math.max(1, turret.ValueRO.CooldownTicks); - ps.ValueRW.NextTick = TickUtil.NonZero(now + cd); + ecb.AppendToBuffer(huskEntities[best], new DamageEvent + { + Amount = turret.ValueRO.Damage, + SourceNetworkId = -1, + SourceTick = TickUtil.NonZero(now), + }); + uint cd = (uint)math.max(1, turret.ValueRO.CooldownTicks); + ps.ValueRW.NextTick = TickUtil.NonZero(now + cd); + } + else if (got > 0) + { + StorageMath.Deposit(ledger, ResourceId.Charge, got); // never consume Charge without firing + } } } diff --git a/Assets/_Project/Scripts/Simulation/Automation/AutomationComponents.cs b/Assets/_Project/Scripts/Simulation/Automation/AutomationComponents.cs index f8f54f48c..85c7ef87a 100644 --- a/Assets/_Project/Scripts/Simulation/Automation/AutomationComponents.cs +++ b/Assets/_Project/Scripts/Simulation/Automation/AutomationComponents.cs @@ -33,6 +33,10 @@ namespace ProjectM.Simulation public byte OutResourceId; public int OutAmount; public int PeriodTicks; + + /// EB-2: 0 = consume the input from the MachineInput buffer (the M7 conveyor chain); !=0 = consume + /// the input from the SHARED ledger (a base-loop ledger-fed Fabricator, e.g. Ore -> Charge). Server-only, NO [GhostField]. + public byte InputFromLedger; } /// diff --git a/Assets/_Project/Scripts/Simulation/Economy/ResourceNode.cs b/Assets/_Project/Scripts/Simulation/Economy/ResourceNode.cs index 34305a816..c12768a3f 100644 --- a/Assets/_Project/Scripts/Simulation/Economy/ResourceNode.cs +++ b/Assets/_Project/Scripts/Simulation/Economy/ResourceNode.cs @@ -17,6 +17,9 @@ namespace ProjectM.Simulation /// Biomass — misc / crafting. public const byte Biomass = 3; + + /// EB-2 turret munition ("Charge") — a ledger-only ammo currency a ledger-fed Fabricator mints from Ore. + public const byte Charge = 4; } /// diff --git a/Assets/_Project/Scripts/Simulation/HomeBase/StorageMath.cs b/Assets/_Project/Scripts/Simulation/HomeBase/StorageMath.cs index b56bdb2e1..98bbae6c7 100644 --- a/Assets/_Project/Scripts/Simulation/HomeBase/StorageMath.cs +++ b/Assets/_Project/Scripts/Simulation/HomeBase/StorageMath.cs @@ -58,5 +58,18 @@ namespace ProjectM.Simulation return 0; } + + /// Total count of across the buffer (0 if absent / itemId 0). EB-2 ledger + /// affordability read; mirrors MachineSlotMath.TotalOf. Pure, non-generic, Burst-safe. + public static int TotalOf(DynamicBuffer buffer, ushort itemId) + { + if (itemId == 0) + return 0; + int total = 0; + for (int i = 0; i < buffer.Length; i++) + if (buffer[i].ItemId == itemId) + total += buffer[i].Count; + return total; + } } } diff --git a/Assets/_Project/Scripts/Simulation/Tuning.cs b/Assets/_Project/Scripts/Simulation/Tuning.cs index a08bf5f81..3dbb2c865 100644 --- a/Assets/_Project/Scripts/Simulation/Tuning.cs +++ b/Assets/_Project/Scripts/Simulation/Tuning.cs @@ -57,6 +57,11 @@ namespace ProjectM.Simulation /// catch-up after any skipped ticks; restore re-seats the baseline so this never reflects wall-clock). public const int MaxProductionCatchup = 600; + /// EB-2: Charge (turret munition) consumed per turret shot, withdrawn from the global ledger. A + /// turret with 0 Charge SOFT-FAILS (no shot, no cooldown advance). A ledger-fed Fabricator mints Charge from + /// Ore. Operator feel-fork: keep generous so turrets stay fed while you keep mining. + public const int TurretChargeCostPerShot = 1; + // ---- Inventory (per-player bag; InventoryMath / ResourceHarvestSystem / InventoryDepositSystem) ---- /// Max stacks a player can carry; InventoryMath rejects deposits past this and the harvest remainder spills to the global ledger. diff --git a/Assets/_Project/Subscenes/Gameplay.unity b/Assets/_Project/Subscenes/Gameplay.unity index 6265d1251..3550a8f8d 100644 --- a/Assets/_Project/Subscenes/Gameplay.unity +++ b/Assets/_Project/Subscenes/Gameplay.unity @@ -901,7 +901,7 @@ MonoBehaviour: PylonCostOre: 2 HarvesterPrefab: {fileID: 0} HarvesterCostOre: 20 - FabricatorPrefab: {fileID: 0} + FabricatorPrefab: {fileID: 3885353946372160549, guid: 8dd9baab4cbf6c04f9320ed5ed764c65, type: 3} FabricatorCostOre: 30 ConveyorPrefab: {fileID: 0} ConveyorCostOre: 2