Files
zeling_v2/Assets/Scripts/World/Shop/ShopController.cs
2026-05-13 09:19:54 +08:00

180 lines
7.6 KiB
C#
Raw 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.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_ShopOpenedshopId
[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 = itemIdvalue = 已购次数
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;
// 通过事件频道扣 GeoPlayerStats 监听 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() { }
}
}