This commit is contained in:
2026-07-07 20:51:18 -07:00
parent d2203c8aa1
commit cc2e7ad95e
20 changed files with 794 additions and 35 deletions
@@ -40,14 +40,18 @@ namespace ProjectM.Simulation
/// Projects a planar movement <paramref name="vel"/> onto a wall plane defined by <paramref name="surfaceNormal"/>
/// (collide-and-slide): removes the component of <paramref name="vel"/> that pushes into the surface so the
/// mover glances along the wall instead of stopping dead. Both inputs are flattened to the XZ plane (top-down).
/// Returns <paramref name="vel"/> unchanged when the normal is degenerate.
/// When the flattened normal is DEGENERATE (a near-vertical / rounded-rock face collapses to ~0 in XZ),
/// sliding is undefined and returning <paramref name="vel"/> unchanged would drive the mover straight back
/// into the wall (the enemy-stuck-on-cover-rock bug) — so it deflects to a horizontal tangent of the motion
/// (a 90 deg turn about Y) to glance AROUND the obstacle instead of freezing against it.
/// </summary>
public static float3 SlideVelocity(float3 vel, float3 surfaceNormal)
{
vel.y = 0f;
surfaceNormal.y = 0f;
float len = math.length(surfaceNormal);
if (len < 1e-6f)
return vel;
return new float3(-vel.z, 0f, vel.x); // degenerate normal -> tangent deflection, not motion-into-wall
float3 n = surfaceNormal / len;
float3 slid = vel - math.dot(vel, n) * n;
slid.y = 0f;
@@ -0,0 +1,28 @@
using Unity.Entities;
using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// Server-only per-Husk navigation / anti-stuck state (NOT replicated — clients never simulate enemies, so this
/// carries no <c>[GhostField]</c> and does not change the ghost hash). Tracks the enemy's XZ position at the end
/// of last tick plus how long it has failed to make progress toward its target, so <c>EnemyAISystem</c> can
/// phase-nudge a genuinely stuck enemy (wedged on a cover rock or a rock-vs-boundary pocket) toward the nearest
/// player — the guaranteed backstop that keeps an expedition room from ever soft-locking on one unreachable Husk.
/// Baked onto every enemy by <c>EnemyAuthoring</c>. Complements the collide-and-slide fixes in
/// <see cref="EnemyAIMath.SlideVelocity"/> / <c>EnemyMoveUtil</c> (which make sticking rare in the first place).
/// </summary>
public struct EnemyNavState : IComponentData
{
/// <summary>Enemy XZ position at the end of last tick (progress baseline; Y unused).</summary>
public float3 LastPos;
/// <summary>Consecutive ticks the enemy wanted to move toward its target but made ~no progress.</summary>
public uint StuckTicks;
/// <summary>While a <c>NetworkTick</c> newer than the current tick is stored here the enemy is phase-nudging
/// toward its target (collide-and-slide bypassed) to escape geometry; <c>0</c> = not nudging. Routed through
/// <c>TickUtil.NonZero</c> so a computed tick never collides with the 0 "inactive" sentinel.</summary>
public uint NudgeUntilTick;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5f3964cc62bbb0042b85e8a6a944dabe