using ProjectM.Simulation; using Unity.Burst; using Unity.Entities; using Unity.NetCode; namespace ProjectM.Server { /// /// Server-authoritative macro-loop director for "The Aether Cycle": Expedition (timed) -> Defend /// (wave-driven) -> Build (timed) -> next cycle. Maintains the singleton and gates /// so the base-defense wave only spawns during Defend. Runs in the plain server /// SimulationSystemGroup (NOT prediction) before . All timing is wrap-safe /// NetworkTick math ( + ), /// never raw uint compares. The CycleState/CycleRuntime live on the runtime-spawned CycleDirector ghost. /// [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); bool timedPhaseDue = cycle.PhaseEndTick != 0 && !new NetworkTick(cycle.PhaseEndTick).IsNewerThan(serverTick); switch (cycle.Phase) { case CyclePhase.Expedition: if (timedPhaseDue) { cycle.Phase = CyclePhase.Defend; cycle.PhaseEndTick = 0; // Defend is wave-driven, not timed. runtime.DefendStartWave = SystemAPI.TryGetSingleton(out var w) ? w.WaveNumber : 0; } break; case CyclePhase.Defend: if (DefendCleared(ref state, runtime.DefendStartWave)) { cycle.Phase = CyclePhase.Build; cycle.PhaseEndTick = TickUtil.NonZero(now + CyclePhase.BuildTicks); } break; case CyclePhase.Build: if (timedPhaseDue) { cycle.Phase = CyclePhase.Expedition; cycle.CycleNumber += 1; cycle.PhaseEndTick = TickUtil.NonZero(now + CyclePhase.ExpeditionTicks); // Long-arc goal: one charge per completed cycle (single writer). if (SystemAPI.HasComponent(cycleEntity)) { var goal = SystemAPI.GetComponent(cycleEntity); goal.Charge += 1; SystemAPI.SetComponent(cycleEntity, goal); } } break; } SystemAPI.SetComponent(cycleEntity, cycle); SystemAPI.SetComponent(cycleEntity, runtime); } // The Defend 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; } } }