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 { /// /// 驱动整局 Roguelite Run 的会话管理器。 /// 负责地图生成、节点选择验证、阶段状态机切换和结算流程。 /// public class RunManager : Singleton { // ---------------------------------------------------------------- // 配置 // ---------------------------------------------------------------- [TitleGroup("配置")] [Tooltip("地图生成配置")] public MapGenerationConfig mapConfig; // ---------------------------------------------------------------- // 运行时状态 // ---------------------------------------------------------------- [TitleGroup("运行时状态"), ReadOnly] public RunState currentRun; [TitleGroup("运行时状态"), ReadOnly] public RunPhase currentPhase = RunPhase.Idle; // ---------------------------------------------------------------- // 事件 // ---------------------------------------------------------------- /// Run 阶段切换时触发,参数为新阶段。 public event Action OnPhaseChanged; /// 节点选择被确认时触发,参数为目标节点坐标。 public event Action OnNodeSelected; /// 当前节点完成时触发,参数为完成的节点。 public event Action OnNodeCompleted; /// 一局 Run 结束时触发(通关或死亡),参数为最终 RunState。 public event Action OnRunEnded; // ================================================================ // 公共 API // ================================================================ /// /// 开始新的一局 Run:生成地图,初始化 RunState,立即加载起点 Zone。 /// 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 { mapData.startPosition }, exhaustedNodes = new HashSet(), completedNodes = new HashSet(), permanentlyRevealedNodes = new HashSet(), permanentlyRevealedTypes = new HashSet(), 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); } } /// /// 玩家在地图 UI 中选择了一个节点。 /// 验证该节点是否可到达(已访问节点 + 其1格邻居),然后执行移动。 /// 允许在 MapSelection 和非战斗阶段(InShop/InRest/InMechanical)中选择节点。 /// 战斗中(InCombat/InBoss)需清空敌人后由 OnRoomCleared 切换到 MapSelection 才可选择。 /// /// 目标节点的网格坐标。 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); } /// /// 当前房间(战斗/商店/休息/机械台)完成,更新统计数据并返回地图。 /// 单次使用节点(MedicalStation、MechanicalTable)会标记为 exhausted。 /// 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); } /// 玩家死亡,进入结算阶段。由玩家 eventSm.onDeath 回调驱动。 private void OnPlayerDeath() { if (currentRun == null) return; currentRun.isCompleted = false; Debug.Log("[RunManager] 玩家死亡,进入结算。"); TransitionToPhase(RunPhase.Settlement); OnRunEnded?.Invoke(currentRun); } /// 结算完毕,清理 RunState 并回到 Idle 阶段。 public void ReturnToHub() { UnsubscribePlayerEvents(); UnsubscribeRoomEvents(); currentRun = null; TransitionToPhase(RunPhase.Idle); Debug.Log("[RunManager] 已返回 Hub,RunState 已清理。"); } // ================================================================ // 战斗房间事件 // ================================================================ /// 订阅 BattleRoomSm.OnRoomCleared,战斗房间清空后自动完成当前节点。 private void SubscribeRoomEvents() { BattleManager.BattleRoomSm.OnRoomCleared += OnRoomCleared; } /// 取消订阅 BattleRoomSm.OnRoomCleared。 private void UnsubscribeRoomEvents() { BattleManager.BattleRoomSm.OnRoomCleared -= OnRoomCleared; } /// 战斗房间清空回调,完成当前战斗节点并切换到 MapSelection。 /// Boss 节点例外:仅切换到 MapSelection,让 ExitGate 交互后再调用 CompleteCurrentNode()。 /// 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"; /// /// 通过 EventSubmodule 订阅玩家的 onHurt 和 onDeath, /// 分别追踪实际扣血次数和玩家死亡触发结算。 /// private void SubscribePlayerEvents() { if (MainGameManager.Player == null) return; EventSubmodule playerEvents = MainGameManager.Player.eventSm; playerEvents.onHurt.InsertByPriority( RunManagerHurtKey, new PrioritizedAction(_ => OnPlayerHurt(), priority: 0)); playerEvents.onDeath.InsertByPriority( RunManagerDeathKey, new PrioritizedAction(OnPlayerDeath, priority: 0)); } /// 取消对玩家 EventSubmodule 中所有 RunManager 相关订阅。 private void UnsubscribePlayerEvents() { if (MainGameManager.Player == null) return; EventSubmodule playerEvents = MainGameManager.Player.eventSm; playerEvents.onHurt.Remove(RunManagerHurtKey); playerEvents.onDeath.Remove(RunManagerDeathKey); } /// 玩家实际受到伤害时回调(来自 eventSm.onHurt),累加受伤次数。 private void OnPlayerHurt() { if (currentRun != null) currentRun.hurtCount++; } // ================================================================ // 查询工具 // ================================================================ /// /// 返回当前可传送的节点坐标集合: /// 所有已访问节点 + 所有已访问节点的1格邻居,不含当前位置。 /// public HashSet GetSelectablePositions() { if (currentRun == null) return new HashSet(); return MapFogCalculator.ComputeSelectableSet(currentRun); } /// /// 计算所有节点的完整显示状态(可见性 / 交互性 / 使用状态),供地图 UI 使用。 /// public Dictionary GetAllNodeDisplayStates() { if (currentRun == null) return new Dictionary(); return MapFogCalculator.Calculate(currentRun); } /// /// 判断指定节点是否已被标记为"用完"(单次使用的特殊节点)。 /// public bool IsNodeExhausted(Vector2Int position) { return currentRun != null && currentRun.exhaustedNodes.Contains(position); } // ================================================================ // 探测范围扩展 API(供道具/装备系统调用) // ================================================================ /// 增加探测半径(如装备"高级雷达")。 public void IncreaseScoutRange(int amount) { if (currentRun == null) return; currentRun.scoutRange += amount; Debug.Log($"[RunManager] 探测范围增加 {amount},当前:{currentRun.scoutRange}"); } /// 永久揭示指定坐标的节点(如道具"地图碎片")。 public void RevealNode(Vector2Int position) { if (currentRun == null) return; currentRun.permanentlyRevealedNodes.Add(position); Debug.Log($"[RunManager] 永久揭示节点 {position}"); } /// 永久揭示指定类型的所有节点(如道具"商人指南针"揭示所有商店)。 public void RevealNodeType(MapNodeType nodeType) { if (currentRun == null) return; currentRun.permanentlyRevealedTypes.Add(nodeType); Debug.Log($"[RunManager] 永久揭示所有 {nodeType} 类型节点"); } // ================================================================ // 内部实现 // ================================================================ /// /// 验证目标是否可到达:已访问节点或已访问节点的1格邻居。 /// 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; } /// /// 判断当前阶段是否允许选择地图节点进行传送。 /// 允许:MapSelection、InShop、InRest、InMechanical(非战斗阶段均可自由离开)。 /// 阻止:Idle、Transitioning、InCombat、InBoss、Settlement。 /// private bool IsPhaseAllowingNodeSelection() { switch (currentPhase) { case RunPhase.MapSelection: case RunPhase.InShop: case RunPhase.InRest: case RunPhase.InMechanical: return true; default: return false; } } /// /// 根据节点类型切换阶段,并通过 MapManager 加载对应的 Zone 场景。 /// 已完成的节点(如已清空的战斗房间)直接进入 MapSelection,且跳过敌人生成和战斗启动。 /// 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); } } /// 将节点类型映射到对应的 RunPhase。 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; } } /// 切换阶段并触发 OnPhaseChanged 事件。 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, // 结算画面 } }