using ProjectM.Simulation; using Unity.Mathematics; using Unity.Physics; namespace ProjectM.Server { /// /// Shared server-side collide-and-slide mover for OWNERLESS enemy ghosts (extracted from EnemyAISystem so the /// boss brain reuses ONE copy — a fix to the tunnelling sweep or wall-glance reaches every enemy at once). /// Both EnemyAISystem's three passes and BossAISystem call . Pure given the CollisionWorld; /// Burst-safe (non-generic closest-hit SphereCast, per the CLAUDE.md generic-collector hazard). /// public static class EnemyMoveUtil { /// Collide-and-slide sphere-cast for server-authoritative enemy movement: sweep the intended step /// against the static environment (boundary ring + landmarks + player-built walls) and stop at / glance along /// the first wall hit. Y is held flat (top-down movement plane). public static float3 SweptMove(in PhysicsWorldSingleton physics, float3 from, float3 to, float radius, CollisionFilter filter) { float3 delta = to - from; delta.y = 0f; float dist = math.length(delta); if (dist < 1e-5f) return to; float3 dir = delta / dist; const float skin = 0.05f; var cw = physics.CollisionWorld; if (!cw.SphereCast(from, radius, dir, dist, out var hit, filter)) return to; float allowed = math.max(0f, hit.Fraction * dist - skin); float3 stop = from + dir * allowed; stop.y = from.y; // Slide the unused motion along the wall, then sweep the slide so we don't tunnel a second wall. float3 slide = EnemyAIMath.SlideVelocity(to - stop, hit.SurfaceNormal); float slideDist = math.length(slide); if (slideDist < 1e-5f) return stop; float3 sdir = slide / slideDist; float3 result = cw.SphereCast(stop, radius, sdir, slideDist, out var hit2, filter) ? stop + sdir * math.max(0f, hit2.Fraction * slideDist - skin) : stop + slide; result.y = from.y; return result; } } }