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();
}
}
}