Files
zeling_v2/Assets/_Game/Scripts/Inventory/InventoryManager.cs
2026-06-05 18:41:33 +08:00

210 lines
8.3 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}
}