地图系统
This commit is contained in:
209
Assets/_Game/Scripts/Inventory/InventoryManager.cs
Normal file
209
Assets/_Game/Scripts/Inventory/InventoryManager.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Inventory
|
||||
{
|
||||
/// <summary>
|
||||
/// 运行时背包管理器。
|
||||
/// 挂在 Persistent 场景 [Services] 下,事件驱动记录玩家持有的道具,
|
||||
/// 实现 <see cref="ISaveable"/> 持久化。设计对照 <see cref="BaseGames.World.Map.MapManager"/>
|
||||
/// 与 <see cref="BaseGames.Equipment.EquipmentManager"/> 的服务注册 / 存档模式。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-700)]
|
||||
public class InventoryManager : MonoBehaviour, ISaveable, IInventoryService
|
||||
{
|
||||
[SerializeField] private ItemDatabaseSO _database;
|
||||
|
||||
[Header("Event Channels - Listen")]
|
||||
[Tooltip("道具拾取(itemId)。Collectible 拾取 Item 类型时广播 EVT_ItemPickup。")]
|
||||
[SerializeField] private StringEventChannelSO _onItemPickup;
|
||||
|
||||
[Header("Event Channels - Raise")]
|
||||
[Tooltip("道具首次获得时广播(itemId),供 HUD Toast / 地图碎片揭示等订阅。")]
|
||||
[SerializeField] private StringEventChannelSO _onItemAcquired;
|
||||
[Tooltip("背包内容变化时广播(无负载),供 UI 轻量刷新。")]
|
||||
[SerializeField] private VoidEventChannelSO _onInventoryChanged;
|
||||
[Tooltip("地图碎片道具获得时广播揭示区域(regionId)。对应 EVT_RevealRegion。")]
|
||||
[SerializeField] private StringEventChannelSO _onRevealRegion;
|
||||
|
||||
private readonly Dictionary<string, int> _counts = new();
|
||||
private readonly HashSet<string> _newItems = new();
|
||||
private readonly List<InventoryEntry> _view = new();
|
||||
private bool _viewDirty = true;
|
||||
private bool _isDuplicate;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
public event Action OnInventoryChanged;
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
private void Awake()
|
||||
{
|
||||
if (ServiceLocator.GetOrDefault<IInventoryService>() != null) { _isDuplicate = true; Destroy(gameObject); return; }
|
||||
ServiceLocator.Register<IInventoryService>(this);
|
||||
Debug.Assert(_database != null, "[InventoryManager] _database 未赋值,请在 Inspector 中指定 ItemDatabaseSO。", this);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_isDuplicate) return;
|
||||
_onItemPickup?.Subscribe(OnItemPickup).AddTo(_subs);
|
||||
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_isDuplicate) return;
|
||||
_subs.Clear();
|
||||
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Unregister(this);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_isDuplicate) return;
|
||||
ServiceLocator.Unregister<IInventoryService>(this);
|
||||
}
|
||||
|
||||
// ── 事件驱动拾取 ──────────────────────────────────────────────────────
|
||||
private void OnItemPickup(string itemId) => AddItem(itemId, 1);
|
||||
|
||||
// ── IInventoryService ─────────────────────────────────────────────────
|
||||
public IReadOnlyList<InventoryEntry> Items
|
||||
{
|
||||
get { RebuildViewIfDirty(); return _view; }
|
||||
}
|
||||
|
||||
public int DistinctCount => _counts.Count;
|
||||
|
||||
public int GetCount(string itemId)
|
||||
=> !string.IsNullOrEmpty(itemId) && _counts.TryGetValue(itemId, out var c) ? c : 0;
|
||||
|
||||
public bool Has(string itemId) => GetCount(itemId) > 0;
|
||||
|
||||
public int AddItem(string itemId, int amount = 1)
|
||||
{
|
||||
if (string.IsNullOrEmpty(itemId) || amount <= 0) return 0;
|
||||
var item = _database != null ? _database.Find(itemId) : null;
|
||||
if (item == null) { Debug.LogWarning($"[InventoryManager] 找不到道具: {itemId}", this); return 0; }
|
||||
|
||||
bool firstTime = !_counts.ContainsKey(itemId);
|
||||
int current = firstTime ? 0 : _counts[itemId];
|
||||
int target;
|
||||
|
||||
if (!item.stackable)
|
||||
{
|
||||
if (current >= 1) return 0; // 非叠加道具已持有,忽略
|
||||
target = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
target = current + amount;
|
||||
if (item.maxStack > 0) target = Mathf.Min(target, item.maxStack);
|
||||
}
|
||||
|
||||
int added = target - current;
|
||||
if (added <= 0) return 0;
|
||||
|
||||
_counts[itemId] = target;
|
||||
_newItems.Add(itemId);
|
||||
_viewDirty = true;
|
||||
|
||||
if (firstTime)
|
||||
{
|
||||
_onItemAcquired?.Raise(itemId);
|
||||
// 地图碎片:揭示对应区域
|
||||
if (item.category == ItemCategory.MapShard && !string.IsNullOrEmpty(item.revealRegionId))
|
||||
_onRevealRegion?.Raise(item.revealRegionId);
|
||||
}
|
||||
|
||||
RaiseChanged();
|
||||
return added;
|
||||
}
|
||||
|
||||
public int RemoveItem(string itemId, int amount = 1)
|
||||
{
|
||||
if (string.IsNullOrEmpty(itemId) || amount <= 0) return 0;
|
||||
if (!_counts.TryGetValue(itemId, out var current) || current <= 0) return 0;
|
||||
|
||||
int removed = Mathf.Min(current, amount);
|
||||
int target = current - removed;
|
||||
if (target <= 0)
|
||||
{
|
||||
_counts.Remove(itemId);
|
||||
_newItems.Remove(itemId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_counts[itemId] = target;
|
||||
}
|
||||
|
||||
_viewDirty = true;
|
||||
RaiseChanged();
|
||||
return removed;
|
||||
}
|
||||
|
||||
public void MarkSeen(string itemId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(itemId)) return;
|
||||
if (_newItems.Remove(itemId))
|
||||
{
|
||||
_viewDirty = true;
|
||||
RaiseChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 内部 ──────────────────────────────────────────────────────────────
|
||||
private void RaiseChanged()
|
||||
{
|
||||
_onInventoryChanged?.Raise();
|
||||
OnInventoryChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void RebuildViewIfDirty()
|
||||
{
|
||||
if (!_viewDirty) return;
|
||||
_view.Clear();
|
||||
foreach (var kv in _counts)
|
||||
{
|
||||
var item = _database != null ? _database.Find(kv.Key) : null;
|
||||
if (item == null) continue;
|
||||
_view.Add(new InventoryEntry(item, kv.Value, _newItems.Contains(kv.Key)));
|
||||
}
|
||||
// 按分类枚举顺序、再按本地化名/ID 稳定排序,供 UI 分组展示
|
||||
_view.Sort((a, b) =>
|
||||
{
|
||||
int c = a.Item.category.CompareTo(b.Item.category);
|
||||
return c != 0 ? c : string.CompareOrdinal(a.Item.itemId, b.Item.itemId);
|
||||
});
|
||||
_viewDirty = false;
|
||||
}
|
||||
|
||||
// ── ISaveable ─────────────────────────────────────────────────────────
|
||||
public void OnSave(SaveData data)
|
||||
{
|
||||
data.Inventory.ItemCounts.Clear();
|
||||
foreach (var kv in _counts) data.Inventory.ItemCounts[kv.Key] = kv.Value;
|
||||
data.Inventory.NewItemIds.Clear();
|
||||
data.Inventory.NewItemIds.AddRange(_newItems);
|
||||
}
|
||||
|
||||
public void OnLoad(SaveData data)
|
||||
{
|
||||
_counts.Clear();
|
||||
_newItems.Clear();
|
||||
if (data.Inventory?.ItemCounts != null)
|
||||
foreach (var kv in data.Inventory.ItemCounts)
|
||||
if (!string.IsNullOrEmpty(kv.Key) && kv.Value > 0)
|
||||
_counts[kv.Key] = kv.Value;
|
||||
if (data.Inventory?.NewItemIds != null)
|
||||
foreach (var id in data.Inventory.NewItemIds)
|
||||
if (_counts.ContainsKey(id)) _newItems.Add(id);
|
||||
|
||||
_viewDirty = true;
|
||||
RaiseChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user