using ProjectM.Simulation; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; using Unity.Transforms; namespace ProjectM.Server { /// /// Server-only turret fire (hitscan) — EnemyAISystem reversed. Snapshots living Husks once (entity, planar /// pos, region); each turret picks the nearest Husk in ITS region within Range and, on a NetworkTick /// cooldown stored in , appends a direct DamageEvent /// (SourceNetworkId=-1) to it. Reuses HealthApplyDamageSystem (already destroys EnemyTag at HP<=0) — no /// projectile, no tunnelling, no friendly-fire. Plain server SimulationSystemGroup /// [UpdateAfter(PredictedSimulationSystemGroup)] (the predicted group is OrderFirst → UpdateBefore is /// ignored); the appended DamageEvent drains next tick (~16ms), consistent with EnemyAISystem. Self-gates: /// Husks only exist during the Defend wave. /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(PredictedSimulationSystemGroup))] public partial struct TurretFireSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); state.RequireForUpdate(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 huskEntities = new NativeList(Allocator.Temp); var huskPos = new NativeList(Allocator.Temp); var huskRegion = new NativeList(Allocator.Temp); foreach (var (xform, health, region, e) in SystemAPI.Query, RefRO, RefRO>() .WithAll().WithEntityAccess()) { if (health.ValueRO.Current <= 0f) continue; huskEntities.Add(e); huskPos.Add(xform.ValueRO.Position.xz); huskRegion.Add(region.ValueRO.Region); } if (huskEntities.Length == 0) { huskEntities.Dispose(); huskPos.Dispose(); huskRegion.Dispose(); return; } var ecb = new EntityCommandBuffer(Allocator.Temp); foreach (var (ps, turret, xform, region) in SystemAPI.Query, RefRO, RefRO, RefRO>()) { uint nextRaw = ps.ValueRO.NextTick; if (nextRaw != 0) { var nextTick = new NetworkTick(nextRaw); if (nextTick.IsValid && nextTick.IsNewerThan(serverTick)) continue; // still cooling down } float2 tp = xform.ValueRO.Position.xz; byte treg = region.ValueRO.Region; float rangeSq = turret.ValueRO.Range * turret.ValueRO.Range; int best = -1; float bestSq = float.MaxValue; for (int i = 0; i < huskEntities.Length; i++) { if (huskRegion[i] != treg) continue; float sq = math.distancesq(huskPos[i], tp); if (sq <= rangeSq && sq < bestSq) { bestSq = sq; best = i; } } if (best >= 0) { ecb.AppendToBuffer(huskEntities[best], new DamageEvent { Amount = turret.ValueRO.Damage, SourceNetworkId = -1, }); uint cd = (uint)math.max(1, turret.ValueRO.CooldownTicks); ps.ValueRW.NextTick = TickUtil.NonZero(now + cd); } } ecb.Playback(state.EntityManager); ecb.Dispose(); huskEntities.Dispose(); huskPos.Dispose(); huskRegion.Dispose(); } } }