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