236 lines
8.9 KiB
C#
236 lines
8.9 KiB
C#
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;
|
||
|
||
// 规则 2:Boss 节点始终完全可见
|
||
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;
|
||
}
|
||
}
|
||
}
|