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, // 结算画面
}
}