using UnityEngine;
using BaseGames.Enemies;
namespace BaseGames.Enemies.Navigation
{
///
/// 地面单位导航:朝目标 X 直接行走,由 的墙/悬崖夹紧决定可达性。
///
/// 不做任何地形扫描:走得过去就是可达,被夹停的地方就是边界。
/// 因此不存在"脱离导航图"这类状态——敌人无论被击退/冲锋到哪里,都能正常继续行走。
///
/// 只驱动水平方向;垂直交给刚体重力。移动经 下发,
/// 速度为 0 时退化为移动层配置的步速。
///
[RequireComponent(typeof(EnemyMovement))]
public sealed class GroundNavigator : MonoBehaviour, IEnemyNavigator
{
[Header("到达判定")]
[Tooltip("身体中心 X 与目标 X 的距离小于此值即视为到达(m)")]
[SerializeField] [Min(0.01f)] private float _stoppingDistance = 0.2f;
[Header("游走取点(Wander 用)")]
[Tooltip("单次游走的最近距离(m)")]
[SerializeField] [Min(0f)] private float _wanderMinRange = 1.5f;
[Tooltip("单次游走的最远距离(m)。走不到会被地形夹停,自然结束本次游走。")]
[SerializeField] [Min(0f)] private float _wanderMaxRange = 5f;
[Header("推进停滞检测")]
[Tooltip("窗口时长(s):此时长内净位移不足即判受阻")]
[SerializeField] [Min(0.05f)] private float _stallWindowSeconds = 0.4f;
[Tooltip("窗口内所需最小净位移(m)")]
[SerializeField] [Min(0f)] private float _stallMinProgress = 0.05f;
private EnemyBase _enemy;
private EnemyMovement _movement;
private StallDetector _stall;
private Vector2 _target;
private bool _hasTarget;
private float _speed; // 0 = 用移动层配置步速
private bool _arrived;
private bool _blocked;
public bool IsMoving => _hasTarget && !_arrived && !_blocked;
public bool HasArrived => _arrived;
public bool IsBlocked => _blocked;
private void Awake()
{
_enemy = GetComponent();
_movement = GetComponent();
_stall = new StallDetector(_stallWindowSeconds, _stallMinProgress);
if (_enemy == null)
Debug.LogError($"[GroundNavigator] {name} 找不到 EnemyBase,无法读取身体几何。", this);
}
// ── IEnemyNavigator ────────────────────────────────────────────
public void MoveTowards(Vector2 target)
{
// 幂等:同一目标重复调用不重置状态,避免每帧调用时永远清掉到达/受阻标志
if (_hasTarget && (target - _target).sqrMagnitude < 1e-6f) return;
_target = target;
_hasTarget = true;
_arrived = false;
_blocked = false;
_stall.Reset(BodyX);
}
public void Stop()
{
_hasTarget = false;
_arrived = false;
_blocked = false;
HaltMovement();
}
///
/// 停下水平移动。
/// ⚠ 必须直接写移动层输入,**不可**回调 ——
/// 后者会调用 Nav.Stop(),与本导航器互相调用造成无限递归(栈溢出)。
/// 分层方向只允许 导航器 → 移动层,不允许反向回调。
///
private void HaltMovement()
{
if (_movement == null) return;
_movement.PendingInput.MoveDir = 0f;
_movement.PendingInput.MoveSpeed = 0f;
_movement.PendingInput.WantStop = true;
}
public void SetSpeed(float speed) => _speed = Mathf.Max(0f, speed);
public bool TryPickWanderPoint(out Vector2 point)
{
float sign = Random.value < 0.5f ? -1f : 1f;
float x = WanderX(BodyX, _wanderMinRange, _wanderMaxRange, sign, Random.value);
point = new Vector2(x, transform.position.y);
return true; // 地面单位总能尝试;走不到会被夹停并置 IsBlocked
}
// ── 驱动 ───────────────────────────────────────────────────────
private float BodyX => _enemy != null ? _enemy.Body.Center.x : transform.position.x;
private void FixedUpdate()
{
if (!_hasTarget || _arrived || _blocked || _movement == null) return;
float bodyX = BodyX;
if (Arrived(bodyX, _target.x, _stoppingDistance))
{
_arrived = true;
HaltMovement();
return;
}
float dir = Mathf.Sign(_target.x - bodyX);
// 地形夹紧即为"不可达边界":着地时前方是墙或悬崖 → 本次移动到此为止
if (_movement.IsGrounded && _movement.WouldBlockAhead(dir))
{
_blocked = true;
HaltMovement();
return;
}
_movement.PendingInput.MoveDir = dir;
_movement.PendingInput.MoveSpeed = _speed; // 0 → MoveHorizontal 用配置步速
_movement.PendingInput.WantStop = false;
// 兜底:命令了移动却推不动(撞上未纳入检测层的碰撞体等)
if (_stall.Tick(bodyX, Time.fixedDeltaTime))
{
_blocked = true;
HaltMovement();
}
}
// ── 纯函数(便于无场景单测)──────────────────────────────────────
/// 身体中心 X 是否已进入目标 X 的停止距离内。
public static bool Arrived(float currentX, float targetX, float stopDistance)
=> Mathf.Abs(targetX - currentX) <= stopDistance;
///
/// 游走目标 X:自当前位置朝 方向,
/// 在 [minRange, maxRange] 区间内按插值系数 (0..1) 取点。
///
public static float WanderX(float currentX, float minRange, float maxRange, float sign, float t)
=> currentX + sign * Mathf.Lerp(minRange, Mathf.Max(minRange, maxRange), Mathf.Clamp01(t));
}
}