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;
|
|
}
|
|
}
|
|
}
|