using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;
namespace ProjectM.Server
{
///
/// MC-2 — resolves hostile Spitter projectiles against PLAYERS + STRUCTURES (never other enemies — only
/// PlayerTag / PlacedStructure are snapshotted, so a spit can't friendly-fire the Husks), server-only in the
/// plain after (post-move position).
/// SWEPT planar hit-test (the DR-018 anti-tunnelling discipline): the travel segment is rebuilt from the STORED
/// (cur - Direction*LastStep), NEVER a fresh delta. REGION-FILTERED: a
/// target whose .Region != the spit's Region is skipped — relevancy hides cross-region
/// ghosts from CLIENTS, but the server world holds base + expedition players 1000u apart, so server damage needs
/// its own guard (the missing-filter blocker the design review caught). On a hit it appends
/// DamageEvent{SourceNetworkId=-1, SourceTick=now} (drained the FOLLOWING tick by the predicted
/// HealthApplyDamageSystem — appending from the predicted loop would double-apply on rollback; SourceTick
/// makes the dash i-frame negation correct across the 1-tick gap, so dash-through-spit works for free) and
/// destroys the spit at-most-once; a spit past its Range expires.
///
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateAfter(typeof(EnemyProjectileMoveSystem))]
public partial struct EnemyProjectileDamageSystem : ISystem
{
/// Extra forgiveness for the spit's own size, added to a target's hit radius.
const float k_ProjectileRadius = 0.2f;
/// Hit radius used for structures, which (by design) bake no HitRadius (so player shots never hit them).
const float k_StructureRadius = 1.0f;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate();
state.RequireForUpdate();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
uint now = SystemAPI.GetSingleton().ServerTick.TickIndexForValidTick;
var ecb = new EntityCommandBuffer(Allocator.Temp);
// Snapshot valid targets once (stable query order). PLAYERS carry HitRadius (PlayerAuthoring);
// STRUCTURES deliberately do NOT (so player projectiles never friendly-fire the base) -> a constant.
var targetEntities = new NativeList(Allocator.Temp);
var targetPositions = new NativeList(Allocator.Temp);
var targetRadii = new NativeList(Allocator.Temp);
var targetRegions = new NativeList(Allocator.Temp);
foreach (var (xform, hitRadius, health, region, e) in
SystemAPI.Query, RefRO, RefRO, RefRO>()
.WithAll().WithEntityAccess())
{
if (health.ValueRO.Current <= 0f) continue; // don't hit a corpse
targetEntities.Add(e);
targetPositions.Add(xform.ValueRO.Position);
targetRadii.Add(hitRadius.ValueRO.Value);
targetRegions.Add(region.ValueRO.Region);
}
foreach (var (xform, health, region, e) in
SystemAPI.Query, RefRO, RefRO>()
.WithAll().WithEntityAccess())
{
if (health.ValueRO.Current <= 0f) continue; // skip a structure pending destroy this tick
targetEntities.Add(e);
targetPositions.Add(xform.ValueRO.Position);
targetRadii.Add(k_StructureRadius);
targetRegions.Add(region.ValueRO.Region);
}
var destroyed = new NativeHashSet(16, Allocator.Temp);
foreach (var (xform, proj, projEntity) in
SystemAPI.Query, RefRO>().WithEntityAccess())
{
float3 cur = xform.ValueRO.Position;
float2 segEnd = new float2(cur.x, cur.z);
float2 dir = proj.ValueRO.Direction;
float2 segStart = segEnd - dir * proj.ValueRO.LastStep; // stored move-step, never a fresh dt
float2 seg = segEnd - segStart;
float segLenSq = math.lengthsq(seg);
byte projRegion = proj.ValueRO.Region;
int bestIdx = -1;
float bestT = float.MaxValue;
for (int i = 0; i < targetEntities.Length; i++)
{
if (targetRegions[i] != projRegion) continue; // server-side damage region guard
float2 tp = new float2(targetPositions[i].x, targetPositions[i].z);
float t = segLenSq > 1e-8f
? math.saturate(math.dot(tp - segStart, seg) / segLenSq)
: 0f;
float2 closest = segStart + t * seg;
float hitDist = targetRadii[i] + k_ProjectileRadius;
if (math.distancesq(tp, closest) <= hitDist * hitDist && t < bestT)
{
bestT = t;
bestIdx = i;
}
}
if (bestIdx >= 0)
{
ecb.AppendToBuffer(targetEntities[bestIdx], new DamageEvent
{
Amount = proj.ValueRO.Damage,
SourceNetworkId = -1, // hostile environment, not a player
SourceTick = TickUtil.NonZero(now),
});
if (destroyed.Add(projEntity))
ecb.DestroyEntity(projEntity);
continue;
}
if (proj.ValueRO.DistanceTravelled >= proj.ValueRO.Range && destroyed.Add(projEntity))
ecb.DestroyEntity(projEntity);
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
destroyed.Dispose();
targetEntities.Dispose();
targetPositions.Dispose();
targetRadii.Dispose();
targetRegions.Dispose();
}
}
}