多轮审查和修复
This commit is contained in:
179
Assets/Scripts/World/Shop/ShopController.cs
Normal file
179
Assets/Scripts/World/Shop/ShopController.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
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
|
||||
.Take(_inventory.MaxDisplaySlots)
|
||||
.Where(item => item != null
|
||||
&& !_soldUniqueItems.Contains(item.ItemId)
|
||||
&& (item.MaxPurchaseCount < 0 || GetPurchaseCount(item.ItemId) < item.MaxPurchaseCount))
|
||||
.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() { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user