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 player-cast zone driver (Vortex / LightZone — the LANTERN Aoe/zone Sparks). Borrows the /// skeleton — the INVERTED invalid-tick guard (a born-0 /// is lazy-stamped born-correct, never storm-fires) and the = now + period reschedule — but is /// ENEMY-ONLY (NO friendly fire), stamps = the caster's NetworkId /// (kills credit the caster — NOT the -1 environment convention), and has NO region gate (a no-world gym). /// A zone ALSO writes a gentle inward every /// tick (EnemyAISystem drives it, sole Position writer), so runs BEFORE EnemyAISystem. DestroyEntity when /// elapses (server-authoritative ghost despawn). Ticks via /// ; compared with only. /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(PredictedSimulationSystemGroup))] [UpdateBefore(typeof(EnemyAISystem))] public partial struct ZonePulseSystem : ISystem { const uint k_PulsePeriodTicks = 30u; // damage cadence (~0.5s at 60 ticks/sec) const float k_VortexPullSpeed = 4f; // gentle inward drift for a Vortex (world units/sec) const float k_VortexDeadzoneSq = 0.25f; // don't re-aim (or NaN) an enemy already at the zone centre ComponentLookup m_KnockbackLookup; ComponentLookup m_BossLookup; [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); state.RequireForUpdate(); m_KnockbackLookup = state.GetComponentLookup(isReadOnly: false); m_BossLookup = state.GetComponentLookup(isReadOnly: true); } [BurstCompile] public void OnUpdate(ref SystemState state) { var serverTick = SystemAPI.GetSingleton().ServerTick; if (!serverTick.IsValid) return; uint now = serverTick.TickIndexForValidTick; uint stamp = TickUtil.NonZero(now); uint reschedule = TickUtil.NonZero(now + k_PulsePeriodTicks); m_KnockbackLookup.Update(ref state); m_BossLookup.Update(ref state); // Living enemies once this tick (entities + positions; stable query order). var enemyEntities = new NativeList(Allocator.Temp); var enemyPositions = new NativeList(Allocator.Temp); foreach (var (lt, hp, e) in SystemAPI.Query, RefRO>() .WithAll().WithNone().WithEntityAccess()) { if (hp.ValueRO.Current <= 0f) continue; enemyEntities.Add(e); enemyPositions.Add(lt.ValueRO.Position); } var ecb = new EntityCommandBuffer(Allocator.Temp); foreach (var (zone, lt, zoneEntity) in SystemAPI.Query, RefRO>().WithEntityAccess()) { var ze = zone.ValueRO; // Lifetime: despawn the zone ghost when the leash elapses (server-authoritative). if (ze.ExpireTick != 0u && !new NetworkTick(ze.ExpireTick).IsNewerThan(serverTick)) { ecb.DestroyEntity(zoneEntity); continue; } float3 center = lt.ValueRO.Position; float radiusSq = ze.Radius * ze.Radius; bool vortex = (ze.Flags & ZoneEffectFlag.Vortex) != 0; // Vortex pull: gentle inward drift EVERY tick (re-stamped, one tick; EnemyAISystem applies it). if (vortex) { for (int i = 0; i < enemyEntities.Length; i++) { float2 to = center.xz - enemyPositions[i].xz; float d2 = math.lengthsq(to); if (d2 > radiusSq || d2 <= k_VortexDeadzoneSq) continue; var e = enemyEntities[i]; if (!m_KnockbackLookup.HasComponent(e) || m_BossLookup.HasComponent(e)) continue; m_KnockbackLookup[e] = new KnockbackState { Dir = math.normalize(to), Speed = k_VortexPullSpeed, UntilTick = TickUtil.NonZero(now + 1u), }; } } // Damage pulse: INVERTED invalid-tick guard (a born-0 NextTick lazy-stamps born-correct, never storms). uint next = ze.NextTick; if (next == 0u || !new NetworkTick(next).IsValid) { zone.ValueRW.NextTick = reschedule; continue; } if (new NetworkTick(next).IsNewerThan(serverTick)) continue; // still counting down for (int i = 0; i < enemyEntities.Length; i++) { if (math.distancesq(enemyPositions[i].xz, center.xz) > radiusSq) continue; ecb.AppendToBuffer(enemyEntities[i], new DamageEvent { Amount = ze.DamagePerPulse, SourceNetworkId = ze.CasterNetworkId, SourceTick = stamp, }); } zone.ValueRW.NextTick = reschedule; } ecb.Playback(state.EntityManager); ecb.Dispose(); enemyEntities.Dispose(); enemyPositions.Dispose(); } } }