多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View File

@@ -0,0 +1,140 @@
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.linearVelocity.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,
ShowLoadingScreen = true,
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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 00954cb28f3eb9e4aaf388feb35565dd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,36 @@
using UnityEngine;
using BaseGames.World;
using BaseGames.Player;
using BaseGames.Core.Save;
namespace BaseGames.Progression
{
/// <summary>
/// 硬性能力门禁(架构 16_SupportingModules §5.3)。
/// 在 AbilityGate 的基础上增加次级验证:检查玩家是否真正拾取了物理道具。
/// 当 _requirePhysicalValidation = true 时,还需要通过 _physicalPickupSwitchKey 确认。
/// </summary>
public class HardAbilityGate : AbilityGate
{
[Header("HardAbilityGate 设置")]
[Tooltip("是否还要求物理拾取验证(需要 World.Switches 中对应 Key = true")]
[SerializeField] private bool _requirePhysicalValidation = false;
[Tooltip("物理拾取验证 Switch Key需 SaveManager.Data.World.Switches[key] == true")]
[SerializeField] private string _physicalPickupSwitchKey;
[Header("引用")]
[SerializeField] private SaveManager _saveManager;
protected override bool EvaluateAccess()
{
if (!base.EvaluateAccess()) return false;
if (!_requirePhysicalValidation) return true;
// 次级检查:物理拾取确认
if (string.IsNullOrEmpty(_physicalPickupSwitchKey)) return true;
var save = _saveManager != null ? _saveManager.Data : null;
if (save?.World?.Switches == null) return false;
return save.World.Switches.TryGetValue(_physicalPickupSwitchKey, out bool val) && val;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f7ae44a614ccc104aa8ad89cf2eebf54
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
using UnityEngine;
using BaseGames.Player;
namespace BaseGames.Progression
{
/// <summary>
/// 房间逃脱信息 SO架构 16_SupportingModules §5.2)。
/// 当 AntiSoftlockSystem 检测到玩家卡关时,提供重置目标场景和生成点。
/// </summary>
[CreateAssetMenu(menuName = "AntiSoftlock/RoomEscapeInfo", fileName = "ESC_")]
public class RoomEscapeInfoSO : ScriptableObject
{
[Tooltip("逃脱目标场景名称(留空 = 最近检查点)")]
public string targetScene;
[Tooltip("目标场景中的生成点 ID留空 = 默认生成点)")]
public string spawnId;
[Tooltip("需要玩家拥有此能力才触发此逃脱路径None = 无要求)")]
public AbilityType requiredAbility = AbilityType.None;
[Tooltip("此逃脱路径的优先级(越高越优先选择)")]
public int priority = 0;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 89e0ac3b73daf45479939b0d29070cf0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: