141 lines
4.9 KiB
C#
141 lines
4.9 KiB
C#
using UnityEngine;
|
||
using BaseGames.Core;
|
||
using BaseGames.Core.Events;
|
||
using BaseGames.Input;
|
||
using BaseGames.Player;
|
||
using BaseGames.Player.States;
|
||
using BaseGames.Progression;
|
||
|
||
namespace BaseGames.Support.AntiSoftlock
|
||
{
|
||
/// <summary>
|
||
/// 防软锁系统(架构 16_SupportingModules §5)。
|
||
/// 检测玩家长时间静止且无法移动的情况,提供逃脱选项。
|
||
/// </summary>
|
||
public class AntiSoftlockSystem : MonoBehaviour
|
||
{
|
||
[Header("配置")]
|
||
[Tooltip("玩家静止超过此秒数后显示逃脱提示")]
|
||
[SerializeField] private float _stuckThresholdSeconds = 30f;
|
||
[Tooltip("移动速度低于此值视为静止")]
|
||
[SerializeField] private float _minVelocityThreshold = 0.1f;
|
||
|
||
[Header("逃脱路径列表(按优先级降序排列)")]
|
||
[SerializeField] private RoomEscapeInfoSO[] _escapeOptions;
|
||
|
||
[Header("事件频道")]
|
||
[SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest;
|
||
[SerializeField] private VoidEventChannelSO _onShowEscapeUI;
|
||
[SerializeField] private VoidEventChannelSO _onHideEscapeUI;
|
||
/// <summary>
|
||
/// 玩家生成事件频道(PlayerController.Start() 广播)。
|
||
/// 替代 FindFirstObjectByType 的全场景扫描。
|
||
/// </summary>
|
||
[SerializeField] private TransformEventChannelSO _onPlayerSpawned;
|
||
|
||
[Header("引用")]
|
||
[SerializeField] private InputReaderSO _input;
|
||
|
||
private PlayerController _player;
|
||
private Rigidbody2D _playerRb;
|
||
private float _stuckTimer;
|
||
private bool _escapeUIVisible;
|
||
private Vector2 _lastPos;
|
||
|
||
private readonly BaseGames.Core.Events.CompositeDisposable _subs = new();
|
||
|
||
private void OnEnable()
|
||
{
|
||
if (_onPlayerSpawned != null)
|
||
_onPlayerSpawned.Subscribe(OnPlayerSpawned).AddTo(_subs);
|
||
}
|
||
|
||
private void OnDisable() => _subs.Clear();
|
||
|
||
private void OnPlayerSpawned(Transform playerTransform)
|
||
{
|
||
if (playerTransform == null) return;
|
||
_player = playerTransform.GetComponent<PlayerController>();
|
||
_playerRb = playerTransform.GetComponent<Rigidbody2D>();
|
||
_lastPos = playerTransform.position;
|
||
_stuckTimer = 0f;
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
if (_player == null || !_player.Stats.IsAlive) return;
|
||
|
||
Vector2 pos = _player.transform.position;
|
||
float vel = _playerRb != null ? _playerRb.velocity.magnitude : Vector2.Distance(pos, _lastPos) / Time.deltaTime;
|
||
|
||
if (vel > _minVelocityThreshold)
|
||
{
|
||
_stuckTimer = 0f;
|
||
_lastPos = pos;
|
||
if (_escapeUIVisible) HideEscapeUI();
|
||
return;
|
||
}
|
||
|
||
_lastPos = pos;
|
||
_stuckTimer += Time.deltaTime;
|
||
|
||
if (_stuckTimer >= _stuckThresholdSeconds && !_escapeUIVisible)
|
||
ShowEscapeUI();
|
||
}
|
||
|
||
private void ShowEscapeUI()
|
||
{
|
||
_escapeUIVisible = true;
|
||
_onShowEscapeUI?.Raise();
|
||
}
|
||
|
||
private void HideEscapeUI()
|
||
{
|
||
_escapeUIVisible = false;
|
||
_onHideEscapeUI?.Raise();
|
||
}
|
||
|
||
/// <summary>执行最优先的逃脱路径(由 UI 按钮调用)。</summary>
|
||
public void ExecuteEscape()
|
||
{
|
||
var escape = ChooseBestEscape();
|
||
HideEscapeUI();
|
||
|
||
var sm = ServiceLocator.GetOrDefault<ISaveService>();
|
||
string scene = string.IsNullOrEmpty(escape?.targetScene)
|
||
? sm?.LastCheckpointScene
|
||
: escape.targetScene;
|
||
string spawn = string.IsNullOrEmpty(escape?.spawnId)
|
||
? sm?.LastCheckpointSpawnId
|
||
: escape.spawnId;
|
||
|
||
_onSceneLoadRequest?.Raise(new SceneLoadRequest
|
||
{
|
||
SceneName = scene,
|
||
EntryId = spawn,
|
||
TransitionType = TransitionType.Scene,
|
||
IsRespawn = true,
|
||
});
|
||
}
|
||
|
||
private RoomEscapeInfoSO ChooseBestEscape()
|
||
{
|
||
if (_escapeOptions == null || _escapeOptions.Length == 0) return null;
|
||
RoomEscapeInfoSO best = null;
|
||
int bestPriority = int.MinValue;
|
||
foreach (var opt in _escapeOptions)
|
||
{
|
||
if (opt == null) continue;
|
||
if (opt.requiredAbility != BaseGames.Player.AbilityType.None
|
||
&& (_player?.Stats.HasAbility(opt.requiredAbility) == false)) continue;
|
||
if (opt.priority > bestPriority)
|
||
{
|
||
bestPriority = opt.priority;
|
||
best = opt;
|
||
}
|
||
}
|
||
return best;
|
||
}
|
||
}
|
||
}
|