using ProjectM.Simulation; using Unity.Burst; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; namespace ProjectM.Server { /// /// END-2 — arms the FINAL siege when the long-arc goal meter fills. Server-only, plain /// , [UpdateAfter(CyclePhaseSystem)] so it reads /// AFTER the survived-siege increment that may have just reached Target. /// On the Charge >= Target rising edge — guarded by + /// so it fires EXACTLY once — it: /// /// arms a bigger siege through the existing single entry point : /// the would-be-next normal siege size (SizeBase + ScheduleSizePerWave*wave) times the live /// (floored at 1 so the final siege is never smaller), telegraphed /// via (wrap-safe ); /// flips to . /// /// It NEVER writes .Phase / WaveState (CyclePhaseSystem stays the sole writer) nor /// .Charge (CyclePhaseSystem clamps it at the increment site) — it only READS the edge. /// CyclePhaseSystem then consumes the next tick exactly like any other /// armed siege; ThreatDirectorSystem stops arming once leaves Normal, so no normal /// siege can stomp the final one. Plain server group => one run per tick, no rollback/predicted exposure. /// Bytes, never enums (Burst-safe). /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(CyclePhaseSystem))] public partial struct GoalReachedSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); state.RequireForUpdate(); state.RequireForUpdate(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var serverTick = SystemAPI.GetSingleton().ServerTick; if (!serverTick.IsValid) return; uint now = serverTick.TickIndexForValidTick; var cycleEntity = SystemAPI.GetSingletonEntity(); // Exactly-once guards: a decided run, or one already in the final siege, arms nothing. if (SystemAPI.HasComponent(cycleEntity) && SystemAPI.GetComponent(cycleEntity).Value != RunOutcomeId.InProgress) return; var runPhase = SystemAPI.GetComponent(cycleEntity); if (runPhase.Value != RunPhaseId.Normal) return; // Goal cap reached? (Charge is clamped to Target at the CyclePhaseSystem increment site.) if (!SystemAPI.HasComponent(cycleEntity)) return; var goal = SystemAPI.GetComponent(cycleEntity); if (goal.Target <= 0 || goal.Charge < goal.Target) return; if (!SystemAPI.HasComponent(cycleEntity) || !SystemAPI.HasComponent(cycleEntity)) return; var threat = SystemAPI.GetComponent(cycleEntity); var config = SystemAPI.GetComponent(cycleEntity); int wave = SystemAPI.TryGetSingleton(out var ws) ? ws.WaveNumber : 0; float mult = math.max(1f, SystemAPI.TryGetSingleton(out var tcfg) ? tcfg.FinalSiegeMultiplier : TuningConfig.Defaults().FinalSiegeMultiplier); int normalSize = config.SizeBase + config.ScheduleSizePerWave * wave; int finalSize = math.max(1, (int)(normalSize * mult)); // Arm the final siege (overwrites any pending normal siege — the final supersedes; at the goal-reach tick // PendingSiegeSize is 0 anyway, the just-cleared siege having consumed it). CyclePhaseSystem consumes it. threat.PendingSiegeSize = finalSize; threat.ArmTick = TickUtil.NonZero(now + config.PostExpeditionDelayTicks); SystemAPI.SetComponent(cycleEntity, threat); runPhase.Value = RunPhaseId.FinalDefense; SystemAPI.SetComponent(cycleEntity, runPhase); } } }