using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;
namespace ProjectM.Server
{
///
/// Erupts PERMANENT Blight s (Geyser_Build_Spec, design-review wf_900e9965-8f0): when a
/// geyser's replicated elapses, radius-damage LIVING enemies AND players
/// (friendly fire — the bait/lure mechanic, exactly like the exploding-barrel hazard), then RESCHEDULE
/// NextEruptTick one period ahead of NOW and leave the geyser alive (it is permanent —
/// teardown is the only removal; NEVER DestroyEntity here). Player + enemy gather mirrors
/// VERBATIM (Health > 0; players also RegionTag == Expedition;
/// = -1 environment convention; SourceTick stamped at NOW, the
/// authoring tick, so dash i-frames negate against it).
///
/// ★ The invalid-tick guard is INVERTED from the barrel (review HIGH H2): an unstamped/invalid NextEruptTick
/// (0, the "ready" sentinel) is NOT-READY and is SKIPPED — it must NEVER erupt. Copying the barrel's
/// "invalid -> detonate" fall-through onto a born-0 [GhostField] would erupt every tick and wipe the party.
/// The reschedule is = now + period (NEVER += — an anchored accumulator catch-up-storms after any
/// multi-period skip; every tick-sentinel in the project is = now + delay). Plain server
/// , NO ordering edges; presence-gated on .
///
///
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial struct GeyserEruptSystem : 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;
uint stamp = TickUtil.NonZero(serverTick.TickIndexForValidTick); // SourceTick = NOW (authoring tick)
uint reschedule = TickUtil.NonZero(serverTick.TickIndexForValidTick + Tuning.GeyserPeriodTicks); // = now + period
var erupts = new NativeList(Allocator.Temp);
foreach (var (geyser, lt) in SystemAPI.Query, RefRO>())
{
uint next = geyser.ValueRO.NextEruptTick;
var until = new NetworkTick(next);
// INVERTED guard (review H2): an unstamped/invalid NextEruptTick (0) is NOT-READY and must NEVER
// erupt. LAZY-STAMP it born-correct (now + period) instead — self-heals the rare case where the
// seed-time stamp missed (NetworkTime invalid at seed), so a 0 can neither storm nor stay inert.
if (next == 0u || !until.IsValid) { geyser.ValueRW.NextEruptTick = reschedule; continue; }
if (until.IsNewerThan(serverTick)) continue; // still counting down
erupts.Add(lt.ValueRO.Position);
geyser.ValueRW.NextEruptTick = reschedule; // reschedule in place; permanent, never destroyed
}
if (erupts.Length > 0)
{
float radiusSq = Tuning.GeyserEruptRadius * Tuning.GeyserEruptRadius;
var ecb = new EntityCommandBuffer(Allocator.Temp);
// Players: living + expedition only (the HazardExplosionSystem gather verbatim).
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 < erupts.Length; i++)
if (math.distancesq(plt.ValueRO.Position.xz, erupts[i].xz) <= radiusSq)
ecb.AppendToBuffer(player, new DamageEvent
{
Amount = Tuning.GeyserEruptDamage,
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 < erupts.Length; i++)
if (math.distancesq(elt.ValueRO.Position.xz, erupts[i].xz) <= radiusSq)
ecb.AppendToBuffer(enemy, new DamageEvent
{
Amount = Tuning.GeyserEruptDamage,
SourceNetworkId = -1,
SourceTick = stamp,
});
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
erupts.Dispose();
}
}
}