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(); } } }