Files
Cielonos/Assets/Scripts/MainGame/GameRun/RunManager.cs
SoulliesOfficial 649b7a5ddc 更新
2026-05-23 08:27:50 -04:00

531 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ZoneEntryContext> OnZoneEntered;
/// <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;
bool isFirstVisit = currentRun.visitedNodes.Add(targetPosition);
bool isCompleted = currentRun.completedNodes.Contains(targetPosition);
OnNodeSelected?.Invoke(targetPosition);
OnZoneEntered?.Invoke(new ZoneEntryContext(targetPosition, targetNode, isFirstVisit, isCompleted));
Debug.Log($"[RunManager] 移动到节点 {targetPosition},类型:{targetNode.nodeType}" +
$",首次进入:{isFirstVisit},已完成:{isCompleted}");
CombatManager.AttackAreaSm.DestroyAll();
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] 已返回 HubRunState 已清理。");
}
// ================================================================
// 战斗房间事件
// ================================================================
/// <summary>订阅 CombatRoomSm.OnRoomCleared战斗房间清空后自动完成当前节点。</summary>
private void SubscribeRoomEvents()
{
CombatManager.CombatRoomSm.OnRoomCleared += OnRoomCleared;
}
/// <summary>取消订阅 CombatRoomSm.OnRoomCleared。</summary>
private void UnsubscribeRoomEvents()
{
CombatManager.CombatRoomSm.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, // 结算画面
}
}