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
@@ -85,13 +85,7 @@ namespace ProjectM.Server
decoyPositions.Add(dx.ValueRO.Position);
}
// END-1: the Engine Core is a FALLBACK target. When no living player/structure remains, undefended
// Husks march on the base heart (PlotCenter) so the base can be overrun instead of the swarm idling.
bool coreAlive = SystemAPI.HasSingleton<BaseAnchor>()
&& SystemAPI.TryGetSingleton<CoreIntegrity>(out var coreInteg) && coreInteg.Current > 0;
float3 corePos = coreAlive ? BaseGridMath.PlotCenter(SystemAPI.GetSingleton<BaseAnchor>()) : float3.zero;
if (playerEntities.Length == 0 && structureEntities.Length == 0 && decoyEntities.Length == 0 && !coreAlive)
if (playerEntities.Length == 0 && structureEntities.Length == 0 && decoyEntities.Length == 0)
{
playerEntities.Dispose();
playerPositions.Dispose();
@@ -137,7 +131,6 @@ namespace ProjectM.Server
{
float3 pos = xform.ValueRO.Position;
byte huskRegion = region.ValueRO.Region;
bool huskCoreAlive = coreAlive && huskRegion == RegionId.Base;
// Knockback overrides seek/strike for its window — EnemyAISystem stays the SOLE writer of Position.
var kb = knockback.ValueRO;
@@ -178,12 +171,10 @@ namespace ProjectM.Server
}
else
{
if (tgtIdx < 0 && !huskCoreAlive)
continue; // no decoy, no player/structure, and no Core -> nothing to seek
targetEntity = tgtIdx < 0 ? Entity.Null
: (tgtIsStruct ? structureEntities[tgtIdx] : playerEntities[tgtIdx]);
targetPos = tgtIdx < 0 ? corePos
: (tgtIsStruct ? structurePositions[tgtIdx] : playerPositions[tgtIdx]);
if (tgtIdx < 0)
continue; // no decoy, no player/structure -> nothing to seek
targetEntity = tgtIsStruct ? structureEntities[tgtIdx] : playerEntities[tgtIdx];
targetPos = tgtIsStruct ? structurePositions[tgtIdx] : playerPositions[tgtIdx];
}
// Seek: stop just inside strike range so the Husk holds position to attack.
@@ -271,7 +262,6 @@ namespace ProjectM.Server
{
float3 pos = xform.ValueRO.Position;
byte cHuskRegion = region.ValueRO.Region;
bool cHuskCoreAlive = coreAlive && cHuskRegion == RegionId.Base;
// 1. Knockback wins (and cancels any in-flight lunge so Position keeps a single writer).
var kb = knockback.ValueRO;
@@ -296,12 +286,10 @@ namespace ProjectM.Server
// EB-1 fortress aggro: same weighted target selection as the Grunt pass (shared helper).
EnemyAIMath.PickWeightedNearest(pos, playerPositions, playerRegions, structurePositions, structureRegions, cHuskRegion, structAggro, out bool cIsStruct, out int cIdx);
if (cIdx < 0 && !cHuskCoreAlive)
if (cIdx < 0)
continue;
Entity cTargetEntity = cIdx < 0 ? Entity.Null
: (cIsStruct ? structureEntities[cIdx] : playerEntities[cIdx]);
float3 cTargetPos = cIdx < 0 ? corePos
: (cIsStruct ? structurePositions[cIdx] : playerPositions[cIdx]);
Entity cTargetEntity = cIsStruct ? structureEntities[cIdx] : playerEntities[cIdx];
float3 cTargetPos = cIsStruct ? structurePositions[cIdx] : playerPositions[cIdx];
// 2. Lunge active: travel the locked direction; damage on contact, or stagger on a wall-stop whiff.
var lg = lunge.ValueRO;
@@ -414,7 +402,6 @@ namespace ProjectM.Server
{
float3 pos = xform.ValueRO.Position;
byte sRegion = region.ValueRO.Region;
bool sCoreAlive = coreAlive && sRegion == RegionId.Base;
// 1. Knockback overrides everything (sole Position writer preserved).
var kb = knockback.ValueRO;
@@ -434,14 +421,12 @@ namespace ProjectM.Server
knockback.ValueRW.UntilTick = 0;
}
// 2. Target (region-scoped shared helper); Core fallback like the Grunt/Charger passes.
// 2. Target (region-scoped shared helper); no target -> idle.
EnemyAIMath.PickWeightedNearest(pos, playerPositions, playerRegions, structurePositions, structureRegions, sRegion, structAggro, out bool sIsStruct, out int sIdx);
if (sIdx < 0 && !sCoreAlive)
if (sIdx < 0)
continue;
Entity sTargetEntity = sIdx < 0 ? Entity.Null
: (sIsStruct ? structureEntities[sIdx] : playerEntities[sIdx]);
float3 sTargetPos = sIdx < 0 ? corePos
: (sIsStruct ? structurePositions[sIdx] : playerPositions[sIdx]);
Entity sTargetEntity = sIsStruct ? structureEntities[sIdx] : playerEntities[sIdx];
float3 sTargetPos = sIsStruct ? structurePositions[sIdx] : playerPositions[sIdx];
// 3. Range-band movement: advance if too far, retreat if too close, hold in-band. Face the target.
var sp = spitter.ValueRO;
@@ -504,7 +489,7 @@ namespace ProjectM.Server
float sDist = math.length(sToTarget);
bool sInBand = math.abs(sDist - sp.PreferredRange) <= sp.RangeTolerance;
bool sCornered = sDist <= sp.CorneredRange;
if (sReady && (sInBand || sCornered) && (sTargetEntity != Entity.Null || sCoreAlive))
if (sReady && (sInBand || sCornered))
{
uint wTicks = (uint)math.max(1, sp.WindupTicks);
windup.ValueRW.WindUpUntilTick = TickUtil.NonZero(now + wTicks);
@@ -625,9 +610,8 @@ namespace ProjectM.Server
}
EnemyAIMath.PickWeightedNearest(npos, playerPositions, playerRegions, structurePositions, structureRegions, nRegion, structAggro, out bool nIsStruct, out int nIdx);
bool nCoreAlive = coreAlive && nRegion == RegionId.Base;
bool hasTarget = nIdx >= 0 || nCoreAlive;
float3 nTarget = nIdx < 0 ? corePos : (nIsStruct ? structurePositions[nIdx] : playerPositions[nIdx]);
bool hasTarget = nIdx >= 0;
float3 nTarget = nIdx < 0 ? npos : (nIsStruct ? structurePositions[nIdx] : playerPositions[nIdx]);
bool wantsToClose = hasTarget && !committed
&& math.distance(npos.xz, nTarget.xz) > nstats.ValueRO.AttackRange * 1.15f;
@@ -39,9 +39,6 @@ namespace ProjectM.Server
if (!serverTick.IsValid)
return;
uint now = serverTick.TickIndexForValidTick;
// Player-driven loop: the base-defense wave only spawns during a Siege.
if (SystemAPI.TryGetSingleton<CycleState>(out var cycle) && cycle.Phase != CyclePhase.Siege)
return;
var director = SystemAPI.GetSingleton<WaveDirector>();
var directorEntity = SystemAPI.GetSingletonEntity<WaveDirector>();