using System; using System.Collections.Generic; using UnityEngine; using BaseGames.Core; using BaseGames.Core.Events; using BaseGames.Core.Save; namespace BaseGames.Inventory { /// /// 运行时背包管理器。 /// 挂在 Persistent 场景 [Services] 下,事件驱动记录玩家持有的道具, /// 实现 持久化。设计对照 /// 与 的服务注册 / 存档模式。 /// [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 _counts = new(); private readonly HashSet _newItems = new(); private readonly List _view = new(); private bool _viewDirty = true; private bool _isDuplicate; private readonly CompositeDisposable _subs = new(); public event Action OnInventoryChanged; // ── 生命周期 ────────────────────────────────────────────────────────── private void Awake() { if (ServiceLocator.GetOrDefault() != null) { _isDuplicate = true; Destroy(gameObject); return; } ServiceLocator.Register(this); Debug.Assert(_database != null, "[InventoryManager] _database 未赋值,请在 Inspector 中指定 ItemDatabaseSO。", this); } private void OnEnable() { if (_isDuplicate) return; _onItemPickup?.Subscribe(OnItemPickup).AddTo(_subs); ServiceLocator.GetOrDefault()?.Register(this); } private void OnDisable() { if (_isDuplicate) return; _subs.Clear(); ServiceLocator.GetOrDefault()?.Unregister(this); } private void OnDestroy() { if (_isDuplicate) return; ServiceLocator.Unregister(this); } // ── 事件驱动拾取 ────────────────────────────────────────────────────── private void OnItemPickup(string itemId) => AddItem(itemId, 1); // ── IInventoryService ───────────────────────────────────────────────── public IReadOnlyList 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(); } } }