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:
@@ -25,17 +25,17 @@ namespace ProjectM.Simulation
|
||||
/// <summary>Opcodes for <see cref="DebugCommandRequest.Op"/> (bytes — never an enum on the wire).</summary>
|
||||
public static class DebugOp
|
||||
{
|
||||
/// <summary>Arm + immediately fire a siege of ArgA Husks (ArgA = size).</summary>
|
||||
/// <summary>Force the NEXT wave to start this tick (re-meant from the old arm-a-siege op; args unused).</summary>
|
||||
public const byte SpawnWave = 0;
|
||||
|
||||
/// <summary>Collapse the current siege (cull Husks, stop spawning, clear pending) -> back to Calm.</summary>
|
||||
/// <summary>Quiet the arena: cull Husks + push the next wave ~1 h out (re-meant from the old end-siege op).</summary>
|
||||
public const byte EndSiege = 1;
|
||||
|
||||
/// <summary>Cull every living Husk now (leaves the phase alone).</summary>
|
||||
/// <summary>Cull every living Husk now (leaves wave state alone).</summary>
|
||||
public const byte ClearEnemies = 2;
|
||||
|
||||
/// <summary>Hard-reset the run-state to Calm (clears any siege/pending).</summary>
|
||||
public const byte SetCalm = 3;
|
||||
// RETIRED (LANTERN purge — keep the byte values reserved, never renumber a wire byte):
|
||||
// 3 = SetCalm · 10 = AdvanceGoal · 11 = SetHeat
|
||||
|
||||
/// <summary>Deposit ArgB of resource ArgA (a <see cref="ResourceId"/>) into the shared ledger.</summary>
|
||||
public const byte GrantResource = 4;
|
||||
@@ -55,11 +55,6 @@ namespace ProjectM.Simulation
|
||||
/// <summary>Kill the sender (Health -> 0; the normal death/respawn loop takes over).</summary>
|
||||
public const byte KillPlayer = 9;
|
||||
|
||||
/// <summary>Add ArgA to the long-arc goal charge.</summary>
|
||||
public const byte AdvanceGoal = 10;
|
||||
|
||||
/// <summary>Set ThreatState.Heat to ArgA (inert until the Heat source ships).</summary>
|
||||
public const byte SetHeat = 11;
|
||||
|
||||
/// <summary>Set the <see cref="TuningKnob"/> ArgA to ArgB/1000f (live dash/Charger feel-tuning; MC-0).</summary>
|
||||
public const byte SetTuning = 12;
|
||||
|
||||
@@ -50,16 +50,6 @@ namespace ProjectM.Simulation
|
||||
// preferred targets); a closer player 'in the way' still wins. Read server-side by EnemyAISystem.
|
||||
public float StructureAggroWeight;
|
||||
|
||||
// END-1 Engine Core (live feel knobs; read server-side by CoreDamageSystem/CoreRestoreSystem/CyclePhaseSystem).
|
||||
// CoreDamagePerHusk + CoreOverrunDrainPct are value knobs (>=0); CoreRegenIntervalTicks is a tick knob (>=1).
|
||||
public float CoreDamagePerHusk; // integrity drained by one breaching Husk (~5 unintercepted = serious dent)
|
||||
public float CoreRegenIntervalTicks; // ticks between +1 regen in Calm (18 -> +1/0.3s -> ~full over one short Calm)
|
||||
public float CoreOverrunDrainPct; // fraction (0..1) of the shared ledger lost on a breach (soft-loss penalty)
|
||||
|
||||
// END-2 final siege: the would-be-next normal siege size is multiplied by this for the FINAL siege so the
|
||||
// climax reads visibly larger. Floors at 1 (the default ClampKnob bucket) — a final siege is never smaller
|
||||
// than a normal one; GoalReachedSystem also math.max(1, ...) at the use-site.
|
||||
public float FinalSiegeMultiplier;
|
||||
|
||||
// Phase 1 B1/B2 (design review wf_fd177263): poise threshold on the EXISTING KnockbackState.Speed channel
|
||||
// (light melee 6 nudges; finisher 6x1.8=10.8 and cone 8 stagger), and the separation pass's push-speed cap.
|
||||
@@ -89,10 +79,6 @@ namespace ProjectM.Simulation
|
||||
MeleeFinisherMult = 1.8f, // finisher (last hit) scales dmg/range/recover/knockback
|
||||
MeleeComboLength = 3f, // light, light, finisher
|
||||
StructureAggroWeight = 0.7f, // EB-1: <1 prefers structures (fortress aggro); live-tunable
|
||||
CoreDamagePerHusk = 10f, // END-1: 10 breaching Husks = full loss; ~5 = a serious dent
|
||||
CoreRegenIntervalTicks = 18f, // END-1: +1 integrity / 0.3s in Calm (~30s to refill 100 from 0)
|
||||
CoreOverrunDrainPct = 0.5f, // END-1: a breach costs half the shared ledger (soft-loss penalty)
|
||||
FinalSiegeMultiplier = 2.5f, // END-2: the final siege is ~2.5x the would-be-next normal siege
|
||||
StaggerKnockbackSpeed = 7f, // B2 poise: kb.Speed >= this interrupts windups/lunges; below = nudge only
|
||||
SeparationMaxSpeed = 3f, // B1: max separation push (units/s) so soft-collision can't fling
|
||||
};
|
||||
@@ -114,8 +100,6 @@ namespace ProjectM.Simulation
|
||||
case TuningKnob.MeleeKnockbackSpeed:
|
||||
case TuningKnob.MeleeFinisherMult:
|
||||
case TuningKnob.StructureAggroWeight:
|
||||
case TuningKnob.CoreDamagePerHusk:
|
||||
case TuningKnob.CoreOverrunDrainPct:
|
||||
case TuningKnob.StaggerKnockbackSpeed:
|
||||
case TuningKnob.SeparationMaxSpeed:
|
||||
return math.max(0f, value);
|
||||
@@ -152,10 +136,6 @@ namespace ProjectM.Simulation
|
||||
case TuningKnob.MeleeFinisherMult: c.MeleeFinisherMult = value; break;
|
||||
case TuningKnob.MeleeComboLength: c.MeleeComboLength = value; break;
|
||||
case TuningKnob.StructureAggroWeight: c.StructureAggroWeight = value; break;
|
||||
case TuningKnob.CoreDamagePerHusk: c.CoreDamagePerHusk = value; break;
|
||||
case TuningKnob.CoreRegenIntervalTicks: c.CoreRegenIntervalTicks = value; break;
|
||||
case TuningKnob.CoreOverrunDrainPct: c.CoreOverrunDrainPct = value; break;
|
||||
case TuningKnob.FinalSiegeMultiplier: c.FinalSiegeMultiplier = value; break;
|
||||
case TuningKnob.StaggerKnockbackSpeed: c.StaggerKnockbackSpeed = value; break;
|
||||
case TuningKnob.SeparationMaxSpeed: c.SeparationMaxSpeed = value; break;
|
||||
// unknown index -> no-op (matches the no-default switch convention in DebugCommandReceiveSystem)
|
||||
@@ -187,10 +167,6 @@ namespace ProjectM.Simulation
|
||||
case TuningKnob.MeleeFinisherMult: return c.MeleeFinisherMult;
|
||||
case TuningKnob.MeleeComboLength: return c.MeleeComboLength;
|
||||
case TuningKnob.StructureAggroWeight: return c.StructureAggroWeight;
|
||||
case TuningKnob.CoreDamagePerHusk: return c.CoreDamagePerHusk;
|
||||
case TuningKnob.CoreRegenIntervalTicks: return c.CoreRegenIntervalTicks;
|
||||
case TuningKnob.CoreOverrunDrainPct: return c.CoreOverrunDrainPct;
|
||||
case TuningKnob.FinalSiegeMultiplier: return c.FinalSiegeMultiplier;
|
||||
case TuningKnob.StaggerKnockbackSpeed: return c.StaggerKnockbackSpeed;
|
||||
case TuningKnob.SeparationMaxSpeed: return c.SeparationMaxSpeed;
|
||||
default: return 0f;
|
||||
@@ -220,10 +196,6 @@ namespace ProjectM.Simulation
|
||||
MeleeFinisherMult = c.MeleeFinisherMult,
|
||||
MeleeComboLength = c.MeleeComboLength,
|
||||
StructureAggroWeight = c.StructureAggroWeight,
|
||||
CoreDamagePerHusk = c.CoreDamagePerHusk,
|
||||
CoreRegenIntervalTicks = c.CoreRegenIntervalTicks,
|
||||
CoreOverrunDrainPct = c.CoreOverrunDrainPct,
|
||||
FinalSiegeMultiplier = c.FinalSiegeMultiplier,
|
||||
StaggerKnockbackSpeed = c.StaggerKnockbackSpeed,
|
||||
SeparationMaxSpeed = c.SeparationMaxSpeed,
|
||||
};
|
||||
@@ -251,10 +223,6 @@ namespace ProjectM.Simulation
|
||||
MeleeFinisherMult = r.MeleeFinisherMult,
|
||||
MeleeComboLength = r.MeleeComboLength,
|
||||
StructureAggroWeight = r.StructureAggroWeight,
|
||||
CoreDamagePerHusk = r.CoreDamagePerHusk,
|
||||
CoreRegenIntervalTicks = r.CoreRegenIntervalTicks,
|
||||
CoreOverrunDrainPct = r.CoreOverrunDrainPct,
|
||||
FinalSiegeMultiplier = r.FinalSiegeMultiplier,
|
||||
StaggerKnockbackSpeed = r.StaggerKnockbackSpeed,
|
||||
SeparationMaxSpeed = r.SeparationMaxSpeed,
|
||||
};
|
||||
@@ -283,10 +251,8 @@ namespace ProjectM.Simulation
|
||||
public const byte MeleeFinisherMult = 17;
|
||||
public const byte MeleeComboLength = 18;
|
||||
public const byte StructureAggroWeight = 19;
|
||||
public const byte CoreDamagePerHusk = 20;
|
||||
public const byte CoreRegenIntervalTicks = 21;
|
||||
public const byte CoreOverrunDrainPct = 22;
|
||||
public const byte FinalSiegeMultiplier = 23;
|
||||
// RETIRED knob ids (LANTERN purge — keep 20-23 reserved, never renumber):
|
||||
// 20 = CoreDamagePerHusk · 21 = CoreRegenIntervalTicks · 22 = CoreOverrunDrainPct · 23 = FinalSiegeMultiplier
|
||||
public const byte StaggerKnockbackSpeed = 24;
|
||||
public const byte SeparationMaxSpeed = 25;
|
||||
|
||||
@@ -322,10 +288,6 @@ namespace ProjectM.Simulation
|
||||
public float MeleeFinisherMult;
|
||||
public float MeleeComboLength;
|
||||
public float StructureAggroWeight;
|
||||
public float CoreDamagePerHusk;
|
||||
public float CoreRegenIntervalTicks;
|
||||
public float CoreOverrunDrainPct;
|
||||
public float FinalSiegeMultiplier;
|
||||
public float StaggerKnockbackSpeed;
|
||||
public float SeparationMaxSpeed;
|
||||
}
|
||||
|
||||
@@ -76,24 +76,7 @@ namespace ProjectM.Simulation
|
||||
/// <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 <= 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<=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->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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// END-1 — the losable Engine Core. An aggregate base-integrity meter that rides the GLOBAL CycleDirector
|
||||
/// ghost (the untagged ghost already carrying <see cref="CycleState"/>/<see cref="GoalProgress"/>/the resource
|
||||
/// ledger), so it is visible to every player regardless of region with NO new ghost and NO relevancy work — it
|
||||
/// must NEVER be region-tagged (the shared-global-state rule; <c>SetIsIrrelevant</c> would hide it cross-region).
|
||||
/// <para>
|
||||
/// A Husk that breaches to the Core radius drains <see cref="Current"/> and despawns (server-only
|
||||
/// <c>CoreDamageSystem</c>); in Calm the Core regenerates toward <see cref="Max"/> (<c>CoreRestoreSystem</c>) so a
|
||||
/// chipped-but-survived base reads as "we got hurt but we're okay." When <see cref="Current"/> reaches 0 during a
|
||||
/// Siege the SOFT-loss edge fires once in <c>CyclePhaseSystem</c> (the sole Phase writer): the siege ends, the
|
||||
/// shared ledger is drained, the base persists wounded (no rollback — the locked DR-029 fork). <see cref="Max"/> is
|
||||
/// baked from <c>CycleDirectorAuthoring</c>; <see cref="Current"/> is born-correct at spawn (full, or the persisted
|
||||
/// wounded value from a Continue save).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public struct CoreIntegrity : IComponentData
|
||||
{
|
||||
/// <summary>Current integrity (0 = breached/overrun). Server-authoritative; replicated for the HUD bar.</summary>
|
||||
[GhostField] public int Current;
|
||||
|
||||
/// <summary>Integrity ceiling (baked from authoring; not persisted — a restored Core caps at the baked Max).</summary>
|
||||
[GhostField] public int Max;
|
||||
|
||||
/// <summary>Server tick of the most recent overrun breach (0 = never). A TRANSIENT pulse the client HUD
|
||||
/// edge-detects to flash a "BASE OVERRUN" banner — a SOFT loss is non-terminal ("keep playing"), so this is
|
||||
/// the right shape, not a latching run-outcome (the terminal Victory latch is END-2's job).</summary>
|
||||
[GhostField] public uint OverrunTick;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a933c59d7c550d844b615e3672b333f6
|
||||
@@ -3,75 +3,15 @@ using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Macro-loop state for "The Aether Cycle": which phase the run is in, the cycle number, and the server
|
||||
/// tick the current (timed) phase ends. Server-authoritative, maintained by CyclePhaseSystem. Currently a
|
||||
/// server-side singleton; the [GhostField]s below are inert until it is moved onto the runtime-spawned
|
||||
/// CycleDirector ghost (when the client HUD is wired), at which point the same struct replicates unchanged.
|
||||
/// The Defend phase is NOT timed — it ends when the base-defense wave is cleared — so PhaseEndTick is only
|
||||
/// meaningful in Expedition/Build (0 during Defend).
|
||||
/// </summary>
|
||||
public struct CycleState : IComponentData
|
||||
{
|
||||
/// <summary>Current phase (see <see cref="CyclePhase"/>).</summary>
|
||||
[GhostField] public byte Phase;
|
||||
// NOTE (LANTERN purge): CycleState/CyclePhase/CycleRuntime (the Calm↔Siege macro-loop) are DELETED.
|
||||
// ExpeditionObjective below is the surviving replicated room-objective readout (live consumers:
|
||||
// RoomEnemyDirectorSystem writes it; RunDirectorSystem/HudSystem read it).
|
||||
|
||||
/// <summary>1-based cycle counter (increments when a new Expedition begins).</summary>
|
||||
[GhostField] public int CycleNumber;
|
||||
|
||||
/// <summary>Server tick the current timed phase ends (Expedition/Build only; 0 in Defend).</summary>
|
||||
[GhostField] public uint PhaseEndTick;
|
||||
|
||||
/// <summary>Live Husk wave number during Defend, synced from the server-only WaveState by CyclePhaseSystem so the replicated-state-only HUD can show it (holds the last wave number outside Defend; the HUD gates the display to the Defend phase).</summary>
|
||||
[GhostField] public int WaveNumber;
|
||||
}
|
||||
|
||||
/// <summary>Phase constants for <see cref="CycleState.Phase"/> — the GLOBAL shared posture (a byte, not an enum, for trivial Burst/serialization). Being out on an expedition is per-player presence (server-only RegionTag), NOT a global phase.</summary>
|
||||
public static class CyclePhase
|
||||
{
|
||||
// Re-meaned IN PLACE — the byte VALUES are unchanged from the old Expedition/Defend/Build, so the
|
||||
// [GhostField] serializer layout is identical (the const re-mean alone forces no re-bake). slot 0 (was
|
||||
// Expedition) -> Calm; slot 1 (was Defend) -> Siege; slot 2 (was Build) -> retired.
|
||||
|
||||
/// <summary>The persistent, unhurried home base — the DEFAULT posture. No countdown; build/prep at your pace.</summary>
|
||||
public const byte Calm = 0;
|
||||
|
||||
/// <summary>The base is under assault by a Husk wave (event-triggered; ends when the wave is cleared).</summary>
|
||||
public const byte Siege = 1;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-only bookkeeping for the run-state machine that must NOT replicate (kept separate from the
|
||||
/// replicated <see cref="CycleState"/>). Records the wave number captured when the current Siege began plus
|
||||
/// the procedural-expedition-field session epoch (bumped when the expedition region goes empty->occupied so
|
||||
/// the field reseeds per sortie).
|
||||
/// </summary>
|
||||
public struct CycleRuntime : IComponentData
|
||||
{
|
||||
/// <summary>WaveState.WaveNumber captured the moment the current Siege started (DefendCleared tests > this).</summary>
|
||||
public int DefendStartWave;
|
||||
|
||||
/// <summary>Monotonic expedition-field session counter; bumped on the expedition region's empty->occupied edge so each sortie reseeds. RNG seed (never tick math; never 0, via max(1, ...)).</summary>
|
||||
public int ExpeditionEpoch;
|
||||
|
||||
/// <summary>The <see cref="ExpeditionEpoch"/> the field was last seeded for (compared by int equality).</summary>
|
||||
public int LastSpawnedEpoch;
|
||||
|
||||
/// <summary>Previous-tick expedition occupancy (1 = at least one player out), for the empty<->occupied edge.</summary>
|
||||
public byte PrevExpeditionOccupied;
|
||||
|
||||
/// <summary>The <see cref="ExpeditionEpoch"/> a zone-clear Ore reward was last banked for — gates the once-per-epoch reward so two same-tick co-op returners pay once and gate re-entry can't farm (int equality, never tick math).</summary>
|
||||
public int LastRewardedEpoch;
|
||||
|
||||
/// <summary>1 once the current epoch's expedition wave has FULLY spawned and been cleared to zero live zone enemies; reset to 0 on the empty->occupied epoch bump. The reward fires only on a REAL clear.</summary>
|
||||
public byte ClearedThisEpoch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DR-042 C7b — a SMALL replicated summary of the current expedition objective so the client HUD can show an
|
||||
/// "enemies remaining / cleared — return to claim" readout. Rides the GLOBAL UNTAGGED CycleDirector ghost
|
||||
/// (alongside <see cref="CycleState"/> / GoalProgress) so GhostRelevancy.SetIsIrrelevant never hides it
|
||||
/// "enemies remaining / cleared — return to claim" readout. Rides the GLOBAL UNTAGGED director ghost so
|
||||
/// GhostRelevancy.SetIsIrrelevant never hides it
|
||||
/// cross-region — a base teammate can't see the expedition's own (region-tagged, relevancy-hidden) enemy
|
||||
/// ghosts. SOLE writer: ZoneEnemyDirectorSystem (server, plain group), written ABOVE its early-returns
|
||||
/// (snapshot-above-early-return) so the readout never freezes stale. byte/short, never enum (writer is [BurstCompile]).
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Long-arc progress toward the goal ("reach THEM"). Lives on the GLOBAL CycleDirector ghost (relevant in
|
||||
/// every region, alongside CycleState + the resource ledger), so it is visible to all players regardless
|
||||
/// of region. Sole PRODUCTION writer (DR-042): <c>ExpeditionGateSystem</c> increments <see cref="Charge"/> by
|
||||
/// one per cleared EXPEDITION (on the player's return). <c>GoalReachedSystem</c> only READS the Charge==Target
|
||||
/// edge to arm the climactic final siege. (<c>DebugCommandReceiveSystem</c> is a manual dev-op writer.) The HUD
|
||||
/// observes it for a progress bar.
|
||||
/// </summary>
|
||||
public struct GoalProgress : IComponentData
|
||||
{
|
||||
/// <summary>Accumulated progress.</summary>
|
||||
[GhostField] public int Charge;
|
||||
|
||||
/// <summary>Charge required to reach the goal.</summary>
|
||||
[GhostField] public int Target;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1f60b3396850074ca0e44b831b5980c
|
||||
@@ -1,61 +0,0 @@
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// END-2 — server-only marker of which run-phase the macro loop is in. Lives on the GLOBAL CycleDirector
|
||||
/// entity beside <see cref="CycleState"/>/<see cref="CycleRuntime"/>/<see cref="ThreatState"/>; NOT replicated
|
||||
/// (the client never needs to distinguish "the final siege is armed" — the larger wave + telegraph already read
|
||||
/// as escalation; the client shows the terminal banner from the replicated <see cref="RunOutcome"/> instead).
|
||||
/// SINGLE writer: <c>GoalReachedSystem</c> flips <see cref="RunPhaseId.Normal"/> ->
|
||||
/// <see cref="RunPhaseId.FinalDefense"/> exactly once when <see cref="GoalProgress.Charge"/> reaches Target.
|
||||
/// Added at spawn by <c>CycleDirectorSpawnSystem</c> (like CycleRuntime/ThreatState), so it is server-world-only
|
||||
/// and never on the ghost serializer (no re-hash). A <c>byte</c> (never an enum) so a Bursted reader can't trip
|
||||
/// the cross-assembly-enum Burst ICE.
|
||||
/// </summary>
|
||||
public struct RunPhase : IComponentData
|
||||
{
|
||||
public byte Value;
|
||||
}
|
||||
|
||||
/// <summary>Phase constants for <see cref="RunPhase.Value"/> (bytes — never an enum on a Bursted path).</summary>
|
||||
public static class RunPhaseId
|
||||
{
|
||||
/// <summary>Normal play: scheduled / post-expedition sieges arm; the goal meter climbs +1 per survived siege.</summary>
|
||||
public const byte Normal = 0;
|
||||
|
||||
/// <summary>The goal cap was reached: the larger FINAL siege is armed/running. No further sieges arm.</summary>
|
||||
public const byte FinalDefense = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// END-2 — the terminal run outcome: the LATCHING win/lose state, REPLICATED so the client HUD shows the
|
||||
/// victory/loss banner by simply observing it (no fragile client-side reconstruction). Rides the GLOBAL
|
||||
/// untagged CycleDirector ghost (relevant to every connection in every region — it must NEVER be region-tagged;
|
||||
/// the shared-global-state rule), one <c>[GhostField] byte</c> alongside <see cref="CoreIntegrity"/>/
|
||||
/// <see cref="GoalProgress"/>. SINGLE writer: <c>CyclePhaseSystem</c> latches <see cref="RunOutcomeId.Victory"/>
|
||||
/// (final siege cleared) or <see cref="RunOutcomeId.Loss"/> (Core breached during the final siege). Once it is
|
||||
/// non-<see cref="RunOutcomeId.InProgress"/> the run HALTS (GoalReachedSystem + ThreatDirectorSystem stop arming;
|
||||
/// CoreRestoreSystem stops regen). Baked onto the prefab so it is part of the ghost (adding this <c>[GhostField]</c>
|
||||
/// re-hashes the CycleDirector ghost -> one re-bake); born-correct at spawn (InProgress for a New Game, or the
|
||||
/// persisted value on Continue — SaveData v5).
|
||||
/// </summary>
|
||||
public struct RunOutcome : IComponentData
|
||||
{
|
||||
[GhostField] public byte Value;
|
||||
}
|
||||
|
||||
/// <summary>Outcome constants for <see cref="RunOutcome.Value"/> (bytes — never an enum on a Bursted/serialized path).</summary>
|
||||
public static class RunOutcomeId
|
||||
{
|
||||
/// <summary>The run is live (no terminal result yet).</summary>
|
||||
public const byte InProgress = 0;
|
||||
|
||||
/// <summary>The final siege was survived — the Engine holds. Terminal; the run halts.</summary>
|
||||
public const byte Victory = 1;
|
||||
|
||||
/// <summary>The Core was breached during the final siege — overrun. Terminal; the run halts.</summary>
|
||||
public const byte Loss = 2;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ce481dc9a135834fa6d59882895b0f5
|
||||
@@ -1,88 +0,0 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Baked, server-only tuning for the composite ThreatDirector (the data-driven base-attack scheduler). Lives
|
||||
/// on the global CycleDirector entity but is NOT a [GhostField] — the server alone decides when a siege
|
||||
/// fires; clients learn about it only through the replicated <see cref="CycleState.Phase"/> flip to Siege.
|
||||
/// Flat scalar fields (never an enum/array — dodges the MCP authoring-drop gotcha + stays Burst-trivial). This
|
||||
/// slice wires only the POST-EXPEDITION source; the Heat/Schedule fields are baked-but-inert so those sources
|
||||
/// drop in later additively with NO re-bake (server-only layout, not a ghost serializer change).
|
||||
/// </summary>
|
||||
public struct ThreatConfig : IComponentData
|
||||
{
|
||||
// ---- Post-expedition retaliation (the only source wired this slice) ----
|
||||
|
||||
/// <summary>1 = a completed expedition (a player returning to base) can draw a retaliation siege.</summary>
|
||||
public byte PostExpeditionEnabled;
|
||||
|
||||
/// <summary>Telegraph/arming delay (server ticks) between the trigger and the siege actually spawning.</summary>
|
||||
public uint PostExpeditionDelayTicks;
|
||||
|
||||
/// <summary>Siege size floor (Husk count) for a post-expedition retaliation.</summary>
|
||||
public int SizeBase;
|
||||
|
||||
/// <summary>Extra Husks per unit of resources hauled back this run (0 = a flat <see cref="SizeBase"/> siege).</summary>
|
||||
public int SizePerExpeditionResource;
|
||||
|
||||
/// <summary>How a pending siege starts (see <see cref="ThreatStartCondition"/>).</summary>
|
||||
public byte StartCondition;
|
||||
|
||||
/// <summary>Max server ticks a Siege may run before it auto-collapses (remaining Husks culled) so an unattended/empty-base siege can never soft-lock. 0 = no cap.</summary>
|
||||
public uint SiegeTimeoutTicks;
|
||||
|
||||
// ---- Reserved, present-but-inert this slice (additive later, no re-bake) ----
|
||||
|
||||
public byte HeatEnabled;
|
||||
public float HeatPerTickAtBase;
|
||||
public float HeatPerHarvest;
|
||||
public float HeatThreshold;
|
||||
public byte ScheduleEnabled;
|
||||
public uint ScheduleIntervalTicks;
|
||||
/// <summary>Extra Husks per surviving wave for a SCHEDULED base siege (size = SizeBase + this*WaveNumber). 0 = flat SizeBase.</summary>
|
||||
public int ScheduleSizePerWave;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Start-condition constants for <see cref="ThreatConfig.StartCondition"/> (bytes — never an enum, never in an RPC).</summary>
|
||||
public static class ThreatStartCondition
|
||||
{
|
||||
/// <summary>DEFAULT: arm via the telegraph countdown (<see cref="ThreatState.ArmTick"/>) then fire — even at an empty base.</summary>
|
||||
public const byte Immediate = 0;
|
||||
|
||||
/// <summary>Hold the pending siege until ≥1 player is in the base region OR the arm tick + a grace window elapses (bounded — never a soft-lock).</summary>
|
||||
public const byte RequirePlayerAtBase = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-only runtime state of the ThreatDirector, on the global CycleDirector entity beside
|
||||
/// <see cref="CycleRuntime"/>. NOT replicated. <see cref="PendingSiegeSize"/> is the single documented entry
|
||||
/// point: any source (post-expedition, dev tools, later Heat/Schedule) sets it; <c>CyclePhaseSystem</c>
|
||||
/// consumes it on the Calm→Siege edge and zeroes it. All stored ticks are wrap-safe (TickUtil.NonZero +
|
||||
/// NetworkTick compares), never raw uint.
|
||||
/// </summary>
|
||||
public struct ThreatState : IComponentData
|
||||
{
|
||||
/// <summary>Husk count of the armed siege; 0 = none pending. Consumed (zeroed) by CyclePhaseSystem at Siege entry.</summary>
|
||||
public int PendingSiegeSize;
|
||||
|
||||
/// <summary>Server tick the pending siege fires (telegraph). 0 = fire as soon as seen. Routed through TickUtil.NonZero.</summary>
|
||||
public uint ArmTick;
|
||||
|
||||
/// <summary>Server tick the current Siege began (0 = not under siege). The bounded-resolution timeout measures from here (TickUtil.NonZero) so an unattended/empty-base siege can never soft-lock.</summary>
|
||||
public uint SiegeStartTick;
|
||||
|
||||
/// <summary>Count of expeditions completed (a player returned to base). Drives the post-expedition source + stats.</summary>
|
||||
public int ExpeditionsCompleted;
|
||||
|
||||
/// <summary>Return events the gate has signalled but the director has not yet consumed (the gate teleports the player out of its radius, so one increment per return — natural de-dup).</summary>
|
||||
public int PendingReturns;
|
||||
|
||||
/// <summary>Accumulated heat (inert this slice; the Heat source reads/writes it later).</summary>
|
||||
public float Heat;
|
||||
|
||||
/// <summary>Next scheduled-siege tick (inert this slice; the Schedule source uses it later). TickUtil.NonZero when used.</summary>
|
||||
public uint NextScheduledTick;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e66b1e7c715ceb418459c9323853271
|
||||
Reference in New Issue
Block a user