180 lines
7.6 KiB
C#
180 lines
7.6 KiB
C#
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using UnityEngine;
|
||
using BaseGames.Core;
|
||
using BaseGames.Core.Events;
|
||
using BaseGames.Core.Save;
|
||
|
||
namespace BaseGames.World.Shop
|
||
{
|
||
/// <summary>
|
||
/// 商店流程控制器(架构 15_MapShopModule §2.3)。
|
||
/// 管理库存可见性、购买逻辑、补货策略,并通过 ISaveable 持久化购买记录。
|
||
/// </summary>
|
||
public class ShopController : MonoBehaviour, ISaveable
|
||
{
|
||
[SerializeField] private ShopInventorySO _inventory;
|
||
[SerializeField] private ShopPanel _shopPanel; // UI 面板(P4 UI 模块实现)
|
||
|
||
[Header("Event Channels(广播)")]
|
||
[SerializeField] private StringEventChannelSO _onShopOpen; // EVT_ShopOpened(shopId)
|
||
[SerializeField] private VoidEventChannelSO _onShopClosed; // EVT_ShopClosed
|
||
[SerializeField] private ShopPurchaseEventChannelSO _onItemPurchased; // EVT_ItemPurchased
|
||
|
||
[Header("Event Channels(订阅补货触发)")]
|
||
[SerializeField] private StringEventChannelSO _onBossDefeated; // EVT_BossDefeated
|
||
[SerializeField] private VoidEventChannelSO _onSavePointActivated; // EVT_SavePointActivated
|
||
|
||
// key = itemId,value = 已购次数
|
||
private Dictionary<string, int> _purchaseCounts = new();
|
||
private HashSet<string> _soldUniqueItems = new();
|
||
private List<ShopItemSO> _availableItemsCache;
|
||
private bool _isDirty = true;
|
||
private readonly CompositeDisposable _subs = new();
|
||
|
||
private void OnEnable()
|
||
{
|
||
if (_inventory == null) return;
|
||
if (_inventory.RestockPolicy == RestockPolicy.OnBossDefeat)
|
||
_onBossDefeated?.Subscribe(OnBossDefeated).AddTo(_subs);
|
||
if (_inventory.RestockPolicy == RestockPolicy.OnSavePoint)
|
||
_onSavePointActivated?.Subscribe(OnSavePointActivated).AddTo(_subs);
|
||
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
_subs.Clear();
|
||
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Unregister(this);
|
||
}
|
||
|
||
private void OnBossDefeated(string _) => Restock();
|
||
private void OnSavePointActivated() => Restock();
|
||
|
||
// ── 公共 API ──────────────────────────────────────────────────────────
|
||
|
||
public void Open()
|
||
{
|
||
_shopPanel?.Show(GetAvailableItems(), this);
|
||
_onShopOpen?.Raise(_inventory.ShopId);
|
||
}
|
||
|
||
public void Close()
|
||
{
|
||
_shopPanel?.Hide();
|
||
_onShopClosed?.Raise();
|
||
}
|
||
|
||
/// <summary>返回当前可购买的商品列表(过滤已售唯一品及超限商品)。结果在库存变更时才重建。</summary>
|
||
public List<ShopItemSO> GetAvailableItems()
|
||
{
|
||
if (!_isDirty) return _availableItemsCache;
|
||
if (_inventory?.DefaultInventory == null)
|
||
{
|
||
_availableItemsCache = new List<ShopItemSO>();
|
||
_isDirty = false;
|
||
return _availableItemsCache;
|
||
}
|
||
_availableItemsCache = _inventory.DefaultInventory
|
||
.Where(item => item != null
|
||
&& !_soldUniqueItems.Contains(item.ItemId)
|
||
&& (item.MaxPurchaseCount < 0 || GetPurchaseCount(item.ItemId) < item.MaxPurchaseCount))
|
||
.Take(_inventory.MaxDisplaySlots)
|
||
.ToList();
|
||
_isDirty = false;
|
||
return _availableItemsCache;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按 RestockPolicy 补货:重置非唯一商品的购买次数(已售唯一品不恢复)。
|
||
/// </summary>
|
||
public void Restock()
|
||
{
|
||
if (_inventory?.DefaultInventory == null) return;
|
||
var nonUniqueIds = _inventory.DefaultInventory
|
||
.Where(i => i != null && !i.IsUnique)
|
||
.Select(i => i.ItemId);
|
||
foreach (var id in nonUniqueIds)
|
||
_purchaseCounts.Remove(id);
|
||
_isDirty = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 尝试购买商品。由 ShopPanel 的购买按钮调用。
|
||
/// </summary>
|
||
/// <returns>购买成功返回 true;资金不足或商品不可购返回 false。</returns>
|
||
public bool TryPurchase(ShopItemSO item, int playerGeo)
|
||
{
|
||
if (item == null) return false;
|
||
int effectivePrice = GetEffectivePrice(item);
|
||
if (playerGeo < effectivePrice) return false;
|
||
if (_soldUniqueItems.Contains(item.ItemId)) return false;
|
||
if (item.MaxPurchaseCount >= 0 && GetPurchaseCount(item.ItemId) >= item.MaxPurchaseCount)
|
||
return false;
|
||
|
||
// 通过事件频道扣 Geo(PlayerStats 监听 EVT_ItemPurchased)
|
||
_onItemPurchased?.Raise(new ShopPurchaseEvent
|
||
{
|
||
ItemId = item.ItemId,
|
||
Price = effectivePrice,
|
||
});
|
||
|
||
// 更新库存状态
|
||
_purchaseCounts[item.ItemId] = GetPurchaseCount(item.ItemId) + 1;
|
||
if (item.IsUnique) _soldUniqueItems.Add(item.ItemId);
|
||
_isDirty = true;
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 返回应用难度乘数后的实际价格。UI 层可通过此方法显示正确标价。
|
||
/// </summary>
|
||
public int GetEffectivePrice(ShopItemSO item)
|
||
{
|
||
var scaler = ServiceLocator.GetOrDefault<IDifficultyService>()?.CurrentScaler;
|
||
if (scaler == null) return item.BasePrice;
|
||
return Mathf.Max(1, Mathf.RoundToInt(item.BasePrice * scaler.ShopPriceMultiplier));
|
||
}
|
||
|
||
private int GetPurchaseCount(string id)
|
||
=> _purchaseCounts.TryGetValue(id, out var c) ? c : 0;
|
||
|
||
// ── ISaveable ─────────────────────────────────────────────────────────
|
||
|
||
public void OnSave(SaveData data)
|
||
{
|
||
if (_inventory == null) return;
|
||
if (!data.Shops.ShopRecords.ContainsKey(_inventory.ShopId))
|
||
data.Shops.ShopRecords[_inventory.ShopId] = new ShopRecord();
|
||
|
||
var record = data.Shops.ShopRecords[_inventory.ShopId];
|
||
record.SoldUniqueItems = _soldUniqueItems.ToList();
|
||
record.PurchaseCounts = new Dictionary<string, int>(_purchaseCounts);
|
||
}
|
||
|
||
public void OnLoad(SaveData data)
|
||
{
|
||
if (_inventory == null) return;
|
||
if (data.Shops.ShopRecords.TryGetValue(_inventory.ShopId, out var record))
|
||
{
|
||
_soldUniqueItems = new HashSet<string>(record.SoldUniqueItems ?? new List<string>());
|
||
_purchaseCounts = record.PurchaseCounts ?? new Dictionary<string, int>();
|
||
_isDirty = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── ShopPanel ──────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 商店 UI 面板基类。
|
||
/// ShopController 通过此接口调用面板显示/隐藏,已解耦具体 UI 实现。
|
||
/// </summary>
|
||
public class ShopPanel : MonoBehaviour
|
||
{
|
||
public virtual void Show(List<ShopItemSO> items, ShopController controller) { }
|
||
public virtual void Hide() { }
|
||
}
|
||
}
|