521 lines
20 KiB
C#
521 lines
20 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using Cielonos.MainGame.Characters;
|
||
using Cielonos.MainGame.Map;
|
||
using Cielonos.MainGame.UI;
|
||
using Sirenix.OdinInspector;
|
||
using SLSUtilities.General;
|
||
using UnityEngine;
|
||
|
||
namespace Cielonos.MainGame
|
||
{
|
||
/// <summary>
|
||
/// 驱动整局 Roguelite Run 的会话管理器。
|
||
/// 负责地图生成、节点选择验证、阶段状态机切换和结算流程。
|
||
/// </summary>
|
||
public class RunManager : Singleton<RunManager>
|
||
{
|
||
// ----------------------------------------------------------------
|
||
// 配置
|
||
// ----------------------------------------------------------------
|
||
|
||
[TitleGroup("配置")]
|
||
[Tooltip("地图生成配置")]
|
||
public MapGenerationConfig mapConfig;
|
||
|
||
// ----------------------------------------------------------------
|
||
// 运行时状态
|
||
// ----------------------------------------------------------------
|
||
|
||
[TitleGroup("运行时状态"), ReadOnly]
|
||
public RunState currentRun;
|
||
|
||
[TitleGroup("运行时状态"), ReadOnly]
|
||
public RunPhase currentPhase = RunPhase.Idle;
|
||
|
||
// ----------------------------------------------------------------
|
||
// 事件
|
||
// ----------------------------------------------------------------
|
||
|
||
/// <summary>Run 阶段切换时触发,参数为新阶段。</summary>
|
||
public event Action<RunPhase> OnPhaseChanged;
|
||
|
||
/// <summary>节点选择被确认时触发,参数为目标节点坐标。</summary>
|
||
public event Action<Vector2Int> OnNodeSelected;
|
||
|
||
/// <summary>当前节点完成时触发,参数为完成的节点。</summary>
|
||
public event Action<RunMapNode> OnNodeCompleted;
|
||
|
||
/// <summary>一局 Run 结束时触发(通关或死亡),参数为最终 RunState。</summary>
|
||
public event Action<RunState> OnRunEnded;
|
||
|
||
// ================================================================
|
||
// 公共 API
|
||
// ================================================================
|
||
|
||
/// <summary>
|
||
/// 开始新的一局 Run:生成地图,初始化 RunState,立即加载起点 Zone。
|
||
/// </summary>
|
||
public void StartNewRun()
|
||
{
|
||
if (mapConfig == null)
|
||
{
|
||
Debug.LogError("[RunManager] mapConfig 未配置,无法开始 Run。");
|
||
return;
|
||
}
|
||
|
||
// 取消上一局的事件订阅(重启时可能已有上一局残留)
|
||
UnsubscribePlayerEvents();
|
||
UnsubscribeRoomEvents();
|
||
|
||
// 从 MainGameManager.Seed 创建本局 RNG;为空则自动生成随机种子
|
||
Randomizer runRng = new Randomizer(MainGameManager.Seed);
|
||
|
||
RunMapData mapData = MapGenerator.Generate(mapConfig);
|
||
if (mapData == null)
|
||
{
|
||
Debug.LogError("[RunManager] 地图生成失败,无法开始 Run。");
|
||
return;
|
||
}
|
||
|
||
currentRun = new RunState
|
||
{
|
||
randomizer = runRng,
|
||
mapData = mapData,
|
||
currentPosition = mapData.startPosition,
|
||
visitedNodes = new HashSet<Vector2Int> { mapData.startPosition },
|
||
exhaustedNodes = new HashSet<Vector2Int>(),
|
||
completedNodes = new HashSet<Vector2Int>(),
|
||
permanentlyRevealedNodes = new HashSet<Vector2Int>(),
|
||
permanentlyRevealedTypes = new HashSet<MapNodeType>(),
|
||
scoutRange = 1,
|
||
elapsedTime = 0f,
|
||
roomsCleared = 0,
|
||
enemiesDefeated = 0,
|
||
hurtCount = 0,
|
||
isCompleted = false,
|
||
};
|
||
|
||
Debug.Log(
|
||
$"[RunManager] 新一局开始(Seed: {runRng.Seed}),地图节点数:{mapData.totalNodes}," +
|
||
$"起点:{mapData.startPosition},Boss:{mapData.bossPosition}");
|
||
|
||
PlayerCanvas.MainGamePages.mapPage.Populate(mapData);
|
||
|
||
// 订阅玩家事件与战斗房间事件
|
||
SubscribePlayerEvents();
|
||
SubscribeRoomEvents();
|
||
|
||
// 立即加载起点 Zone,不先进入 MapSelection
|
||
RunMapNode startNode = mapData.nodes[mapData.startPosition];
|
||
if (startNode.zoneData != null)
|
||
{
|
||
TransitionToPhase(RunPhase.Transitioning);
|
||
MapManager.Instance.LoadZone(startNode.zoneData, onComplete: () =>
|
||
{
|
||
TransitionToPhase(RunPhase.MapSelection);
|
||
});
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("[RunManager] 起点节点没有配置 ZoneData,直接进入地图选择。");
|
||
TransitionToPhase(RunPhase.MapSelection);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 玩家在地图 UI 中选择了一个节点。
|
||
/// 验证该节点是否可到达(已访问节点 + 其1格邻居),然后执行移动。
|
||
/// 允许在 MapSelection 和非战斗阶段(InShop/InRest/InMechanical)中选择节点。
|
||
/// 战斗中(InCombat/InBoss)需清空敌人后由 OnRoomCleared 切换到 MapSelection 才可选择。
|
||
/// </summary>
|
||
/// <param name="targetPosition">目标节点的网格坐标。</param>
|
||
public void SelectNode(Vector2Int targetPosition)
|
||
{
|
||
if (currentRun == null)
|
||
{
|
||
Debug.LogWarning("[RunManager] 当前没有进行中的 Run,忽略节点选择。");
|
||
return;
|
||
}
|
||
|
||
if (!IsPhaseAllowingNodeSelection())
|
||
{
|
||
Debug.LogWarning($"[RunManager] 当前阶段为 {currentPhase},不允许选择节点。");
|
||
return;
|
||
}
|
||
|
||
if (targetPosition == currentRun.currentPosition)
|
||
{
|
||
Debug.Log("[RunManager] 目标即当前位置,忽略。");
|
||
return;
|
||
}
|
||
|
||
if (!IsNodeReachable(targetPosition))
|
||
{
|
||
Debug.LogWarning(
|
||
$"[RunManager] 节点 {targetPosition} 不可到达" +
|
||
$"(不在已访问节点的1格范围内)。");
|
||
return;
|
||
}
|
||
|
||
if (!currentRun.mapData.nodes.TryGetValue(targetPosition, out RunMapNode targetNode))
|
||
{
|
||
Debug.LogError($"[RunManager] 节点 {targetPosition} 在地图数据中不存在。");
|
||
return;
|
||
}
|
||
|
||
// 移动到目标节点
|
||
currentRun.currentPosition = targetPosition;
|
||
currentRun.visitedNodes.Add(targetPosition);
|
||
|
||
OnNodeSelected?.Invoke(targetPosition);
|
||
Debug.Log($"[RunManager] 移动到节点 {targetPosition},类型:{targetNode.nodeType}");
|
||
|
||
EnterNode(targetNode);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前房间(战斗/商店/休息/机械台)完成,更新统计数据并返回地图。
|
||
/// 单次使用节点(MedicalStation、MechanicalTable)会标记为 exhausted。
|
||
/// </summary>
|
||
public void CompleteCurrentNode()
|
||
{
|
||
if (currentRun == null) return;
|
||
|
||
Vector2Int pos = currentRun.currentPosition;
|
||
|
||
// 已完成的节点不重复结算
|
||
if (currentRun.completedNodes.Contains(pos))
|
||
{
|
||
Debug.Log($"[RunManager] 节点 {pos} 已完成过,跳过重复结算。");
|
||
return;
|
||
}
|
||
|
||
if (!currentRun.mapData.nodes.TryGetValue(pos, out RunMapNode node))
|
||
return;
|
||
|
||
// 标记为已完成
|
||
currentRun.completedNodes.Add(pos);
|
||
|
||
// 更新战斗统计
|
||
if (node.nodeType == MapNodeType.NormalBattle || node.nodeType == MapNodeType.EliteBattle)
|
||
currentRun.roomsCleared++;
|
||
|
||
// 单次使用节点标记为已用完
|
||
if (node.nodeType == MapNodeType.MedicalStation || node.nodeType == MapNodeType.MechanicalTable)
|
||
currentRun.exhaustedNodes.Add(pos);
|
||
|
||
OnNodeCompleted?.Invoke(node);
|
||
|
||
// Boss 通关判定
|
||
if (currentRun.currentPosition == currentRun.mapData.bossPosition)
|
||
{
|
||
currentRun.isCompleted = true;
|
||
Debug.Log("[RunManager] Boss 已击败,Run 通关!");
|
||
TransitionToPhase(RunPhase.Settlement);
|
||
OnRunEnded?.Invoke(currentRun);
|
||
return;
|
||
}
|
||
|
||
TransitionToPhase(RunPhase.MapSelection);
|
||
}
|
||
|
||
/// <summary>玩家死亡,进入结算阶段。由玩家 eventSm.onDeath 回调驱动。</summary>
|
||
private void OnPlayerDeath()
|
||
{
|
||
if (currentRun == null) return;
|
||
|
||
currentRun.isCompleted = false;
|
||
Debug.Log("[RunManager] 玩家死亡,进入结算。");
|
||
TransitionToPhase(RunPhase.Settlement);
|
||
OnRunEnded?.Invoke(currentRun);
|
||
}
|
||
|
||
/// <summary>结算完毕,清理 RunState 并回到 Idle 阶段。</summary>
|
||
public void ReturnToHub()
|
||
{
|
||
UnsubscribePlayerEvents();
|
||
UnsubscribeRoomEvents();
|
||
currentRun = null;
|
||
TransitionToPhase(RunPhase.Idle);
|
||
Debug.Log("[RunManager] 已返回 Hub,RunState 已清理。");
|
||
}
|
||
|
||
// ================================================================
|
||
// 战斗房间事件
|
||
// ================================================================
|
||
|
||
/// <summary>订阅 BattleRoomSm.OnRoomCleared,战斗房间清空后自动完成当前节点。</summary>
|
||
private void SubscribeRoomEvents()
|
||
{
|
||
BattleManager.BattleRoomSm.OnRoomCleared += OnRoomCleared;
|
||
}
|
||
|
||
/// <summary>取消订阅 BattleRoomSm.OnRoomCleared。</summary>
|
||
private void UnsubscribeRoomEvents()
|
||
{
|
||
BattleManager.BattleRoomSm.OnRoomCleared -= OnRoomCleared;
|
||
}
|
||
|
||
/// <summary>战斗房间清空回调,完成当前战斗节点并切换到 MapSelection。
|
||
/// Boss 节点例外:仅切换到 MapSelection,让 ExitGate 交互后再调用 CompleteCurrentNode()。
|
||
/// </summary>
|
||
private void OnRoomCleared()
|
||
{
|
||
if (currentRun == null) return;
|
||
|
||
bool isBoss = currentRun.currentPosition == currentRun.mapData.bossPosition;
|
||
|
||
if (isBoss)
|
||
{
|
||
// Boss 击败:激活 ExitGate(由 ExitGate 脚本监听 OnRoomCleared 自行激活),
|
||
// RunManager 只需切换到 MapSelection 阻止战斗状态机继续锁定输入。
|
||
Debug.Log("[RunManager] Boss 已击败,等待玩家通过 ExitGate 进入结算。");
|
||
TransitionToPhase(RunPhase.MapSelection);
|
||
}
|
||
else
|
||
{
|
||
Debug.Log("[RunManager] 收到 OnRoomCleared,完成当前战斗节点。");
|
||
CompleteCurrentNode();
|
||
}
|
||
}
|
||
|
||
// ================================================================
|
||
// 玩家事件追踪
|
||
// ================================================================
|
||
|
||
private const string RunManagerHurtKey = "RunManager_HurtTracking";
|
||
private const string RunManagerDeathKey = "RunManager_Death";
|
||
|
||
/// <summary>
|
||
/// 通过 EventSubmodule 订阅玩家的 onHurt 和 onDeath,
|
||
/// 分别追踪实际扣血次数和玩家死亡触发结算。
|
||
/// </summary>
|
||
private void SubscribePlayerEvents()
|
||
{
|
||
if (MainGameManager.Player == null) return;
|
||
|
||
EventSubmodule playerEvents = MainGameManager.Player.eventSm;
|
||
|
||
playerEvents.onHurt.InsertByPriority(
|
||
RunManagerHurtKey,
|
||
new PrioritizedAction<AttackAreaBase>(_ => OnPlayerHurt(), priority: 0));
|
||
|
||
playerEvents.onDeath.InsertByPriority(
|
||
RunManagerDeathKey,
|
||
new PrioritizedAction(OnPlayerDeath, priority: 0));
|
||
}
|
||
|
||
/// <summary>取消对玩家 EventSubmodule 中所有 RunManager 相关订阅。</summary>
|
||
private void UnsubscribePlayerEvents()
|
||
{
|
||
if (MainGameManager.Player == null) return;
|
||
|
||
EventSubmodule playerEvents = MainGameManager.Player.eventSm;
|
||
playerEvents.onHurt.Remove(RunManagerHurtKey);
|
||
playerEvents.onDeath.Remove(RunManagerDeathKey);
|
||
}
|
||
|
||
/// <summary>玩家实际受到伤害时回调(来自 eventSm.onHurt),累加受伤次数。</summary>
|
||
private void OnPlayerHurt()
|
||
{
|
||
if (currentRun != null)
|
||
currentRun.hurtCount++;
|
||
}
|
||
|
||
// ================================================================
|
||
// 查询工具
|
||
// ================================================================
|
||
|
||
/// <summary>
|
||
/// 返回当前可传送的节点坐标集合:
|
||
/// 所有已访问节点 + 所有已访问节点的1格邻居,不含当前位置。
|
||
/// </summary>
|
||
public HashSet<Vector2Int> GetSelectablePositions()
|
||
{
|
||
if (currentRun == null)
|
||
return new HashSet<Vector2Int>();
|
||
|
||
return MapFogCalculator.ComputeSelectableSet(currentRun);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算所有节点的完整显示状态(可见性 / 交互性 / 使用状态),供地图 UI 使用。
|
||
/// </summary>
|
||
public Dictionary<Vector2Int, NodeDisplayState> GetAllNodeDisplayStates()
|
||
{
|
||
if (currentRun == null)
|
||
return new Dictionary<Vector2Int, NodeDisplayState>();
|
||
|
||
return MapFogCalculator.Calculate(currentRun);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断指定节点是否已被标记为"用完"(单次使用的特殊节点)。
|
||
/// </summary>
|
||
public bool IsNodeExhausted(Vector2Int position)
|
||
{
|
||
return currentRun != null && currentRun.exhaustedNodes.Contains(position);
|
||
}
|
||
|
||
// ================================================================
|
||
// 探测范围扩展 API(供道具/装备系统调用)
|
||
// ================================================================
|
||
|
||
/// <summary>增加探测半径(如装备"高级雷达")。</summary>
|
||
public void IncreaseScoutRange(int amount)
|
||
{
|
||
if (currentRun == null) return;
|
||
currentRun.scoutRange += amount;
|
||
Debug.Log($"[RunManager] 探测范围增加 {amount},当前:{currentRun.scoutRange}");
|
||
}
|
||
|
||
/// <summary>永久揭示指定坐标的节点(如道具"地图碎片")。</summary>
|
||
public void RevealNode(Vector2Int position)
|
||
{
|
||
if (currentRun == null) return;
|
||
currentRun.permanentlyRevealedNodes.Add(position);
|
||
Debug.Log($"[RunManager] 永久揭示节点 {position}");
|
||
}
|
||
|
||
/// <summary>永久揭示指定类型的所有节点(如道具"商人指南针"揭示所有商店)。</summary>
|
||
public void RevealNodeType(MapNodeType nodeType)
|
||
{
|
||
if (currentRun == null) return;
|
||
currentRun.permanentlyRevealedTypes.Add(nodeType);
|
||
Debug.Log($"[RunManager] 永久揭示所有 {nodeType} 类型节点");
|
||
}
|
||
|
||
// ================================================================
|
||
// 内部实现
|
||
// ================================================================
|
||
|
||
/// <summary>
|
||
/// 验证目标是否可到达:已访问节点或已访问节点的1格邻居。
|
||
/// </summary>
|
||
private bool IsNodeReachable(Vector2Int targetPosition)
|
||
{
|
||
// 已访问的节点可以再次进入
|
||
if (currentRun.visitedNodes.Contains(targetPosition))
|
||
return true;
|
||
|
||
// 检查是否是任意已访问节点的直接邻居
|
||
foreach (Vector2Int visitedPos in currentRun.visitedNodes)
|
||
{
|
||
if (currentRun.mapData.nodes.TryGetValue(visitedPos, out RunMapNode node))
|
||
{
|
||
if (node.connectedPositions.Contains(targetPosition))
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断当前阶段是否允许选择地图节点进行传送。
|
||
/// 允许:MapSelection、InShop、InRest、InMechanical(非战斗阶段均可自由离开)。
|
||
/// 阻止:Idle、Transitioning、InCombat、InBoss、Settlement。
|
||
/// </summary>
|
||
private bool IsPhaseAllowingNodeSelection()
|
||
{
|
||
switch (currentPhase)
|
||
{
|
||
case RunPhase.MapSelection:
|
||
case RunPhase.InShop:
|
||
case RunPhase.InRest:
|
||
case RunPhase.InMechanical:
|
||
return true;
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据节点类型切换阶段,并通过 MapManager 加载对应的 Zone 场景。
|
||
/// 已完成的节点(如已清空的战斗房间)直接进入 MapSelection,且跳过敌人生成和战斗启动。
|
||
/// </summary>
|
||
private void EnterNode(RunMapNode node)
|
||
{
|
||
bool isCompleted = currentRun.completedNodes.Contains(currentRun.currentPosition);
|
||
RunPhase targetPhase = isCompleted ? RunPhase.MapSelection : NodeTypeToPhase(node.nodeType);
|
||
|
||
if (isCompleted)
|
||
{
|
||
Debug.Log($"[RunManager] 节点 {currentRun.currentPosition} 已完成,跳过 {NodeTypeToPhase(node.nodeType)},直接进入 MapSelection。");
|
||
}
|
||
|
||
if (node.zoneData != null)
|
||
{
|
||
TransitionToPhase(RunPhase.Transitioning);
|
||
MapManager.Instance.LoadZone(node.zoneData, onComplete: () =>
|
||
{
|
||
TransitionToPhase(targetPhase);
|
||
}, skipBattleSetup: isCompleted);
|
||
}
|
||
else
|
||
{
|
||
// 没有 ZoneData 的节点(如 MedicalStation)直接切换阶段
|
||
TransitionToPhase(targetPhase);
|
||
}
|
||
}
|
||
|
||
/// <summary>将节点类型映射到对应的 RunPhase。</summary>
|
||
private static RunPhase NodeTypeToPhase(MapNodeType nodeType)
|
||
{
|
||
switch (nodeType)
|
||
{
|
||
case MapNodeType.NormalBattle:
|
||
case MapNodeType.EliteBattle:
|
||
return RunPhase.InCombat;
|
||
case MapNodeType.BossBattle:
|
||
return RunPhase.InBoss;
|
||
case MapNodeType.LogisticsCenter:
|
||
return RunPhase.InShop;
|
||
case MapNodeType.MedicalStation:
|
||
return RunPhase.InRest;
|
||
case MapNodeType.MechanicalTable:
|
||
return RunPhase.InMechanical;
|
||
default:
|
||
return RunPhase.MapSelection;
|
||
}
|
||
}
|
||
|
||
/// <summary>切换阶段并触发 OnPhaseChanged 事件。</summary>
|
||
private void TransitionToPhase(RunPhase newPhase)
|
||
{
|
||
if (currentPhase == newPhase) return;
|
||
currentPhase = newPhase;
|
||
Debug.Log($"[RunManager] 阶段切换 → {newPhase}");
|
||
OnPhaseChanged?.Invoke(newPhase);
|
||
}
|
||
|
||
// ================================================================
|
||
// MonoBehaviour
|
||
// ================================================================
|
||
|
||
private void Update()
|
||
{
|
||
if (currentRun != null && currentPhase != RunPhase.Idle && currentPhase != RunPhase.Settlement)
|
||
currentRun.elapsedTime += Time.deltaTime;
|
||
}
|
||
}
|
||
|
||
// ================================================================
|
||
// Run 阶段枚举
|
||
// ================================================================
|
||
|
||
public enum RunPhase
|
||
{
|
||
Idle, // 在 Hub 中,没有进行中的 Run
|
||
MapSelection, // 显示地图,等待玩家选择节点
|
||
Transitioning, // 场景切换中
|
||
InCombat, // 普通/精英战斗中
|
||
InShop, // 商店(LogisticsCenter)中
|
||
InRest, // 休息(MedicalStation)中
|
||
InMechanical, // 机械台(MechanicalTable)中
|
||
InBoss, // Boss 战中
|
||
Settlement, // 结算画面
|
||
}
|
||
}
|