using UnityEngine;
namespace UAI
{
///
/// V4 movement and pathfinding utilities: speed, path-finding, ladder navigation,
/// jump assistance, blockage detection, and leader teleportation.
///
/// Differences from :
///
/// - All magic numbers replaced with constants.
/// - has the large commented-out block removed.
/// - Inline comments cleaned up throughout.
///
///
///
public static class PathingUtils
{
// ── Speed ─────────────────────────────────────────────────────────────────
///
/// Sets the entity's navigator move speed and returns the chosen value.
/// Horde zombies always use aggro speed; the flag
/// overrides to panic speed for any entity type.
///
public static float SetSpeed(Context context, bool panic = false)
{
var speed = context.Self.GetMoveSpeed();
if (context.Self is EntityEnemy enemy && enemy.IsHordeZombie)
speed = enemy.GetMoveSpeedAggro();
if (panic)
speed = context.Self.GetMoveSpeedPanic();
context.Self.navigator.setMoveSpeed(speed);
return speed;
}
///
/// Returns the estimated squared XZ distance between the entity and
/// after ticks
/// of current motion.
///
public static float GetTargetXZDistanceSq(Context context, Vector3 position, float estimatedTicks)
{
position += Vector3.zero * estimatedTicks;
var delta = context.Self.position + context.Self.motion * estimatedTicks - position;
delta.y = 0f;
return delta.sqrMagnitude;
}
// ── FindPath ──────────────────────────────────────────────────────────────
///
/// Configures entity speed (matching leader running state when following) and
/// issues a path-find request toward .
///
public static void FindPath(Context context, Vector3 position, bool panic = false)
{
// When following a leader, match their run/walk pace.
var leader = EntityUtilities.GetLeaderOrOwner(context.Self.entityId) as EntityAlive;
if (leader != null && context.ActionData.CurrentTask is UAITaskFollowSDXV4)
{
var runningTag = FastTags.Parse("running");
panic = leader.CurrentMovementTag.Test_AllSet(runningTag);
}
var speed = SetSpeed(context, panic);
var moveTo = GetMoveToLocation(context, position);
context.Self.FindPath(moveTo, speed, true, null);
}
// ── Move Location ─────────────────────────────────────────────────────────
///
/// Resolves a world-space move destination that accounts for ladders and
/// slope geometry. Beyond 5 units the destination is returned unchanged. Within
/// the entity has arrived, so its own XZ is returned
/// (preserving the target's Y) to hold it in place; between the two the vector is
/// clamped to short of the target and snapped to a valid
/// surface via physics raycast.
///
public static Vector3 GetMoveToLocation(Context context, Vector3 position,
float maxDist = AIConstants.MoveToArrivalDistance)
{
var resolved = context.Self.world.FindSupportingBlockPos(position);
if (!(maxDist > 0f))
return resolved;
// Align ladder positions to block edges for smoother climbing.
var targetBlock = context.Self.world.GetBlock(new Vector3i(resolved));
if (targetBlock.Block is BlockLadder)
resolved = AlignToEdge(resolved);
var selfXZ = new Vector3(context.Self.position.x, resolved.y, context.Self.position.z);
var toTarget = resolved - selfXZ;
var dist = toTarget.magnitude;
if (dist > 5f)
return resolved;
if (dist <= maxDist)
{
// Align to ladder edge when there is meaningful vertical travel.
var yDelta = resolved.y - context.Self.position.y;
if (yDelta > AIConstants.LadderYDeltaThreshold || yDelta < -AIConstants.LadderYDeltaThreshold)
return AlignToEdge(resolved);
return selfXZ;
}
// Clamp vector and snap to surface.
toTarget *= maxDist / dist;
var clamped = resolved - toTarget;
clamped.y += 0.51f;
var blockPos = World.worldToBlockPos(clamped);
var block = context.Self.world.GetBlock(blockPos);
if (block.Block.PathType <= 0)
{
if (Physics.Raycast(clamped - Origin.position, Vector3.down, out var hit,
AIConstants.LandingCheckRayLength, 1082195968))
{
clamped.y = hit.point.y + Origin.position.y;
return clamped;
}
if (block.Block.IsElevator((int) block.rotation))
{
clamped.y = resolved.y;
return clamped;
}
}
return AlignToEdge(resolved);
}
/// Snaps a world-space vector to the nearest block-edge position.
public static Vector3 AlignToEdge(Vector3 vector)
{
return new Vector3i(vector).ToVector3();
}
// ── Jump ─────────────────────────────────────────────────────────────────
///
/// Evaluates whether the entity should jump given the terrain immediately ahead
/// and initiates the appropriate jump if so. Called automatically by .
///
public static void CheckJump(Context context)
{
if (!context.Self.onGround || context.Self.Jumping)
return;
// Sample just ahead of and slightly above the entity's feet.
var samplePos = context.Self.position + context.Self.GetForwardVector() * AIConstants.JumpForwardNudge;
samplePos.y += AIConstants.JumpVerticalNudge;
// If solid ground is close below the sample point, no jump is needed.
if (Physics.Raycast(samplePos - Origin.position, Vector3.down, out _, AIConstants.GroundCheckRayLength, 1082195968))
return;
// Check headroom directly above (avoids jumping into a ceiling).
var headroomCheck = new Vector3i(
Utils.Fastfloor(context.Self.position.x),
Utils.Fastfloor(context.Self.position.y + AIConstants.JumpHeadroomYOffset),
Utils.Fastfloor(context.Self.position.z));
headroomCheck += Vector3i.forward;
var headBlock = context.Self.world.GetBlock(headroomCheck);
if (headBlock.Block.IsMovementBlocked(context.Self.world, headroomCheck, headBlock, BlockFace.None))
return;
// Stop forward motion before jumping to prevent sliding off edges.
EntityUtilities.Stop(context.Self.entityId);
// Use a low jump when the target is significantly below the entity.
var target = UAIUtils.ConvertToEntityAlive(context.ActionData.Target);
if (target != null && target.IsAlive())
{
var drop = Mathf.Abs(target.position.y - context.Self.position.y);
if (drop > AIConstants.LowJumpDropThreshold)
{
context.Self.moveHelper.StartJump(true, 1f, 0f);
return;
}
}
// If the landing spot is air, use a low jump; otherwise a standard jump.
var landingSpot = context.Self.position
+ context.Self.GetForwardVector()
+ context.Self.GetForwardVector()
+ Vector3.down;
var landingBlock = context.Self.world.GetBlock(new Vector3i(landingSpot));
if (landingBlock.isair)
context.Self.moveHelper.StartJump(true, 1f, 0f);
else
context.Self.moveHelper.StartJump(true, 2f, 1f);
}
// ── Blockage ─────────────────────────────────────────────────────────────
///
/// Returns true when the entity is stunned or its move-helper blocked-time
/// exceeds . Also calls
/// and as side effects.
///
public static bool IsBlocked(Context context)
{
if (context.Self.bodyDamage.CurrentStun != EnumEntityStunType.None)
return true;
CheckJump(context);
DoorUtils.CheckForClosedDoor(context);
return context.Self.moveHelper.BlockedTime > SCoreConstants.BlockedTime;
}
// ── Teleport ─────────────────────────────────────────────────────────────
/// Teleports the entity directly to .
public static void TeleportToPosition(Context context, Vector3 position)
{
context.Self.SetPosition(position);
}
///
/// Teleports the entity near its leader.
/// When is true, a random nearby spawn point is
/// chosen; otherwise the entity uses its own TeleportToPlayer logic (only
/// when not currently on a mission).
///
public static void TeleportToLeader(Context context, bool blocked = true)
{
var leader = EntityUtilities.GetLeaderOrOwner(context.Self.entityId) as EntityAlive;
if (leader == null) return;
if (blocked)
{
GameManager.Instance.World.GetRandomSpawnPositionMinMaxToPosition(
leader.position,
AIConstants.TeleportSpawnRadiusMin,
AIConstants.TeleportSpawnRadiusMax,
AIConstants.TeleportSpawnClearance,
false,
out var spawnPos);
if (spawnPos == Vector3.zero)
spawnPos = leader.position + Vector3.back;
context.Self.SetPosition(spawnPos);
return;
}
if (context.Self is IEntityAliveSDX sdxEntity && !sdxEntity.IsOnMission())
sdxEntity.TeleportToPlayer(leader, false);
}
// ── Move Back ────────────────────────────────────────────────────────────
/// Directs the entity's move-helper toward .
public static void MoveBack(Context context, Vector3 position)
{
context.Self.moveHelper.SetMoveTo(position, true);
}
}
}