Rework shop to support multiple item types and add unified PlayerModel

Generalize the shop from selling only ModifierDefinition to any IShopItem
(modifiers + dice). Add purchase condition checks, hover tooltips, and
fixed prices. Introduce PlayerModel as a facade over CurrencyBank,
InventoryModel, and DiceCollection for centralized state and save/load.

New files:
- IShopItem interface for purchasable items
- ShopCatalog SO (unified catalog for all item types)
- ShopTooltipView (description on hover)
- PlayerModel (aggregates player state)
- DiceCollection (owned dice tracking)
- DiceCatalog SO (dice definition registry)
- DiceCollectionTests

Modified:
- ModifierDefinition/DieDefinitionSO implement IShopItem
- ShopModel/ShopView/ShopItemView/ShopController use IShopItem
- SaveData v3 with OwnedDiceIds
- GameController uses PlayerModel for save/load
- GameLifetimeScope registers new services

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 21:04:34 +07:00
parent 85d639aa70
commit fcceb0ce45
18 changed files with 576 additions and 54 deletions
+26 -8
View File
@@ -9,6 +9,7 @@ using YachtDice.Economy;
using YachtDice.Shop;
using YachtDice.Inventory;
using YachtDice.Persistence;
using YachtDice.Player;
using YachtDice.Modifiers.Definition;
using YachtDice.Modifiers.Runtime;
@@ -36,7 +37,9 @@ namespace YachtDice.UI
private ModifierRegistry modifierRegistry;
private CategoryCatalog categoryCatalog;
private ModifierCatalog modifierCatalog;
private DiceCatalog diceCatalog;
private ShopModel shopModel;
private PlayerModel playerModel;
[Inject]
public void Construct(
@@ -49,7 +52,9 @@ namespace YachtDice.UI
ModifierRegistry modifierRegistry,
CategoryCatalog categoryCatalog,
ModifierCatalog modifierCatalog,
ShopModel shopModel)
DiceCatalog diceCatalog,
ShopModel shopModel,
PlayerModel playerModel)
{
this.gameManager = gameManager;
this.scoringSystem = scoringSystem;
@@ -60,7 +65,9 @@ namespace YachtDice.UI
this.modifierRegistry = modifierRegistry;
this.categoryCatalog = categoryCatalog;
this.modifierCatalog = modifierCatalog;
this.diceCatalog = diceCatalog;
this.shopModel = shopModel;
this.playerModel = playerModel;
}
// ── Lifecycle ──────────────────────────────────────────────
@@ -82,9 +89,9 @@ namespace YachtDice.UI
gameInfoView.OnShopClicked += HandleShopClicked;
gameInfoView.OnInventoryClicked += HandleInventoryClicked;
// Currency & Modifiers
// Currency & Player state
currencyBank.OnBalanceChanged += HandleCurrencyChanged;
modifierRegistry.OnChanged += HandleInventoryChangedForSave;
playerModel.OnChanged += HandlePlayerChangedForSave;
// Initialize
scoreCardView.Initialize(categoryCatalog);
@@ -112,8 +119,8 @@ namespace YachtDice.UI
currencyBank.OnBalanceChanged -= HandleCurrencyChanged;
if (modifierRegistry != null)
modifierRegistry.OnChanged -= HandleInventoryChangedForSave;
if (playerModel != null)
playerModel.OnChanged -= HandlePlayerChangedForSave;
}
// ── Save / Load ─────────────────────────────────────────
@@ -157,13 +164,25 @@ namespace YachtDice.UI
modifierRegistry.LoadSaveData(entries, modifierCatalog);
shopModel.LoadPurchasedPermanentIds(permanentIds);
}
if (diceCatalog != null && save.OwnedDiceIds != null && save.OwnedDiceIds.Count > 0)
{
playerModel.Dice.LoadSaveData(save.OwnedDiceIds, diceCatalog);
var dicePermIds = new HashSet<string>(save.OwnedDiceIds);
var existingIds = shopModel.GetPurchasedPermanentIds();
foreach (var id in dicePermIds)
existingIds.Add(id);
shopModel.LoadPurchasedPermanentIds(existingIds);
}
}
private void PerformSave()
{
var save = new SaveData
{
Currency = currencyBank.Balance
Currency = currencyBank.Balance,
OwnedDiceIds = playerModel.Dice.GetSaveData(),
};
var entries = modifierRegistry.GetSaveData();
@@ -274,10 +293,9 @@ namespace YachtDice.UI
private void HandleCurrencyChanged(int newBalance)
{
gameInfoView.SetCurrencyText(newBalance);
PerformSave();
}
private void HandleInventoryChangedForSave()
private void HandlePlayerChangedForSave()
{
PerformSave();
}