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:
@@ -144,7 +144,8 @@ MonoBehaviour:
|
|||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.FabricatorAuthoring
|
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.FabricatorAuthoring
|
||||||
InResourceId: 2
|
InResourceId: 2
|
||||||
InAmount: 2
|
InAmount: 1
|
||||||
OutResourceId: 1
|
OutResourceId: 4
|
||||||
OutAmount: 1
|
OutAmount: 3
|
||||||
PeriodTicks: 90
|
PeriodTicks: 30
|
||||||
|
InputFromLedger: 1
|
||||||
|
|||||||
@@ -6,19 +6,23 @@ namespace ProjectM.Authoring
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authoring for a Fabricator machine ghost prefab. Bakes <see cref="PlacedStructure"/>{Type=Fabricator} +
|
/// Authoring for a Fabricator machine ghost prefab. Bakes <see cref="PlacedStructure"/>{Type=Fabricator} +
|
||||||
/// <see cref="Fabricator"/> recipe + an empty <see cref="MachineInput"/> buffer (a conveyor fills it; the
|
/// <see cref="Fabricator"/> recipe + a (kept) empty <see cref="MachineInput"/> buffer — the production query
|
||||||
/// fabricator deposits its output directly into the GLOBAL ledger, so it needs no output buffer). Default
|
/// needs the buffer as a column, but a ledger-fed Fabricator ignores it. It deposits its output directly into
|
||||||
/// recipe: 2 Ore -> 1 Aether (both existing resources — the "auto-gather existing resources" terminal).
|
/// the GLOBAL ledger, so it needs no output buffer. Default recipe (EB-2): 1 Ore -> 3 Charge, ledger-fed
|
||||||
|
/// (<see cref="Fabricator.InputFromLedger"/> != 0) — mints the turret-ammo Charge that <c>TurretFireSystem</c>
|
||||||
|
/// spends per shot.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class FabricatorAuthoring : MonoBehaviour
|
public class FabricatorAuthoring : MonoBehaviour
|
||||||
{
|
{
|
||||||
[Tooltip("Input resource id consumed per run (1=Aether, 2=Ore, 3=Biomass).")]
|
[Tooltip("Input resource id consumed per run (1=Aether, 2=Ore, 3=Biomass).")]
|
||||||
public byte InResourceId = 2; // Ore
|
public byte InResourceId = 2; // Ore
|
||||||
[Min(1)] public int InAmount = 2;
|
[Min(1)] public int InAmount = 1;
|
||||||
[Tooltip("Output resource id deposited to the global ledger.")]
|
[Tooltip("Output resource id deposited to the global ledger (4 = EB-2 Charge / turret ammo).")]
|
||||||
public byte OutResourceId = 1; // Aether
|
public byte OutResourceId = 4; // Charge
|
||||||
[Min(1)] public int OutAmount = 1;
|
[Min(1)] public int OutAmount = 3;
|
||||||
[Min(1)] public int PeriodTicks = 90;
|
[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<FabricatorAuthoring>
|
private class FabricatorBaker : Baker<FabricatorAuthoring>
|
||||||
{
|
{
|
||||||
@@ -39,6 +43,7 @@ namespace ProjectM.Authoring
|
|||||||
OutResourceId = authoring.OutResourceId,
|
OutResourceId = authoring.OutResourceId,
|
||||||
OutAmount = authoring.OutAmount,
|
OutAmount = authoring.OutAmount,
|
||||||
PeriodTicks = authoring.PeriodTicks,
|
PeriodTicks = authoring.PeriodTicks,
|
||||||
|
InputFromLedger = authoring.InputFromLedger,
|
||||||
});
|
});
|
||||||
AddBuffer<MachineInput>(entity);
|
AddBuffer<MachineInput>(entity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace ProjectM.Client
|
|||||||
static readonly Color AetherCyan = new(0.30f, 0.85f, 1f);
|
static readonly Color AetherCyan = new(0.30f, 0.85f, 1f);
|
||||||
static readonly Color OreAmber = new(1f, 0.72f, 0.35f);
|
static readonly Color OreAmber = new(1f, 0.72f, 0.35f);
|
||||||
static readonly Color BioGreen = new(0.55f, 0.85f, 0.45f);
|
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 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 PanelWarm = new(0.16f, 0.09f, 0.09f, 0.88f);
|
||||||
static readonly Color PipDim = new(0.25f, 0.30f, 0.36f, 0.9f);
|
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();
|
readonly List<VisualElement> _pips = new();
|
||||||
|
|
||||||
// resources
|
// resources
|
||||||
Label _aetherNum, _oreNum, _bioNum;
|
Label _aetherNum, _oreNum, _bioNum, _chargeNum;
|
||||||
|
|
||||||
// build palette + hints
|
// build palette + hints
|
||||||
VisualElement _paletteRow, _hintBar, _facingArrow;
|
VisualElement _paletteRow, _hintBar, _facingArrow;
|
||||||
@@ -190,7 +191,7 @@ namespace ProjectM.Client
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Resources (feed palette affordability) ----
|
// ---- 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))
|
if (SystemAPI.TryGetSingletonEntity<ResourceLedger>(out var ledgerE))
|
||||||
{
|
{
|
||||||
var buf = SystemAPI.GetBuffer<StorageEntry>(ledgerE);
|
var buf = SystemAPI.GetBuffer<StorageEntry>(ledgerE);
|
||||||
@@ -200,11 +201,20 @@ namespace ProjectM.Client
|
|||||||
if (en.ItemId == ResourceId.Aether) aether = en.Count;
|
if (en.ItemId == ResourceId.Aether) aether = en.Count;
|
||||||
else if (en.ItemId == ResourceId.Ore) ore = 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.Biomass) bio = en.Count;
|
||||||
|
else if (en.ItemId == ResourceId.Charge) charge = en.Count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_aetherNum.text = aether.ToString();
|
_aetherNum.text = aether.ToString();
|
||||||
_oreNum.text = ore.ToString();
|
_oreNum.text = ore.ToString();
|
||||||
_bioNum.text = bio.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 ----
|
// ---- Threat readout (top-right) — hidden entirely at base with zero husks; its reappearance is the cue ----
|
||||||
bool showThreat = siege || huskCount > 0;
|
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.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.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(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);
|
root.Add(strip);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,15 +75,20 @@ namespace ProjectM.Server
|
|||||||
byte inId = fab.ValueRO.InResourceId;
|
byte inId = fab.ValueRO.InResourceId;
|
||||||
int inAmount = fab.ValueRO.InAmount;
|
int inAmount = fab.ValueRO.InAmount;
|
||||||
|
|
||||||
// Input-limited: never produce more than the buffered input affords (no mint-from-nothing). A
|
// Input-limited: never produce more than the available input affords (no mint-from-nothing). EB-2:
|
||||||
// zero/negative recipe input amount is treated as unsatisfiable rather than dividing by zero.
|
// a ledger-fed Fabricator (InputFromLedger != 0) sources its input from the SHARED ledger (read LIVE
|
||||||
int affordable = inAmount > 0
|
// here so a 2nd ledger-fed Fabricator sees the 1st's same-tick withdrawal) instead of MachineInput;
|
||||||
? MachineSlotMath.TotalOf(input, inId) / inAmount
|
// both modes deposit the output to the ledger. A zero/negative input amount is unsatisfiable.
|
||||||
: 0;
|
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);
|
int runs = math.min(cycles, affordable);
|
||||||
|
|
||||||
if (runs > 0)
|
if (runs > 0)
|
||||||
{
|
{
|
||||||
|
if (fromLedger)
|
||||||
|
StorageMath.Withdraw(ledger, inId, inAmount * runs);
|
||||||
|
else
|
||||||
MachineSlotMath.Withdraw(input, inId, inAmount * runs);
|
MachineSlotMath.Withdraw(input, inId, inAmount * runs);
|
||||||
StorageMath.Deposit(ledger, (ushort)fab.ValueRO.OutResourceId, fab.ValueRO.OutAmount * runs);
|
StorageMath.Deposit(ledger, (ushort)fab.ValueRO.OutResourceId, fab.ValueRO.OutAmount * runs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace ProjectM.Server
|
|||||||
public void OnCreate(ref SystemState state)
|
public void OnCreate(ref SystemState state)
|
||||||
{
|
{
|
||||||
state.RequireForUpdate<NetworkTime>();
|
state.RequireForUpdate<NetworkTime>();
|
||||||
|
state.RequireForUpdate<ResourceLedger>();
|
||||||
state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly<Turret>()));
|
state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly<Turret>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +60,12 @@ namespace ProjectM.Server
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EB-2: resolve the shared ledger ONCE (NEVER GetSingleton<StorageEntry> — 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<ResourceLedger>();
|
||||||
|
var ledger = SystemAPI.GetBuffer<StorageEntry>(ledgerEntity);
|
||||||
|
|
||||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||||
|
|
||||||
foreach (var (ps, turret, xform, region) in
|
foreach (var (ps, turret, xform, region) in
|
||||||
@@ -91,6 +98,13 @@ namespace ProjectM.Server
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (best >= 0)
|
if (best >= 0)
|
||||||
|
{
|
||||||
|
// 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)
|
||||||
{
|
{
|
||||||
ecb.AppendToBuffer(huskEntities[best], new DamageEvent
|
ecb.AppendToBuffer(huskEntities[best], new DamageEvent
|
||||||
{
|
{
|
||||||
@@ -101,6 +115,11 @@ namespace ProjectM.Server
|
|||||||
uint cd = (uint)math.max(1, turret.ValueRO.CooldownTicks);
|
uint cd = (uint)math.max(1, turret.ValueRO.CooldownTicks);
|
||||||
ps.ValueRW.NextTick = TickUtil.NonZero(now + cd);
|
ps.ValueRW.NextTick = TickUtil.NonZero(now + cd);
|
||||||
}
|
}
|
||||||
|
else if (got > 0)
|
||||||
|
{
|
||||||
|
StorageMath.Deposit(ledger, ResourceId.Charge, got); // never consume Charge without firing
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ecb.Playback(state.EntityManager);
|
ecb.Playback(state.EntityManager);
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ namespace ProjectM.Simulation
|
|||||||
public byte OutResourceId;
|
public byte OutResourceId;
|
||||||
public int OutAmount;
|
public int OutAmount;
|
||||||
public int PeriodTicks;
|
public int PeriodTicks;
|
||||||
|
|
||||||
|
/// <summary>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].</summary>
|
||||||
|
public byte InputFromLedger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ namespace ProjectM.Simulation
|
|||||||
|
|
||||||
/// <summary>Biomass — misc / crafting.</summary>
|
/// <summary>Biomass — misc / crafting.</summary>
|
||||||
public const byte Biomass = 3;
|
public const byte Biomass = 3;
|
||||||
|
|
||||||
|
/// <summary>EB-2 turret munition ("Charge") — a ledger-only ammo currency a ledger-fed Fabricator mints from Ore.</summary>
|
||||||
|
public const byte Charge = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -58,5 +58,18 @@ namespace ProjectM.Simulation
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Total count of <paramref name="itemId"/> across the buffer (0 if absent / itemId 0). EB-2 ledger
|
||||||
|
/// affordability read; mirrors MachineSlotMath.TotalOf. Pure, non-generic, Burst-safe.</summary>
|
||||||
|
public static int TotalOf(DynamicBuffer<StorageEntry> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ namespace ProjectM.Simulation
|
|||||||
/// catch-up after any skipped ticks; restore re-seats the baseline so this never reflects wall-clock).</summary>
|
/// catch-up after any skipped ticks; restore re-seats the baseline so this never reflects wall-clock).</summary>
|
||||||
public const int MaxProductionCatchup = 600;
|
public const int MaxProductionCatchup = 600;
|
||||||
|
|
||||||
|
/// <summary>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.</summary>
|
||||||
|
public const int TurretChargeCostPerShot = 1;
|
||||||
|
|
||||||
// ---- Inventory (per-player bag; InventoryMath / ResourceHarvestSystem / InventoryDepositSystem) ----
|
// ---- Inventory (per-player bag; InventoryMath / ResourceHarvestSystem / InventoryDepositSystem) ----
|
||||||
|
|
||||||
/// <summary>Max stacks a player can carry; InventoryMath rejects deposits past this and the harvest remainder spills to the global ledger.</summary>
|
/// <summary>Max stacks a player can carry; InventoryMath rejects deposits past this and the harvest remainder spills to the global ledger.</summary>
|
||||||
|
|||||||
@@ -901,7 +901,7 @@ MonoBehaviour:
|
|||||||
PylonCostOre: 2
|
PylonCostOre: 2
|
||||||
HarvesterPrefab: {fileID: 0}
|
HarvesterPrefab: {fileID: 0}
|
||||||
HarvesterCostOre: 20
|
HarvesterCostOre: 20
|
||||||
FabricatorPrefab: {fileID: 0}
|
FabricatorPrefab: {fileID: 3885353946372160549, guid: 8dd9baab4cbf6c04f9320ed5ed764c65, type: 3}
|
||||||
FabricatorCostOre: 30
|
FabricatorCostOre: 30
|
||||||
ConveyorPrefab: {fileID: 0}
|
ConveyorPrefab: {fileID: 0}
|
||||||
ConveyorCostOre: 2
|
ConveyorCostOre: 2
|
||||||
|
|||||||
Reference in New Issue
Block a user