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