210 lines
8.3 KiB
C#
210 lines
8.3 KiB
C#
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();
|
||
}
|
||
}
|
||
}
|