LANTERN purge B3+B5: delete the cycle/core/win-lose spine + onboarding; save epoch v7

Deletes CyclePhaseSystem, GoalReachedSystem, CoreDamage/CoreRestore, ThreatDirector,
CoreIntegrity/GoalProgress/RunPhase/RunOutcome/ThreatState components,
CoreVisualFeedbackSystem, and the whole Client/Onboarding slice (+6 test files).

Keepers reworked: RunDirectorSystem (UpdateBefore attr + launch guard + goal/threat
bank removed; sole SaveRequest raiser now), CycleDirectorSpawnSystem (ledger/meta
host only), WaveSystem UNGATED (waves run wherever a WaveDirector is baked),
EnemyAISystem core-fallback stripped, AmbientAudioSystem reworked (bed + run cues;
no CycleState gate), MusicSystem RunInfo-only, HudSystem big trim (goal meter, core
bar, siege banner, terminal banner, outcome flash, onboarding hook all gone),
MetaShop/ClassPrep/AimReticle siege gates dropped, DebugOverlay/ops re-meant
(SpawnWave=force next wave, EndSiege=quiet arena; SetCalm/AdvanceGoal/SetHeat
retired, bytes reserved), TuningConfig Core knobs retired (ids 20-23 reserved),
StorageMath.DrainFraction deleted, HowToPlay copy rewritten.

Save epoch v7 (fresh epoch, operator-approved): SaveData drops goal/core/outcome +
conveyor/machine-IO fields; MinLoadableVersion=7; PendingSave/PendingStructure
trimmed; RollTerminalCampaignForward deleted; SaveStructureScan signature slimmed.

