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;
}
}
}