Files
kronic 60e1e21dd3 END-1: the base can be lost - a losable Engine Core with integrity
Adds CoreIntegrity{[GhostField] Current,Max,OverrunTick} on the GLOBAL
CycleDirector ghost (no new ghost/relevancy). CoreDamageSystem (server,
after EnemyAISystem): a Husk within ~3u of PlotCenter drains + is consumed;
CoreRestoreSystem regenerates only in Calm. The SOFT-loss edge lives inside
CyclePhaseSystem (sole Phase writer): Current<=0 in Siege flips to Calm with
NO goal reward, StorageMath.DrainFraction drains the shared ledger, all Husks
despawn, and OverrunTick is stamped (a transient HUD-flash pulse, not a
latching outcome - the Victory latch is END-2's). EnemyAISystem treats the
Core as a FALLBACK target so an undefended base is overrun instead of idling.
SaveData -> v4 persists CoreCurrent (0 -> born full, the EB-1 HP sentinel);
3 live TuningConfig knobs + a red HUD Core bar. Soft-loss + targeting +
breach-resolution forks operator-locked.

See DR-034.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 21:51:43 -07:00

100 lines
3.9 KiB
C#

using Unity.Entities;using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// Pure, deterministic merge logic for a shared storage container's <see cref="StorageEntry"/> buffer.
/// No RNG / wall-clock, so server and (future) prediction agree. Deposit merges into an existing row
/// for the same item or appends a new row; Withdraw decrements and drops a row that hits zero, clamping
/// to available. DynamicBuffer is a handle, so mutations apply to the underlying entity buffer.
/// Unit-tested in EditMode via a plain Entities world.
/// </summary>
public static class StorageMath
{
/// <summary>Add <paramref name="count"/> of <paramref name="itemId"/>, merging into an existing row if present. No-op for count &lt;= 0 or itemId 0.</summary>
public static void Deposit(DynamicBuffer<StorageEntry> buffer, ushort itemId, int count)
{
if (count <= 0 || itemId == 0)
return;
for (int i = 0; i < buffer.Length; i++)
{
if (buffer[i].ItemId == itemId)
{
var entry = buffer[i];
entry.Count += count;
buffer[i] = entry;
return;
}
}
buffer.Add(new StorageEntry { ItemId = itemId, Count = count });
}
/// <summary>
/// Remove up to <paramref name="count"/> of <paramref name="itemId"/>, clamped to what is available.
/// Drops the row when it reaches zero. Returns the amount actually withdrawn (0 if none). No-op for
/// count &lt;= 0 or itemId 0.
/// </summary>
public static int Withdraw(DynamicBuffer<StorageEntry> buffer, ushort itemId, int count)
{
if (count <= 0 || itemId == 0)
return 0;
for (int i = 0; i < buffer.Length; i++)
{
if (buffer[i].ItemId == itemId)
{
var entry = buffer[i];
int taken = entry.Count < count ? entry.Count : count;
entry.Count -= taken;
if (entry.Count <= 0)
buffer.RemoveAt(i);
else
buffer[i] = entry;
return taken;
}
}
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;
}
/// <summary>END-1 soft-loss penalty: remove a FRACTION (0..1) of EVERY row, floored per row, dropping any
/// row that hits zero. Pure/deterministic (no RNG, no wall-clock), Burst-safe; iterates back-to-front so a
/// dropped row never skips its successor. No-op for fraction &lt;= 0.</summary>
public static void DrainFraction(DynamicBuffer<StorageEntry> buffer, float fraction)
{
fraction = math.clamp(fraction, 0f, 1f);
if (fraction <= 0f)
return;
for (int i = buffer.Length - 1; i >= 0; i--)
{
var entry = buffer[i];
int drop = (int)math.floor(entry.Count * fraction);
if (drop <= 0)
continue;
entry.Count -= drop;
if (entry.Count <= 0)
buffer.RemoveAt(i);
else
buffer[i] = entry;
}
}
}
}