390 tests green; Play world-creation clean (player + waves live, no exceptions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 15:27:12 -07:00
parent 835dace213
commit b34945c2d2
74 changed files with 233 additions and 3955 deletions
@@ -16,6 +16,9 @@ namespace ProjectM.Simulation
dest.Add(new StorageEntry { ItemId = src[i].ItemId, Count = src[i].Count });
}
/// <summary>EB-1: map a serialized <see cref="StructureSave"/> to the staged <see cref="PendingStructure"/>
/// (the menu->ServerWorld copy in WorldLauncher). Pure so the field-for-field copy — including the
/// easy-to-miss HP — is unit-tested; an omitted field here silently restores every structure at full HP.</summary>
/// <summary>EB-1: map a serialized <see cref="StructureSave"/> to the staged <see cref="PendingStructure"/>
/// (the menu->ServerWorld copy in WorldLauncher). Pure so the field-for-field copy — including the
/// easy-to-miss HP — is unit-tested; an omitted field here silently restores every structure at full HP.</summary>
@@ -24,10 +27,6 @@ namespace ProjectM.Simulation
Type = s.Type,
CellX = s.CellX,
CellZ = s.CellZ,
Direction = s.Direction,
RemainingTicks = s.RemainingTicks,
ConveyorResId = s.ConveyorResId,
ConveyorCount = s.ConveyorCount,
HP = s.HP,
};
}
@@ -5,23 +5,13 @@ namespace ProjectM.Simulation
/// <summary>
/// Server-world, UNMANAGED bridge holding a save slice the menu staged for a "Continue" session, applied
/// AT SPAWN by the server CycleDirectorSpawnSystem so the director ghost is BORN correct — it never
/// serializes a default <see cref="GoalProgress"/> / empty ledger to clients (no replication flicker). The
/// menu creates exactly one of these (with the <see cref="PendingSaveLedgerRow"/> buffer) in the freshly
/// created ServerWorld BEFORE the gameplay subscene streams in; the spawn system consumes + destroys it.
/// Unmanaged so the Bursted spawn system reads it without a managed bridge.
/// serializes an empty ledger to clients (no replication flicker). The menu creates exactly one of these
/// (with the <see cref="PendingSaveLedgerRow"/> buffer) in the freshly created ServerWorld BEFORE the
/// gameplay subscene streams in; the spawn system consumes + destroys it. Unmanaged so the Bursted spawn
/// system reads it without a managed bridge.
/// </summary>
public struct PendingSave : IComponentData
{
public int GoalCharge;
public int GoalTarget;
/// <summary>END-1: Engine Core integrity to restore (0 = pre-v4 save / New Game -> born full at baked Max).</summary>
public int CoreCurrent;
/// <summary>END-2: terminal run outcome to restore (0 = InProgress / pre-v5 save / New Game; 1 = Victory,
/// 2 = Loss -> the run loads finished + halted, no re-arm). Born-correct at director spawn.</summary>
public byte RunOutcome;
/// <summary>v6: persisted run counters to restore into MetaCounters + the born-correct RunInfo HUD mirror.</summary>
public int RunsCompleted;
public int MaxDepthReached;
@@ -40,14 +30,15 @@ namespace ProjectM.Simulation
/// <summary>One staged PERMANENT meta tier for a Continue session (v6) — copied VERBATIM into the director's
/// replicated MetaTierState buffer at spawn (unknown ids preserved for round-trip; clamping happens only at
/// seed/shop/spend). The buffer is added UNCONDITIONALLY at staging (empty OK) — the Bursted spawn system
/// GetBuffers it inside the HasData block and a missing buffer would throw on any v&lt;=5 Continue.</summary>
/// GetBuffers it inside the HasData block and a missing buffer would throw on any Continue.</summary>
public struct PendingMetaRow : IBufferElementData
{
public byte ClassId;
public byte UpgradeId;
public byte Tier;
}
/// <summary>One staged player-built structure row for a Continue session (M7); BaseRestoreSystem replays it
/// <summary>One staged player-built structure row for a Continue session; BaseRestoreSystem replays it
/// charge-free into the freshly-streamed base. Mirrors <see cref="StructureSave"/> but as an unmanaged ECS
/// buffer element (staged in the ServerWorld before the subscene streams).</summary>
public struct PendingStructure : IBufferElementData
@@ -55,28 +46,13 @@ namespace ProjectM.Simulation
public byte Type;
public int CellX;
public int CellZ;
public byte Direction;
public uint RemainingTicks;
public byte ConveyorResId;
public int ConveyorCount;
public float HP; // EB-1: staged hit points (BaseRestoreSystem restores 0 -> baked Max)
}
/// <summary>One staged machine I/O row (M7), joined to the <see cref="PendingStructure"/> buffer by index.
/// Slot 0 = MachineInput, 1 = MachineOutput.</summary>
public struct PendingStructureIo : IBufferElementData
{
public int StructureIndex;
public byte Slot;
public byte ResourceId;
public int Count;
}
/// <summary>
/// Host-only autosave request flag on the CycleDirector entity (added at spawn). The Bursted CyclePhaseSystem
/// sets <see cref="Pending"/>=1 on the Siege-&gt;Calm checkpoint; the managed SaveWriteSystem reads it, writes
/// the JSON save, and clears it. A plain byte => Burst-safe (no managed/string/file touch in the sim loop).
/// Host-only autosave request flag on the director entity (added at spawn). RunDirectorSystem sets
/// <see cref="Pending"/>=1 on the terminal bank; the managed SaveWriteSystem reads it, writes the JSON save,
/// and clears it. A plain byte => Burst-safe (no managed/string/file touch in the sim loop).
/// </summary>
public struct SaveRequest : IComponentData
{
@@ -21,67 +21,38 @@ namespace ProjectM.Simulation
public byte Tier;
}
/// <summary>
/// One serialized player-built structure (M7). Flat scalars (JsonUtility has no int2). The production
/// cooldown is stored as REMAINING ticks (epoch-independent) so it survives the server-tick origin reset on a
/// fresh session; the in-flight conveyor item (if any) rides here, while variable-length machine I/O buffers
/// live in the flat <see cref="SaveData.StructureIo"/> table keyed by index.
/// </summary>
/// <summary>One serialized player-built structure. Flat scalars (JsonUtility has no int2).</summary>
[Serializable]
public struct StructureSave
{
public byte Type;
public int CellX;
public int CellZ;
public byte Direction; // conveyor facing (0 for non-conveyors)
public uint RemainingTicks; // production/cooldown ticks left at save time
public byte ConveyorResId; // in-flight conveyor item resource (0 = none)
public int ConveyorCount;
public float HP; // EB-1: hit points at save time (0 from a pre-v3 save -> restored to baked Max)
public float HP; // EB-1: hit points at save time (0 -> restored to baked Max)
}
/// <summary>
/// One serialized machine I/O buffer row, joined to <see cref="SaveData.Structures"/> by
/// <see cref="StructureIndex"/>. A flat top-level array (JsonUtility can't nest arrays-of-arrays); Slot 0 =
/// MachineInput, Slot 1 = MachineOutput.
/// </summary>
[Serializable]
public struct StructureIoRow
{
public int StructureIndex;
public byte Slot;
public byte ResourceId;
public int Count;
}
/// <summary>
/// Versioned, host-authoritative save slice (the FOUNDATION): the long-arc goal charge/target + the shared
/// resource ledger. JsonUtility-friendly — a class with flat fields and an array FIELD (never a root array).
/// The schema is intentionally ADDITIVE: future fields (placed structures, threat, storage) append without
/// breaking old saves, gated by <see cref="Version"/> migration.
/// Versioned, host-authoritative save slice: the shared resource ledger + player-built structures + the
/// permanent meta. JsonUtility-friendly — a class with flat fields and an array FIELD (never a root array).
/// The schema is ADDITIVE going forward, gated by <see cref="Version"/> migration.
/// </summary>
[Serializable]
public class SaveData
{
public const int CurrentVersion = 6; // v6: permanent META (per-class upgrade tiers + run counters); v5 added RunOutcome; v4 added CoreCurrent
public const int CurrentVersion = 7; // v7: LANTERN fresh epoch — goal/core/outcome + conveyor/machine-I/O fields dropped
/// <summary>Oldest save schema the loader accepts (additive); a v2 save loads with structures at full HP.</summary>
public const int MinLoadableVersion = 2;
/// <summary>Oldest save schema the loader accepts. v7 is a FRESH EPOCH (operator-approved): older saves are ignored.</summary>
public const int MinLoadableVersion = 7;
public int Version = CurrentVersion;
public int GoalCharge;
public int GoalTarget;
public int CoreCurrent; // END-1: Engine Core integrity at save time (0 from a pre-v4 save -> restored to baked Max)
public int RunOutcome; // END-2: 0=InProgress (also any pre-v5 save) / 1=Victory / 2=Loss -> a finished run loads finished
// v6 — permanent meta-progression (0/empty-defaults on any v<=5 save):
// Permanent meta-progression:
public int RunsCompleted; // boss-cleared runs (the HUD counter + the HostSalt fold at restore)
public int MaxDepthReached; // deepest room actually CLEARED across all runs (honest depth, never planned)
public MetaUpgradeSave[] MetaUpgrades = Array.Empty<MetaUpgradeSave>(); // sparse per-class tiers
public LedgerRow[] Ledger = Array.Empty<LedgerRow>();
public StructureSave[] Structures = Array.Empty<StructureSave>();
public StructureIoRow[] StructureIo = Array.Empty<StructureIoRow>();
public long SavedAtMs;
}
}
@@ -44,13 +44,7 @@ namespace ProjectM.Simulation
/// placed structures survive untouched. Without this, Continue/PLAY AGAIN after a win re-latches the dead
/// outcome banner on the first replicated snapshot. Pure + idempotent; no-op for in-progress saves.
/// </summary>
public static void RollTerminalCampaignForward(SaveData data)
{
if (data == null || data.RunOutcome == 0) return;
data.RunOutcome = 0;
data.GoalCharge = 0;
data.CoreCurrent = 0;
}
public static void Save(SaveData data)
@@ -13,7 +13,7 @@ namespace ProjectM.Simulation
/// </summary>
public static class SaveStructureScan
{
public static void Collect(EntityManager em, uint nowTick, out StructureSave[] structures, out StructureIoRow[] io)
public static void Collect(EntityManager em, uint nowTick, out StructureSave[] structures)
{
var structs = new List<StructureSave>();
@@ -38,7 +38,6 @@ namespace ProjectM.Simulation
}
structures = structs.ToArray();
io = System.Array.Empty<StructureIoRow>(); // machine I/O retired with the automation chain (row type dies at save v7)
}
}
}