using ProjectM.Simulation; using Unity.Burst; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; namespace ProjectM.Server { /// /// Server-authoritative macro-loop director for the PLAYER-DRIVEN loop. The base sits in Calm /// (persistent, unhurried — build/prep at your pace, no countdown) until the arms a /// siege, then flips to Siege (the base-defense wave) and back to Calm when the wave is cleared. /// There is no global "Expedition" phase — being out on an expedition is per-player presence (server-only /// ), read client-side by the HUD, so one global byte never has to represent /// "player A out / player B home." Maintains the replicated singleton and gates /// (waves spawn only during Siege). Runs in the plain server SimulationSystemGroup /// before WaveSystem. All timing is wrap-safe NetworkTick math ( /// + ), never raw uint compares. Lives on the /// runtime-spawned CycleDirector ghost. Supersedes the forced timed Expedition→Defend→Build cycle. /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateBefore(typeof(WaveSystem))] public partial struct CyclePhaseSystem : ISystem { EntityQuery m_AliveHusks; [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); state.RequireForUpdate(); m_AliveHusks = state.GetEntityQuery(ComponentType.ReadOnly()); } [BurstCompile] public void OnUpdate(ref SystemState state) { var serverTick = SystemAPI.GetSingleton().ServerTick; if (!serverTick.IsValid) return; uint now = serverTick.TickIndexForValidTick; var cycleEntity = SystemAPI.GetSingletonEntity(); var cycle = SystemAPI.GetComponent(cycleEntity); var runtime = SystemAPI.GetComponent(cycleEntity); if (cycle.Phase == CyclePhase.Calm) { // Default calm: no pending siege => no countdown. cycle.PhaseEndTick = 0; if (SystemAPI.HasComponent(cycleEntity)) { var threat = SystemAPI.GetComponent(cycleEntity); if (threat.PendingSiegeSize > 0) { // Telegraph: mirror the arm tick into the replicated PhaseEndTick so the HUD can show an // "incursion in Ns" countdown (reuses the existing HUD countdown path) while it arms. cycle.PhaseEndTick = threat.ArmTick; bool armed = threat.ArmTick == 0 || !new NetworkTick(threat.ArmTick).IsNewerThan(serverTick); if (armed && SystemAPI.TryGetSingletonEntity(out var waveEntity)) { // ---- Calm -> Siege: seed WaveSystem's own Spawning entry atomically. Writing // Phase=Spawning bypasses its Lull escalation recompute (WaveSystem only recomputes // RemainingToSpawn while Phase==Lull), so the siege spawns EXACTLY the director-chosen // size and WaveSystem stays the sole WaveState writer thereafter. ---- var w = SystemAPI.GetComponent(waveEntity); runtime.DefendStartWave = w.WaveNumber; // capture BEFORE the bump (DefendCleared tests > this) w.WaveNumber += 1; w.Phase = WavePhase.Spawning; w.RemainingToSpawn = math.max(1, threat.PendingSiegeSize); w.NextActionTick = TickUtil.NonZero(now); // spawn the first Husk this tick SystemAPI.SetComponent(waveEntity, w); cycle.Phase = CyclePhase.Siege; cycle.PhaseEndTick = 0; // Siege is wave-driven, not timed. threat.PendingSiegeSize = 0; // consume once threat.ArmTick = 0; SystemAPI.SetComponent(cycleEntity, threat); } } } } else if (cycle.Phase == CyclePhase.Siege) { if (DefendCleared(ref state, runtime.DefendStartWave)) { cycle.Phase = CyclePhase.Calm; cycle.PhaseEndTick = 0; // Long-arc goal: +1 per siege survived (single writer; was +1 per completed timed cycle). if (SystemAPI.HasComponent(cycleEntity)) { var goal = SystemAPI.GetComponent(cycleEntity); goal.Charge += 1; SystemAPI.SetComponent(cycleEntity, goal); } } } // Surface the live wave number on the replicated CycleState for the HUD (single writer). if (SystemAPI.TryGetSingleton(out var waveSync)) cycle.WaveNumber = waveSync.WaveNumber; SystemAPI.SetComponent(cycleEntity, cycle); SystemAPI.SetComponent(cycleEntity, runtime); } // The Siege wave has run for this phase (WaveNumber advanced past the captured start), is fully spawned, // and no Husks remain alive. bool DefendCleared(ref SystemState state, int defendStartWave) { if (!SystemAPI.TryGetSingleton(out var wave)) return false; return wave.WaveNumber > defendStartWave && wave.RemainingToSpawn == 0 && m_AliveHusks.CalculateEntityCount() == 0; } } }