Files
Cielonos/Assets/Scripts/MainGame/GameRun/Map/MapFogCalculator.cs
2026-05-10 11:47:55 -04:00

236 lines
8.9 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.Collections.Generic;
using UnityEngine;
namespace Cielonos.MainGame.Map
{
// ================================================================
// 节点显示状态(三个正交维度)
// ================================================================
/// <summary>节点对玩家的可见程度。</summary>
public enum MapNodeVisibility
{
Hidden, // 完全隐藏(雾中,不渲染)
Silhouette, // 显示轮廓和"?",不显示具体类型
Revealed, // 完全可见,显示节点类型图标
}
/// <summary>节点的交互状态(是否可点击传送)。</summary>
public enum MapNodeInteraction
{
Locked, // 可见但不可点击(超出移动范围,或尚未探索到的区域)
Selectable, // 可点击传送在已访问节点的1格范围内或本身已被访问
Current, // 玩家当前所在节点
}
/// <summary>节点的使用状态(用于视觉区分)。</summary>
public enum MapNodeUsage
{
Fresh, // 未访问过
Visited, // 已访问(战斗已清/已进入过,可再次进入但不触发效果)
Exhausted, // 单次使用节点已用完MedicalStation、MechanicalTable
}
/// <summary>
/// 单个节点的完整显示状态,包含可见性、交互性和使用状态三个维度。
/// </summary>
public struct NodeDisplayState
{
public MapNodeVisibility visibility;
public MapNodeInteraction interaction;
public MapNodeUsage usage;
}
/// <summary>
/// 地图迷雾计算器。根据 RunState 计算每个节点的显示状态。
/// 纯静态工具类不持有状态UI 层直接调用即可。
/// </summary>
public static class MapFogCalculator
{
/// <summary>
/// 计算地图中所有节点的显示状态。
/// </summary>
/// <param name="state">当前 Run 的状态。</param>
/// <returns>每个节点坐标对应的显示状态字典。</returns>
public static Dictionary<Vector2Int, NodeDisplayState> Calculate(RunState state)
{
Dictionary<Vector2Int, NodeDisplayState> result =
new Dictionary<Vector2Int, NodeDisplayState>();
RunMapData mapData = state.mapData;
int scoutRange = state.scoutRange;
// 从所有已访问节点出发,做多源 BFS计算每个节点到最近已访问节点的距离
// 只需要探测到 scoutRange + 1 深度Silhouette 需要 +1
Dictionary<Vector2Int, int> distFromVisited =
MultiSourceBfs(mapData, state.visitedNodes, scoutRange + 1);
// 计算可传送集合:所有已访问节点 + 已访问节点的1格邻居
HashSet<Vector2Int> selectableSet = ComputeSelectableSet(state);
foreach (KeyValuePair<Vector2Int, RunMapNode> kvp in mapData.nodes)
{
Vector2Int pos = kvp.Key;
RunMapNode node = kvp.Value;
NodeDisplayState displayState = new NodeDisplayState
{
visibility = ComputeVisibility(pos, node, state, distFromVisited, scoutRange),
interaction = ComputeInteraction(pos, state, selectableSet),
usage = ComputeUsage(pos, state),
};
result[pos] = displayState;
}
return result;
}
/// <summary>
/// 计算可传送节点集合:所有已访问节点 + 已访问节点的1格邻居不含当前位置
/// </summary>
public static HashSet<Vector2Int> ComputeSelectableSet(RunState state)
{
HashSet<Vector2Int> result = new HashSet<Vector2Int>();
foreach (Vector2Int visitedPos in state.visitedNodes)
{
// 已访问节点本身可再次传送
result.Add(visitedPos);
// 已访问节点的直接邻居也可传送(新领域扩展)
if (state.mapData.nodes.TryGetValue(visitedPos, out RunMapNode node))
{
foreach (Vector2Int neighbor in node.connectedPositions)
result.Add(neighbor);
}
}
// 当前位置不算"可选"——玩家已经在那了
result.Remove(state.currentPosition);
return result;
}
// ================================================================
// 内部计算方法
// ================================================================
/// <summary>计算单个节点的可见性。</summary>
private static MapNodeVisibility ComputeVisibility(
Vector2Int pos,
RunMapNode node,
RunState state,
Dictionary<Vector2Int, int> distFromVisited,
int scoutRange)
{
// 规则 1已访问的节点始终完全可见
if (state.visitedNodes.Contains(pos))
return MapNodeVisibility.Revealed;
// 规则 2Boss 节点始终完全可见
if (pos == state.mapData.bossPosition)
return MapNodeVisibility.Revealed;
// 规则 3被道具永久揭示的特定节点
if (state.permanentlyRevealedNodes.Contains(pos))
return MapNodeVisibility.Revealed;
// 规则 4被道具永久揭示的节点类型如"显示所有商店"
if (state.permanentlyRevealedTypes.Contains(node.nodeType))
return MapNodeVisibility.Revealed;
// 规则 5在探测范围内BFS 距离 <= scoutRange→ Revealed
// 在探测范围+1 → Silhouette"?"
// 超出 → Hidden
if (distFromVisited.TryGetValue(pos, out int dist))
{
if (dist <= scoutRange)
return MapNodeVisibility.Revealed;
if (dist <= scoutRange + 1)
return MapNodeVisibility.Silhouette;
}
// 所有节点始终可见,超出探测范围的显示为 Silhouette"?"
return MapNodeVisibility.Silhouette;
}
/// <summary>计算单个节点的交互状态。</summary>
private static MapNodeInteraction ComputeInteraction(
Vector2Int pos,
RunState state,
HashSet<Vector2Int> selectableSet)
{
if (pos == state.currentPosition)
return MapNodeInteraction.Current;
if (selectableSet.Contains(pos))
return MapNodeInteraction.Selectable;
return MapNodeInteraction.Locked;
}
/// <summary>计算单个节点的使用状态。</summary>
private static MapNodeUsage ComputeUsage(Vector2Int pos, RunState state)
{
if (state.exhaustedNodes.Contains(pos))
return MapNodeUsage.Exhausted;
if (state.visitedNodes.Contains(pos))
return MapNodeUsage.Visited;
return MapNodeUsage.Fresh;
}
// ================================================================
// 多源 BFS
// ================================================================
/// <summary>
/// 从多个起点同时出发,沿节点连接进行 BFS返回每个可达节点到最近起点的距离。
/// 仅探索到 maxDepth 深度,超出的节点不包含在结果中。
/// </summary>
private static Dictionary<Vector2Int, int> MultiSourceBfs(
RunMapData mapData,
HashSet<Vector2Int> sources,
int maxDepth)
{
Dictionary<Vector2Int, int> distances = new Dictionary<Vector2Int, int>();
Queue<Vector2Int> queue = new Queue<Vector2Int>();
// 所有源节点距离为 0
foreach (Vector2Int src in sources)
{
if (mapData.nodes.ContainsKey(src))
{
distances[src] = 0;
queue.Enqueue(src);
}
}
while (queue.Count > 0)
{
Vector2Int current = queue.Dequeue();
int currentDist = distances[current];
if (currentDist >= maxDepth)
continue;
if (!mapData.nodes.TryGetValue(current, out RunMapNode node))
continue;
foreach (Vector2Int neighbor in node.connectedPositions)
{
if (!distances.ContainsKey(neighbor) && mapData.nodes.ContainsKey(neighbor))
{
distances[neighbor] = currentDist + 1;
queue.Enqueue(neighbor);
}
}
}
return distances;
}
}
}