101 lines
4.2 KiB
C#
101 lines
4.2 KiB
C#
using ProjectM.Simulation;
|
|
using Unity.Burst;
|
|
using Unity.Entities;
|
|
using Unity.NetCode;
|
|
|
|
namespace ProjectM.Server
|
|
{
|
|
/// <summary>
|
|
/// Server-authoritative macro-loop director for "The Aether Cycle": Expedition (timed) -> Defend
|
|
/// (wave-driven) -> Build (timed) -> next cycle. Maintains the <see cref="CycleState"/> singleton and gates
|
|
/// <see cref="WaveSystem"/> so the base-defense wave only spawns during Defend. Runs in the plain server
|
|
/// SimulationSystemGroup (NOT prediction) before <see cref="WaveSystem"/>. All timing is wrap-safe
|
|
/// NetworkTick math (<see cref="TickUtil.NonZero"/> + <see cref="Unity.NetCode.NetworkTick.IsNewerThan"/>),
|
|
/// never raw uint compares. The CycleState/CycleRuntime live on the runtime-spawned CycleDirector ghost.
|
|
/// </summary>
|
|
[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<NetworkTime>();
|
|
state.RequireForUpdate<CycleState>();
|
|
m_AliveHusks = state.GetEntityQuery(ComponentType.ReadOnly<EnemyTag>());
|
|
}
|
|
|
|
[BurstCompile]
|
|
public void OnUpdate(ref SystemState state)
|
|
{
|
|
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
|
if (!serverTick.IsValid)
|
|
return;
|
|
uint now = serverTick.TickIndexForValidTick;
|
|
|
|
var cycleEntity = SystemAPI.GetSingletonEntity<CycleState>();
|
|
|
|
var cycle = SystemAPI.GetComponent<CycleState>(cycleEntity);
|
|
var runtime = SystemAPI.GetComponent<CycleRuntime>(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<WaveState>(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<GoalProgress>(cycleEntity))
|
|
{
|
|
var goal = SystemAPI.GetComponent<GoalProgress>(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<WaveState>(out var wave))
|
|
return false;
|
|
return wave.WaveNumber > defendStartWave
|
|
&& wave.RemainingToSpawn == 0
|
|
&& m_AliveHusks.CalculateEntityCount() == 0;
|
|
}
|
|
}
|
|
}
|