using System.Collections.Generic;
using Cielonos.MainGame.Map;
using SLSUtilities.UI;
using UnityEngine;
using UnityEngine.UI.Extensions;
namespace Cielonos.MainGame.UI
{
///
/// 地图 UI 页面。根据 RunMapData 动态生成节点和连接线元素。
/// 监听 RunManager 阶段变化,自动在 MapSelection 阶段打开/刷新,其他阶段关闭。
/// 节点点击后调用 RunManager.SelectNode 执行传送。
/// 支持通过 MapPanZoom 组件进行拖拽平移和滚轮缩放。
///
public class MapUIPage : UIPageBase
{
// ================================================================
// 常量
// ================================================================
private const float CONNECTION_LINE_THICKNESS = 3f;
private static readonly Color CONNECTION_LINE_COLOR = new Color(0.5f, 0.5f, 0.5f, 0.8f);
// ================================================================
// 序列化字段
// ================================================================
[Header("Containers")]
public RectTransform mapContainer;
[SerializeField] private RectTransform nodeContainer;
[SerializeField] private RectTransform connectionContainer;
[Header("Prefabs")]
[SerializeField] private GameObject nodePrefab;
[Header("Pan & Zoom")]
[SerializeField] private MapPanZoom panZoom;
// ================================================================
// 运行时状态
// ================================================================
/// 已生成的节点元素,按网格坐标索引。
private readonly Dictionary _nodeElements =
new Dictionary();
/// 已生成的连接线 GameObject 列表。
private readonly List _connectionObjects = new List();
/// 当前绑定的地图数据。
private RunMapData _mapData;
// ================================================================
// 生命周期
// ================================================================
protected override void Start()
{
base.Start();
// 订阅 RunManager 阶段切换事件,自动控制地图页面的开关
if (RunManager.Instance != null)
{
RunManager.Instance.OnPhaseChanged += OnRunPhaseChanged;
}
else
{
Debug.LogWarning("[MapUIPage] RunManager.Instance 不存在,无法自动切换地图页面。");
}
}
private void OnDestroy()
{
// 取消订阅,防止 RunManager 被先销毁时的空引用
if (RunManager.Instance != null)
{
RunManager.Instance.OnPhaseChanged -= OnRunPhaseChanged;
}
}
// ================================================================
// 阶段切换响应
// ================================================================
///
/// RunManager 阶段切换回调。
/// 离开 MapSelection 阶段时自动关闭地图;打开由玩家手动操作(地图按钮)。
///
private void OnRunPhaseChanged(RunPhase phase)
{
if (phase != RunPhase.MapSelection && IsOpen)
{
Close();
}
}
///
/// 页面打开时自动刷新节点显示状态并居中到当前节点。
///
protected override void OnPageOpened()
{
base.OnPageOpened();
if (RunManager.Instance != null && RunManager.Instance.currentRun != null)
{
RefreshDisplayStates(RunManager.Instance.currentRun);
CenterOnCurrentNode();
}
}
// ================================================================
// 公共 API
// ================================================================
///
/// 根据 RunMapData 生成所有地图 UI 元素(节点 + 连接线)。
/// 若之前已生成则先清除。生成完成后自动应用初始显示状态。
///
/// 要显示的地图数据。
public void Populate(RunMapData mapData)
{
if (mapData == null)
{
Debug.LogWarning("[MapUIPage] 传入的 RunMapData 为 null,无法生成地图。");
return;
}
_mapData = mapData;
ClearAll();
CreateNodes(mapData);
CreateConnections(mapData);
// 生成完成后立即应用初始显示状态(避免短暂显示原始外观)
if (RunManager.Instance != null && RunManager.Instance.currentRun != null)
{
RefreshDisplayStates(RunManager.Instance.currentRun);
}
}
///
/// 根据 RunState 更新所有节点的显示状态(迷雾、交互、使用状态、边框颜色)。
///
/// 当前 Run 的状态。
public void RefreshDisplayStates(RunState state)
{
Dictionary displayStates = MapFogCalculator.Calculate(state);
foreach (KeyValuePair kvp in displayStates)
{
if (_nodeElements.TryGetValue(kvp.Key, out MapNodeElement element))
{
element.ApplyDisplayState(kvp.Value);
}
}
// 更新连接线的可见性(所有连接线始终可见,因为所有节点至少显示为 Silhouette)
RefreshConnectionVisibility(displayStates);
}
// ================================================================
// 节点点击处理
// ================================================================
///
/// 节点被玩家点击后的回调。调用 RunManager.SelectNode 执行传送逻辑。
/// RunManager 内部会验证可达性并处理阶段切换和场景加载。
///
/// 被点击节点的网格坐标。
private void OnNodeClicked(Vector2Int gridPosition)
{
if (RunManager.Instance == null)
{
Debug.LogWarning("[MapUIPage] RunManager 不存在,无法处理节点点击。");
return;
}
//Debug.Log($"[MapUIPage] 玩家点击节点 {gridPosition},请求传送。");
RunManager.Instance.SelectNode(gridPosition);
}
// ================================================================
// 视角控制
// ================================================================
///
/// 将地图视角居中到玩家当前所在节点的 UI 位置。
///
private void CenterOnCurrentNode()
{
if (panZoom == null || _mapData == null) return;
if (RunManager.Instance == null || RunManager.Instance.currentRun == null) return;
Vector2Int currentPos = RunManager.Instance.currentRun.currentPosition;
if (_mapData.nodes.TryGetValue(currentPos, out RunMapNode node))
{
panZoom.CenterOn(node.position);
}
}
// ================================================================
// 节点生成
// ================================================================
///
/// 为每个 RunMapNode 实例化一个节点 UI 元素,并绑定点击事件。
///
private void CreateNodes(RunMapData mapData)
{
if (nodePrefab == null)
{
Debug.LogError("[MapUIPage] nodePrefab 未赋值,无法生成节点。");
return;
}
Transform parent = nodeContainer != null ? nodeContainer : mapContainer;
foreach (KeyValuePair kvp in mapData.nodes)
{
RunMapNode node = kvp.Value;
GameObject nodeGo = Instantiate(nodePrefab, parent);
nodeGo.name = $"Node_{node.gridPosition.x}_{node.gridPosition.y}";
MapNodeElement element = nodeGo.GetComponent();
if (element == null)
{
Debug.LogWarning($"[MapUIPage] nodePrefab 缺少 MapNodeElement 组件,跳过节点 {node.gridPosition}。");
Destroy(nodeGo);
continue;
}
element.Setup(node);
// 绑定点击事件 → OnNodeClicked → RunManager.SelectNode
element.OnClicked += OnNodeClicked;
_nodeElements[kvp.Key] = element;
}
}
// ================================================================
// 连接线生成
// ================================================================
///
/// 为所有节点之间的连接创建 UILineRenderer。
/// 使用 HashSet 去重,确保双向连接只生成一条线。
///
private void CreateConnections(RunMapData mapData)
{
Transform parent = connectionContainer != null ? connectionContainer : mapContainer;
HashSet processedEdges = new HashSet();
foreach (KeyValuePair kvp in mapData.nodes)
{
Vector2Int fromPos = kvp.Key;
RunMapNode fromNode = kvp.Value;
foreach (Vector2Int toPos in fromNode.connectedPositions)
{
// 使用排序后的 hash 去重双向边
long edgeKey = GetEdgeKey(fromPos, toPos);
if (!processedEdges.Add(edgeKey)) continue;
// 确保目标节点存在
if (!mapData.nodes.TryGetValue(toPos, out RunMapNode toNode)) continue;
CreateConnectionLine(parent, fromNode.position, toNode.position, fromPos, toPos);
}
}
}
///
/// 创建一条从 startPos 到 endPos 的 UILineRenderer 连接线。
///
private void CreateConnectionLine(Transform parent, Vector2 startUIPos, Vector2 endUIPos,
Vector2Int fromGrid, Vector2Int toGrid)
{
GameObject lineGo = new GameObject($"Connection_{fromGrid.x},{fromGrid.y}_to_{toGrid.x},{toGrid.y}");
lineGo.transform.SetParent(parent, false);
RectTransform rt = lineGo.AddComponent();
// 锚定到容器中心,不拉伸
rt.anchorMin = new Vector2(0.5f, 0.5f);
rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
rt.anchoredPosition = Vector2.zero;
rt.sizeDelta = mapContainer != null ? mapContainer.sizeDelta : new Vector2(1200f, 675f);
UILineRenderer lineRenderer = lineGo.AddComponent();
lineRenderer.LineThickness = CONNECTION_LINE_THICKNESS;
lineRenderer.color = CONNECTION_LINE_COLOR;
lineRenderer.raycastTarget = false;
// UILineRenderer 使用相对于自身 RectTransform 的坐标
// 由于 RectTransform 的 pivot 在 (0.5, 0.5),节点的 position 是相对于 mapContainer 中心的坐标
lineRenderer.Points = new Vector2[] { startUIPos, endUIPos };
_connectionObjects.Add(lineGo);
}
///
/// 生成一条边的唯一标识 key(与方向无关)。
///
private static long GetEdgeKey(Vector2Int a, Vector2Int b)
{
// 确保小的坐标在前,保证 (a→b) 和 (b→a) 生成相同的 key
Vector2Int min, max;
if (a.x < b.x || (a.x == b.x && a.y < b.y))
{
min = a;
max = b;
}
else
{
min = b;
max = a;
}
// 将两个 Vector2Int 打包为 long
// 每个坐标分量占 16 位,总共 64 位
return ((long)(min.x + 32768) << 48)
| ((long)(min.y + 32768) << 32)
| ((long)(max.x + 32768) << 16)
| (long)(max.y + 32768);
}
// ================================================================
// 连接线可见性
// ================================================================
///
/// 根据节点的显示状态更新连接线的可见性。
/// 两端节点都隐藏时,连接线也隐藏。
///
private void RefreshConnectionVisibility(Dictionary displayStates)
{
foreach (GameObject connectionGo in _connectionObjects)
{
if (connectionGo == null) continue;
// 从命名中解析连接的两个节点坐标
if (!TryParseConnectionName(connectionGo.name, out Vector2Int fromPos, out Vector2Int toPos))
continue;
bool fromVisible = displayStates.TryGetValue(fromPos, out NodeDisplayState fromState)
&& fromState.visibility != MapNodeVisibility.Hidden;
bool toVisible = displayStates.TryGetValue(toPos, out NodeDisplayState toState)
&& toState.visibility != MapNodeVisibility.Hidden;
connectionGo.SetActive(fromVisible || toVisible);
}
}
///
/// 从连接线 GameObject 名称中解析两端节点坐标。
/// 命名格式:Connection_{x1},{y1}_to_{x2},{y2}
///
private static bool TryParseConnectionName(string name, out Vector2Int from, out Vector2Int to)
{
from = Vector2Int.zero;
to = Vector2Int.zero;
// Connection_{x1},{y1}_to_{x2},{y2}
if (!name.StartsWith("Connection_")) return false;
string body = name.Substring("Connection_".Length);
string[] parts = body.Split(new[] { "_to_" }, System.StringSplitOptions.None);
if (parts.Length != 2) return false;
return TryParseCoord(parts[0], out from) && TryParseCoord(parts[1], out to);
}
private static bool TryParseCoord(string s, out Vector2Int result)
{
result = Vector2Int.zero;
string[] components = s.Split(',');
if (components.Length != 2) return false;
if (!int.TryParse(components[0], out int x)) return false;
if (!int.TryParse(components[1], out int y)) return false;
result = new Vector2Int(x, y);
return true;
}
// ================================================================
// 清理
// ================================================================
///
/// 清除所有已生成的节点和连接线。
///
private void ClearAll()
{
ClearNodes();
ClearConnections();
}
private void ClearNodes()
{
foreach (KeyValuePair kvp in _nodeElements)
{
if (kvp.Value != null)
{
// 取消订阅点击事件,防止内存泄漏
kvp.Value.OnClicked -= OnNodeClicked;
Destroy(kvp.Value.gameObject);
}
}
_nodeElements.Clear();
}
private void ClearConnections()
{
foreach (GameObject go in _connectionObjects)
{
if (go != null) Destroy(go);
}
_connectionObjects.Clear();
}
}
}