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 Harpooner reel driver. For every Husk carrying an active (stamped by
/// on a Reel-flagged HookPull hit), it re-aims the Husk's
/// toward the CASTER's CURRENT position every tick — so the pull HOMES (unlike
/// the frozen-Dir pull flag). then DRIVES that re-aimed knockback, staying the
/// SOLE writer of the Husk's Position; this system writes only KnockbackState.Dir/Speed/UntilTick (never a
/// position). Runs in the plain AFTER the predicted group (so THIS tick's
/// stamp is visible) and BEFORE . Releases (clears both states) when the target is
/// within release range, the leash () elapses, or the caster is gone.
/// Bosses never reel (they don't carry KnockbackState/ReelState — knockback-immune, A4). Ticks via
/// ; compared with only.
///
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateAfter(typeof(PredictedSimulationSystemGroup))]
[UpdateBefore(typeof(EnemyAISystem))]
public partial struct ReelSystem : ISystem
{
const float k_ReleaseDistance = 1.6f; // stop reeling once the target is this close to the caster (world units)
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate();
state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly()));
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var serverTick = SystemAPI.GetSingleton().ServerTick;
if (!serverTick.IsValid) return;
uint now = serverTick.TickIndexForValidTick;
// Snapshot living players by NetworkId (<=4 in co-op; linear lookup is cheaper than a hashmap).
var casterIds = new NativeList(Allocator.Temp);
var casterPos = new NativeList(Allocator.Temp);
foreach (var (owner, lt, hp) in
SystemAPI.Query, RefRO, RefRO>().WithAll())
{
if (hp.ValueRO.Current <= 0f) continue;
casterIds.Add(owner.ValueRO.NetworkId);
casterPos.Add(lt.ValueRO.Position);
}
float releaseSq = k_ReleaseDistance * k_ReleaseDistance;
foreach (var (reel, knockback, lt) in
SystemAPI.Query, RefRW, RefRO>()
.WithAll().WithNone())
{
uint expire = reel.ValueRO.ExpireTick;
bool active = expire != 0u && new NetworkTick(expire).IsNewerThan(serverTick);
if (!active)
{
// Leash elapsed (or never reeling): release so EnemyAISystem resumes seek next tick.
if (expire != 0u) { reel.ValueRW.ExpireTick = 0u; knockback.ValueRW.UntilTick = 0u; }
continue;
}
int idx = -1;
for (int i = 0; i < casterIds.Length; i++)
if (casterIds[i] == reel.ValueRO.CasterNetworkId) { idx = i; break; }
if (idx < 0) { reel.ValueRW.ExpireTick = 0u; knockback.ValueRW.UntilTick = 0u; continue; } // caster gone
float3 pos = lt.ValueRO.Position;
float2 to = casterPos[idx].xz - pos.xz;
if (math.lengthsq(to) <= releaseSq)
{
// Arrived at the caster: release (don't jitter on top of the player).
reel.ValueRW.ExpireTick = 0u;
knockback.ValueRW.UntilTick = 0u;
continue;
}
// Home: re-aim the knockback toward the caster's LIVE position for exactly this tick; EnemyAISystem
// applies it. Re-stamped every tick while reeling so it tracks the caster as they move.
float2 dir = math.normalize(to);
knockback.ValueRW.Dir = dir;
knockback.ValueRW.Speed = reel.ValueRO.Speed;
knockback.ValueRW.UntilTick = TickUtil.NonZero(now + 1u);
}
casterIds.Dispose();
casterPos.Dispose();
}
}
}