Files
Cielonos/Assets/Scripts/MainGame/GameRun/RunManager.cs

531 lines
21 KiB
C#
Raw Normal View History

2026-05-10 11:47:55 -04:00
using System;
using System.Collections.Generic;
using Cielonos.MainGame.Characters;
using Cielonos.MainGame.Map;
using Cielonos.MainGame.UI;
using Sirenix.OdinInspector;
using SLSUtilities.General;
2026-04-30 07:06:38 -04:00
using UnityEngine;
namespace Cielonos.MainGame
{
2026-05-10 11:47:55 -04:00
/// <summary>
/// 驱动整局 Roguelite Run 的会话管理器。
/// 负责地图生成、节点选择验证、阶段状态机切换和结算流程。
/// </summary>
2026-04-30 07:06:38 -04:00
public class RunManager : Singleton<RunManager>
{
2026-05-10 11:47:55 -04:00
// ----------------------------------------------------------------
// 配置
// ----------------------------------------------------------------
[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;
2026-05-23 08:27:50 -04:00
/// <summary>
/// 玩家进入区域时触发,携带完整上下文(坐标、节点数据、是否首次、是否已完成)。
/// 装备和子系统应优先订阅此事件以获取区域状态。
/// </summary>
public event Action<ZoneEntryContext> OnZoneEntered;
2026-05-10 11:47:55 -04:00
/// <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;
2026-05-23 08:27:50 -04:00
bool isFirstVisit = currentRun.visitedNodes.Add(targetPosition);
bool isCompleted = currentRun.completedNodes.Contains(targetPosition);
2026-05-10 11:47:55 -04:00
OnNodeSelected?.Invoke(targetPosition);
2026-05-23 08:27:50 -04:00
OnZoneEntered?.Invoke(new ZoneEntryContext(targetPosition, targetNode, isFirstVisit, isCompleted));
Debug.Log($"[RunManager] 移动到节点 {targetPosition},类型:{targetNode.nodeType}" +
$",首次进入:{isFirstVisit},已完成:{isCompleted}");
2026-05-10 11:47:55 -04:00
2026-05-23 08:27:50 -04:00
CombatManager.AttackAreaSm.DestroyAll();
2026-05-10 11:47:55 -04:00
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 已清理。");
}
// ================================================================
// 战斗房间事件
// ================================================================
2026-05-23 08:27:50 -04:00
/// <summary>订阅 CombatRoomSm.OnRoomCleared战斗房间清空后自动完成当前节点。</summary>
2026-05-10 11:47:55 -04:00
private void SubscribeRoomEvents()
{
2026-05-23 08:27:50 -04:00
CombatManager.CombatRoomSm.OnRoomCleared += OnRoomCleared;
2026-05-10 11:47:55 -04:00
}
2026-05-23 08:27:50 -04:00
/// <summary>取消订阅 CombatRoomSm.OnRoomCleared。</summary>
2026-05-10 11:47:55 -04:00
private void UnsubscribeRoomEvents()
{
2026-05-23 08:27:50 -04:00
CombatManager.CombatRoomSm.OnRoomCleared -= OnRoomCleared;
2026-05-10 11:47:55 -04:00
}
/// <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);
2026-05-23 08:27:50 -04:00
2026-05-10 11:47:55 -04:00
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;
2026-05-23 08:27:50 -04:00
//Debug.Log($"[RunManager] 阶段切换 → {newPhase}");
2026-05-10 11:47:55 -04:00
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, // 结算画面
2026-04-30 07:06:38 -04:00
}
2026-05-10 11:47:55 -04:00
}