using ProjectM.Simulation; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; using Unity.Transforms; namespace ProjectM.Server { /// /// Detonates lit-fuse EXPLOSIVE barrels (Exploding_Barrels_Build_Spec, design-review wf_2cb10454-fdf): /// when a 's ExplodeTick elapses, radius-damage LIVING enemies AND players /// (friendly fire = the bait mechanic), then the single DestroyEntity. Player gather mirrors the /// BossAISystem slam filter VERBATIM (Health > 0 + RegionTag == Expedition — a dead player lying in /// radius gets nothing; a review-confirmed precedent). is stamped at /// DETONATION (the authoring moment — dash i-frames negate against it, honouring the DamageEvent /// authored-tick contract) and = -1 (the environment /// convention: a player id would score+consume Charger whiff-punish windows). Plain server /// , NO ordering edges; presence-gated on BarrelFuse only — NEVER /// lifecycle-gated (a fuse must always drain; it rides the RoomTag'd barrel, so room teardown / the /// Staging sweep clean unexploded barrels for free). An invalid NetworkTime skips the tick; the fuse /// persists. /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] public partial struct HazardExplosionSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); } [BurstCompile] public void OnUpdate(ref SystemState state) { if (!SystemAPI.TryGetSingleton(out var netTime) || !netTime.ServerTick.IsValid) return; var serverTick = netTime.ServerTick; var ecb = new EntityCommandBuffer(Allocator.Temp); var blasts = new NativeList(Allocator.Temp); foreach (var (fuse, lt, barrel) in SystemAPI.Query, RefRO>().WithEntityAccess()) { var until = new NetworkTick(fuse.ValueRO.ExplodeTick); if (until.IsValid && until.IsNewerThan(serverTick)) continue; // still burning blasts.Add(lt.ValueRO.Position); ecb.DestroyEntity(barrel); // the single destroy site for a popped explosive } if (blasts.Length > 0) { float radiusSq = Tuning.BarrelExplodeRadius * Tuning.BarrelExplodeRadius; uint stamp = TickUtil.NonZero(serverTick.TickIndexForValidTick); // Players: the BossAISystem slam gather verbatim — living + expedition only. foreach (var (hp, region, plt, player) in SystemAPI.Query, RefRO, RefRO>() .WithAll().WithEntityAccess()) { if (hp.ValueRO.Current <= 0f || region.ValueRO.Region != RegionId.Expedition) continue; for (int i = 0; i < blasts.Length; i++) if (math.distancesq(plt.ValueRO.Position.xz, blasts[i].xz) <= radiusSq) ecb.AppendToBuffer(player, new DamageEvent { Amount = Tuning.BarrelExplodeDamage, SourceNetworkId = -1, SourceTick = stamp, }); } // Enemies: living only (Health > 0 also excludes Dying corpses, the established convention). foreach (var (hp, elt, enemy) in SystemAPI.Query, RefRO>() .WithAll().WithEntityAccess()) { if (hp.ValueRO.Current <= 0f) continue; for (int i = 0; i < blasts.Length; i++) if (math.distancesq(elt.ValueRO.Position.xz, blasts[i].xz) <= radiusSq) ecb.AppendToBuffer(enemy, new DamageEvent { Amount = Tuning.BarrelExplodeDamage, SourceNetworkId = -1, SourceTick = stamp, }); } } ecb.Playback(state.EntityManager); ecb.Dispose(); blasts.Dispose(); } } }