using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using TMPro; using BaseGames.Core; using BaseGames.Core.Events; using BaseGames.Core.Save; namespace BaseGames.World.Map { /// 房间可见性三级状态:未知 / 已踏入 / 已标注(购买地图碎片)。 public enum RoomVisibility { Unknown, Explored, Mapped } /// /// 全屏地图 UI 面板(架构 15_MapShopModule §1.3)。 /// 由 UIManager PanelStack 管理开关;OnEnable 时重建格子并订阅更新事件。 /// public class MapPanel : MonoBehaviour { [SerializeField] private MapDatabaseSO _database; [SerializeField] private RectTransform _roomContainer; // 格子图放置根节点 [SerializeField] private MapRoomCellUI _cellPrefab; // 地图格子预制 [SerializeField] private Image _exitConnectorPrefab; // 出口连接线预制(小矩形 Image) [SerializeField] private ScrollRect _scrollRect; // 包裹 _roomContainer 的滚动矩形(可空,无滚动时留空) [Header("图标 Sprites")] [SerializeField] private Sprite _iconSavePoint; [SerializeField] private Sprite _iconBossRoom; [SerializeField] private Sprite _iconShop; [SerializeField] private Sprite _iconPlayerPos; [Header("颜色")] [SerializeField] private Color _colorExplored = Color.white; [SerializeField] private Color _colorMapped = new Color(0.45f, 0.45f, 0.45f, 1f); [SerializeField] private Color _colorUnknown = Color.black; [Header("玩家位置")] [SerializeField] private MapPlayerTracker _playerTracker; // 挂在 Player 上的追踪器 [SerializeField] private Image _playerIconImg; // _roomContainer 内的玩家图标 [Header("地图标记")] [SerializeField] private Image _pinPrefab; [SerializeField] private PinSpriteEntry[] _pinSprites; // PinType → 对应 Sprite [SerializeField] private MapPinManager _pinManager; [Header("Tooltip")] [SerializeField] private GameObject _tooltipPanel; [SerializeField] private TMP_Text _tooltipText; [Header("Event Channels")] [SerializeField] private StringEventChannelSO _onMapUpdated; // 房间发现/标注时刷新 private Dictionary _cells = new(); private List _pinImages = new(); private List _exitImages = new(); private string _highlightedRoomId; private IMapService _mapSvc; // 面板活跃期间缓存,避免高频 ServiceLocator 查询 private readonly CompositeDisposable _subs = new(); private void OnEnable() { _mapSvc = ServiceLocator.GetOrDefault(); // 首次打开时建立格子;后续打开只刷新探索状态,跳过重复 Instantiate if (_cells.Count == 0) BuildGrid(); else RefreshAllCells(); RenderPins(); UpdatePlayerIcon(); CenterOnCurrentRoom(); _onMapUpdated?.Subscribe(OnMapUpdated).AddTo(_subs); } private void OnDisable() { _subs.Clear(); _mapSvc = null; HideTooltip(); } private void OnDestroy() { foreach (var cell in _cells.Values) if (cell != null) Destroy(cell.gameObject); _cells.Clear(); ClearPins(); ClearExits(); } private void LateUpdate() { UpdatePlayerIcon(); } // 面板重新打开时同步关闭期间积累的探索进度 private void RefreshAllCells() { foreach (var (roomId, cell) in _cells) { if (cell == null) continue; cell.SetVisibility(GetVisibility(_mapSvc, roomId)); } } // ── 格子 & 出口连接 ────────────────────────────────────────────────── private void BuildGrid() { if (_database?.AllRooms == null) return; foreach (var room in _database.AllRooms) { if (room == null) continue; var cell = Instantiate(_cellPrefab, _roomContainer); cell.Setup(room, GetVisibility(_mapSvc, room.RoomId), ChooseIcon(room), ShowTooltip, HideTooltip); _cells[room.RoomId] = cell; } DrawExits(); } /// 为每条出口在格子坐标处实例化一个小矩形连接线图像。 private void DrawExits() { if (_exitConnectorPrefab == null || _database?.AllRooms == null) return; ClearExits(); const float px = 32f; foreach (var room in _database.AllRooms) { if (room?.Exits == null) continue; foreach (var exit in room.Exits) { var conn = Instantiate(_exitConnectorPrefab, _roomContainer); conn.rectTransform.anchoredPosition = new Vector2(exit.ExitGridPos.x * px, exit.ExitGridPos.y * px); bool vertical = exit.Direction == ExitDirection.Up || exit.Direction == ExitDirection.Down; conn.rectTransform.sizeDelta = vertical ? new Vector2(16f, 8f) : new Vector2(8f, 16f); _exitImages.Add(conn); } } } private void ClearExits() { foreach (var img in _exitImages) if (img != null) Destroy(img.gameObject); _exitImages.Clear(); } private void OnMapUpdated(string roomId) { if (_cells.TryGetValue(roomId, out var cell)) cell.SetVisibility(GetVisibility(_mapSvc, roomId)); } // ── 玩家位置图标 ────────────────────────────────────────────────────── private void UpdatePlayerIcon() { if (_playerIconImg == null || _playerTracker == null) return; var roomId = _playerTracker.CurrentRoomId; if (string.IsNullOrEmpty(roomId) || !_cells.TryGetValue(roomId, out var cell)) { _playerIconImg.enabled = false; UpdateCellHighlight(null); return; } var cellRT = cell.GetComponent(); _playerIconImg.sprite = _iconPlayerPos; _playerIconImg.enabled = true; _playerIconImg.rectTransform.anchoredPosition = cellRT.anchoredPosition + Vector2.Scale(_playerTracker.NormalizedPositionInRoom, cellRT.sizeDelta); UpdateCellHighlight(roomId); } // ── 当前房间高亮 & ScrollRect 居中 ────────────────────────────────────── /// 切换高亮描边:取消旧房间高亮,激活新房间高亮。 private void UpdateCellHighlight(string roomId) { if (roomId == _highlightedRoomId) return; if (_highlightedRoomId != null && _cells.TryGetValue(_highlightedRoomId, out var prev)) prev.SetHighlight(false); _highlightedRoomId = roomId; if (roomId != null && _cells.TryGetValue(roomId, out var next)) next.SetHighlight(true); } /// 面板打开时将 ScrollRect 视口居中到玩家当前所在房间。 private void CenterOnCurrentRoom() { if (_scrollRect == null || _playerTracker == null) return; var roomId = _playerTracker.CurrentRoomId; if (string.IsNullOrEmpty(roomId) || !_cells.TryGetValue(roomId, out var cell)) return; Canvas.ForceUpdateCanvases(); var content = _scrollRect.content; var viewport = _scrollRect.viewport != null ? _scrollRect.viewport : (RectTransform)_scrollRect.transform; var cellRT = cell.GetComponent(); // 将 cell 中心转换到 content 本地坐标系 Vector2 cellWorldCenter = cellRT.TransformPoint(cellRT.rect.center); Vector2 cellLocal = content.InverseTransformPoint(cellWorldCenter); // 距 content 左下角的距离(pivot 无关) float cellX = cellLocal.x - content.rect.xMin; float cellY = cellLocal.y - content.rect.yMin; Vector2 viewSize = viewport.rect.size; Vector2 contentSize = content.rect.size; float rangeX = contentSize.x - viewSize.x; float rangeY = contentSize.y - viewSize.y; float normX = rangeX > 0 ? Mathf.Clamp01((cellX - viewSize.x * 0.5f) / rangeX) : 0.5f; float normY = rangeY > 0 ? Mathf.Clamp01((cellY - viewSize.y * 0.5f) / rangeY) : 0.5f; _scrollRect.normalizedPosition = new Vector2(normX, normY); } // ── 地图标记渲染 ────────────────────────────────────────────────────── private void RenderPins() { ClearPins(); if (_pinPrefab == null || _pinManager == null) return; foreach (var pin in _pinManager.Pins) { if (!_cells.TryGetValue(pin.RoomId, out var cell)) continue; var img = Instantiate(_pinPrefab, _roomContainer); img.sprite = GetPinSprite((PinType)pin.PinTypeInt); var cellRT = cell.GetComponent(); img.rectTransform.anchoredPosition = cellRT.anchoredPosition + new Vector2( pin.NormalizedPosX * cellRT.sizeDelta.x, pin.NormalizedPosY * cellRT.sizeDelta.y); _pinImages.Add(img); } } private void ClearPins() { foreach (var img in _pinImages) if (img != null) Destroy(img.gameObject); _pinImages.Clear(); } // ── Tooltip ─────────────────────────────────────────────────────────── private void ShowTooltip(string text) { if (_tooltipPanel == null || string.IsNullOrEmpty(text)) return; if (_tooltipText != null) _tooltipText.text = text; _tooltipPanel.SetActive(true); } private void HideTooltip() => _tooltipPanel?.SetActive(false); // ── 辅助方法 ────────────────────────────────────────────────────────── /// 按优先级推导可见性:Explored > Mapped > Unknown。 private static RoomVisibility GetVisibility(IMapService svc, string roomId) { if (svc == null) return RoomVisibility.Unknown; if (svc.IsExplored(roomId)) return RoomVisibility.Explored; if (svc.IsMapped(roomId)) return RoomVisibility.Mapped; return RoomVisibility.Unknown; } private Sprite ChooseIcon(MapRoomDataSO room) { if (room.MapIconOverride != null) return room.MapIconOverride; if (room.IsSavePoint) return _iconSavePoint; if (room.IsBossRoom) return _iconBossRoom; if (room.IsShop) return _iconShop; return null; } private Sprite GetPinSprite(PinType type) { if (_pinSprites != null) foreach (var e in _pinSprites) if (e.PinType == type) return e.Sprite; return null; } } [Serializable] public struct PinSpriteEntry { public PinType PinType; public Sprite Sprite; } // ─── 单个地图格子 UI ───────────────────────────────────────────────────────── /// 地图面板中每个房间对应的格子 UI 组件。 public class MapRoomCellUI : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler { [SerializeField] private Image _bg; [SerializeField] private Image _icon; [SerializeField] private RawImage _outlineImage; // 可选:房间非矩形轮廓纹理 [SerializeField] private Image _highlight; // 可选:当前房间高亮描边(玩家所在时激活) private static readonly Color ColExplored = Color.white; private static readonly Color ColMapped = new Color(0.45f, 0.45f, 0.45f, 1f); private static readonly Color ColUnknown = Color.black; private string _displayName; private Action _onHover; private Action _onHoverExit; /// 初始化格子(位置、可见性、图标、Tooltip 回调)。 public void Setup(MapRoomDataSO room, RoomVisibility visibility, Sprite icon, Action onHover = null, Action onHoverExit = null) { _displayName = room.DisplayName; _onHover = onHover; _onHoverExit = onHoverExit; if (TryGetComponent(out var rt)) { rt.anchoredPosition = new Vector2(room.GridPosition.x * 32f, room.GridPosition.y * 32f); rt.sizeDelta = new Vector2(room.GridSize.x * 32f, room.GridSize.y * 32f); } // 房间轮廓纹理(非矩形形状,覆盖在矩形背景上方) if (_outlineImage != null) { _outlineImage.texture = room.RoomOutlineTex; _outlineImage.enabled = room.RoomOutlineTex != null; } SetVisibility(visibility); if (_icon != null) { _icon.sprite = icon; _icon.enabled = icon != null; } } public void SetVisibility(RoomVisibility v) { if (_bg == null) return; _bg.color = v switch { RoomVisibility.Explored => ColExplored, RoomVisibility.Mapped => ColMapped, _ => ColUnknown, }; } /// 向后兼容:直接传 bool 时等同于 Explored / Unknown。 public void SetDiscovered(bool v) => SetVisibility(v ? RoomVisibility.Explored : RoomVisibility.Unknown); /// 激活/取消当前房间高亮描边。 public void SetHighlight(bool v) { if (_highlight != null) _highlight.enabled = v; } public void OnPointerEnter(PointerEventData _) { if (!string.IsNullOrEmpty(_displayName)) _onHover?.Invoke(_displayName); } public void OnPointerExit(PointerEventData _) => _onHoverExit?.Invoke(); } }