3995af736c
BossState (server-only) + BossAISystem: sole mover, seek -> telegraphed radial slam (AttackWindup) -> phase-2 speed + swarmer summon. Excluded from the EnemyAISystem Charger pass; knockback-immune at all 3 stamp sites; arena-anchored spawn; scaled Health/HitRadius/AttackRange. Grunt windup now commits in its last ~30% (whiffs out-of-range); Charger stagger roots; grunt speed 4.2->5.2; swarmer telegraph synced. SweptMove + zone-enemy spawn-stamp extracted to shared helpers. Zero new [GhostField]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
49 lines
2.2 KiB
C#
49 lines
2.2 KiB
C#
using ProjectM.Simulation;
|
|
using Unity.Mathematics;
|
|
using Unity.Physics;
|
|
|
|
namespace ProjectM.Server
|
|
{
|
|
/// <summary>
|
|
/// 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 <see cref="SweptMove"/>. Pure given the CollisionWorld;
|
|
/// Burst-safe (non-generic closest-hit SphereCast, per the CLAUDE.md generic-collector hazard).
|
|
/// </summary>
|
|
public static class EnemyMoveUtil
|
|
{
|
|
/// <summary>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).</summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|