角色能力,存档

This commit is contained in:
2026-05-19 11:50:21 +08:00
parent d25f237e76
commit 2dcb7a961a
136 changed files with 36035 additions and 27551 deletions

View File

@@ -1,11 +1,18 @@
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
{
/// <summary>房间可见性三级状态:未知 / 已踏入 / 已标注(购买地图碎片)。</summary>
public enum RoomVisibility { Unknown, Explored, Mapped }
/// <summary>
/// 全屏地图 UI 面板(架构 15_MapShopModule §1.3)。
/// 由 UIManager PanelStack 管理开关OnEnable 时重建格子并订阅更新事件。
@@ -13,8 +20,10 @@ namespace BaseGames.World.Map
public class MapPanel : MonoBehaviour
{
[SerializeField] private MapDatabaseSO _database;
[SerializeField] private RectTransform _roomContainer; // 格子图放置根节点
[SerializeField] private MapRoomCellUI _cellPrefab; // 地图格子预制
[SerializeField] private RectTransform _roomContainer; // 格子图放置根节点
[SerializeField] private MapRoomCellUI _cellPrefab; // 地图格子预制
[SerializeField] private Image _exitConnectorPrefab; // 出口连接线预制(小矩形 Image
[SerializeField] private ScrollRect _scrollRect; // 包裹 _roomContainer 的滚动矩形(可空,无滚动时留空)
[Header("图标 Sprites")]
[SerializeField] private Sprite _iconSavePoint;
@@ -23,73 +32,241 @@ namespace BaseGames.World.Map
[SerializeField] private Sprite _iconPlayerPos;
[Header("颜色")]
[SerializeField] private Color _colorDiscovered = Color.white;
[SerializeField] private Color _colorUndiscovered = Color.black;
[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; // 房间发现时刷新
[SerializeField] private StringEventChannelSO _onMapUpdated; // 房间发现/标注时刷新
private Dictionary<string, MapRoomCellUI> _cells = new();
private readonly CompositeDisposable _subs = new();
private Dictionary<string, MapRoomCellUI> _cells = new();
private List<Image> _pinImages = new();
private List<Image> _exitImages = new();
private string _highlightedRoomId;
private readonly CompositeDisposable _subs = new();
private void OnEnable()
{
// 首次打开时建立格子;后续打开只刷新探索状态,跳过 N 次 Instantiate
// 首次打开时建立格子;后续打开只刷新探索状态,跳过重复 Instantiate
if (_cells.Count == 0)
BuildGrid();
else
RefreshAllCells();
RenderPins();
UpdatePlayerIcon();
CenterOnCurrentRoom();
_onMapUpdated?.Subscribe(OnMapUpdated).AddTo(_subs);
}
private void OnDisable()
{
_subs.Clear();
// 格子保留,不销毁——随面板 GameObject 一同隐藏
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()
{
var mapManager = ServiceLocator.GetOrDefault<IMapService>();
var svc = ServiceLocator.GetOrDefault<IMapService>();
foreach (var (roomId, cell) in _cells)
{
if (cell == null) continue;
bool discovered = mapManager != null && mapManager.IsExplored(roomId);
cell.SetDiscovered(discovered);
cell.SetVisibility(GetVisibility(svc, roomId));
}
}
// ── 内部 ──────────────────────────────────────────────────────────────
// ── 格子 & 出口连接 ──────────────────────────────────────────────────
private void BuildGrid()
{
if (_database == null || _database.AllRooms == null) return;
var mapManager = ServiceLocator.GetOrDefault<IMapService>();
if (_database?.AllRooms == null) return;
var svc = ServiceLocator.GetOrDefault<IMapService>();
foreach (var room in _database.AllRooms)
{
if (room == null) continue;
var cell = Instantiate(_cellPrefab, _roomContainer);
bool discovered = mapManager != null && mapManager.IsExplored(room.RoomId);
cell.Setup(room, discovered, ChooseIcon(room));
cell.Setup(room, GetVisibility(svc, room.RoomId), ChooseIcon(room),
ShowTooltip, HideTooltip);
_cells[room.RoomId] = cell;
}
DrawExits();
}
/// <summary>为每条出口在格子坐标处实例化一个小矩形连接线图像。</summary>
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)
{
var svc = ServiceLocator.GetOrDefault<IMapService>();
if (_cells.TryGetValue(roomId, out var cell))
cell.SetDiscovered(true);
cell.SetVisibility(GetVisibility(svc, 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<RectTransform>();
_playerIconImg.sprite = _iconPlayerPos;
_playerIconImg.enabled = true;
_playerIconImg.rectTransform.anchoredPosition =
cellRT.anchoredPosition
+ Vector2.Scale(_playerTracker.NormalizedPositionInRoom, cellRT.sizeDelta);
UpdateCellHighlight(roomId);
}
// ── 当前房间高亮 & ScrollRect 居中 ──────────────────────────────────────
/// <summary>切换高亮描边:取消旧房间高亮,激活新房间高亮。</summary>
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);
}
/// <summary>面板打开时将 ScrollRect 视口居中到玩家当前所在房间。</summary>
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<RectTransform>();
// 将 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<RectTransform>();
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);
// ── 辅助方法 ──────────────────────────────────────────────────────────
/// <summary>按优先级推导可见性Explored > Mapped > Unknown。</summary>
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)
@@ -100,34 +277,63 @@ namespace BaseGames.World.Map
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 ─────────────────────────────────────────────────────────
/// <summary>地图面板中每个房间对应的格子 UI 组件。</summary>
public class MapRoomCellUI : MonoBehaviour
public class MapRoomCellUI : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
[SerializeField] private Image _bg;
[SerializeField] private Image _icon;
[SerializeField] private Image _bg;
[SerializeField] private Image _icon;
[SerializeField] private RawImage _outlineImage; // 可选:房间非矩形轮廓纹理
[SerializeField] private Image _highlight; // 可选:当前房间高亮描边(玩家所在时激活)
private static readonly Color Discovered = Color.white;
private static readonly Color Undiscovered = Color.black;
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;
/// <summary>初始化格子(位置、颜色、图标)。</summary>
public void Setup(MapRoomDataSO room, bool discovered, Sprite icon)
private string _displayName;
private Action<string> _onHover;
private Action _onHoverExit;
/// <summary>初始化格子位置、可见性、图标、Tooltip 回调)。</summary>
public void Setup(MapRoomDataSO room, RoomVisibility visibility, Sprite icon,
Action<string> onHover = null, Action onHoverExit = null)
{
// 根据 GridPosition/GridSize 设置 RectTransform 位置与大小
_displayName = room.DisplayName;
_onHover = onHover;
_onHoverExit = onHoverExit;
if (TryGetComponent<RectTransform>(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);
rt.anchoredPosition = new Vector2(room.GridPosition.x * 32f, room.GridPosition.y * 32f);
rt.sizeDelta = new Vector2(room.GridSize.x * 32f, room.GridSize.y * 32f);
}
SetDiscovered(discovered);
// 房间轮廓纹理(非矩形形状,覆盖在矩形背景上方)
if (_outlineImage != null)
{
_outlineImage.texture = room.RoomOutlineTex;
_outlineImage.enabled = room.RoomOutlineTex != null;
}
SetVisibility(visibility);
if (_icon != null)
{
@@ -136,9 +342,32 @@ namespace BaseGames.World.Map
}
}
public void SetDiscovered(bool v)
public void SetVisibility(RoomVisibility v)
{
if (_bg != null) _bg.color = v ? Discovered : Undiscovered;
if (_bg == null) return;
_bg.color = v switch
{
RoomVisibility.Explored => ColExplored,
RoomVisibility.Mapped => ColMapped,
_ => ColUnknown,
};
}
/// <summary>向后兼容:直接传 bool 时等同于 Explored / Unknown。</summary>
public void SetDiscovered(bool v)
=> SetVisibility(v ? RoomVisibility.Explored : RoomVisibility.Unknown);
/// <summary>激活/取消当前房间高亮描边。</summary>
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();
}
}