Purpose: Main in-game shop controller. Responsible for:
- generating item UI from
ShopItemData; - processing purchases of individual items and bundles (
ShopBundleData); - persisting state (owned items, equipped id, runtime discounts) as a single JSON blob via
SaveProvider; - managing the selected (equipped) item;
- multi-currency support through per-item and per-bundle
IMoneySpendoverrides.
Inventory integration lives in a separate bridge: ShopInventoryGrantBridge in Neo.Tools.Inventory. The bridge listens to Shop.OnPurchasedId and grants InventoryItemData from its mapping table. Neo.Shop.asmdef intentionally does not depend on Neo.Tools.Inventory, which avoids an asmdef cycle.
Since version 8.5.0, item identity is the stable string Id from ShopItemData, not an array index. The save format is a hard break: legacy keys Shop0/Shop1/.../ShopEquipped are no longer read.
Since 8.5.1, if catalog assets still have an empty Id, Shop backfills unique ids in Awake before LoadProfile() (details: ShopItemData -> Id auto-fill). This fixes cases where every ShopListView cell showed the same state.
Shop can be used only as the purchase/catalog controller while external views own all visible cells.
- Disable
Auto Spawn Itemswhen usingShopListView. - Use
ShopListViewto create/reuseShopItemcells and filter byShopItemData.Category. - Use
ShopCategoryButtonfor Inspector-only category tabs. - Runtime catalog helpers:
SetItems(...),SetBundles(...),SetMoneySpendSource(...),SetAutoSpawnItems(...). - Refresh helpers/events:
RefreshVisuals(),OnShopChanged,GetCategories(...).
This keeps one Shop as the source of truth for save, ownership, prices, currency, bundles, and inventory bridge events.
ShopItemData and ShopBundleData can select a currency by Money.SaveKey.
- Leave
Currency Override Save Keyempty to use the Shop default (moneySpendSource, thenMoney.I). - Set it to a key such as
Gemsto spend from theMoneyinstance whoseSaveKey == "Gems". - The old GameObject override is still supported as a scene fallback, but the save key field is the recommended option for ScriptableObjects.
Add Component > Neoxider > Shop > Shopon an empty GameObject.- Fill
_shopItemDataswithShopItemDataassets (see ShopItemData). - Optionally fill
_bundleswithShopBundleDataassets. - Use
_prefab+_containerfor auto-spawn UI, or pre-placeShopItemcomponents and assign_shopItems. - Optionally add
ShopInventoryGrantBridgeon the same GameObject to auto-grantInventoryItemDataon purchase.
| Mode | Behavior |
|---|---|
BuyAndEquip (default) |
Buy -> auto-select. Equivalent to the legacy _useSetItem = true path. |
BuyOnly |
Purchase only; equipped item is not changed. |
EquipOnly |
No spending; toggle selection only, useful for skin/cosmetic UI. |
Browse |
Read-only storefront: Buy() and BuyBundle() are no-ops; preview still works. |
| Field | Description |
|---|---|
_purchaseFlow |
Purchase mode. |
_shopItemDatas |
Array of item assets. Source of prices, sprites, descriptions, and stable ids. |
_bundles |
Optional bundle assets. |
_shopItemPreview |
UI preview slot. |
_shopItems |
Auto-populated from children plus optional auto-spawn from _prefab. |
_container, _prefab |
Parent and prefab for auto-spawn. |
_keySave |
Single SaveProvider key for the JSON ShopProfileData. Deleting this key fully wipes shop state. |
moneySpendSource |
Default GameObject with IMoneySpend. When null, Money.I is used. Item/Bundle CurrencyOverrideSaveKey takes precedence. |
_autoSubscribe |
Auto-subscribe ShopItem.buttonBuy to the item purchase action. |
_changePreviewOnPurchaseFailed |
Switch preview to the item on failed purchase. |
_propagateSelectionVisual (formerly _useSetItem) |
Call ShopItem.Select(bool) on every list entry when equipped changes. |
_activateSavedEquipped |
Auto-equip on load (BuyAndEquip / EquipOnly only): saved item, or the first catalog entry when save is empty or invalid. |
_prices, _keySaveEquipped |
Deprecated. Kept as serialized fields for legacy scene compatibility but ignored at runtime. |
Prefer these overloads when gameplay/UI code already works with catalog assets. They keep code independent from array order and make the v9 removal of int-indexed calls straightforward.
| Member | Purpose |
|---|---|
Buy(ShopItemData itemData) |
Buy / equip by item asset. |
BuyBundle(ShopBundleData bundleData) |
Buy a bundle by bundle asset. |
Select(ShopItemData itemData) |
Equip by item asset; pass null to clear selection. |
ShowPreview(ShopItemData itemData) |
Set preview by item asset; pass null to clear preview. |
IsOwned(ShopItemData itemData) / IsBundleOwned(ShopBundleData bundleData) |
Ownership query by typed asset. |
GetPrice(ShopItemData itemData) |
Current item price with runtime override applied. |
SetRuntimePrice(ShopItemData itemData, float price) / ClearRuntimePrice(ShopItemData itemData) |
Runtime discounts by typed item asset. |
Use this API when code stores or receives ids rather than assets.
| Member | Purpose |
|---|---|
EquippedId : string |
Currently equipped item. |
PreviewIdString : string |
Item shown in the preview slot. |
Buy(string itemId) |
Buy / equip by id. Respects _purchaseFlow. |
BuyBundle(string bundleId) |
Buy a bundle by id. |
Select(string itemId) |
Equip without buying. Pass "" to clear. |
ShowPreview(string itemId) |
Set the preview slot. |
IsOwned(string itemId) / IsBundleOwned(string bundleId) |
Ownership query. |
GetPrice(string itemId) |
Current price with runtime override applied. |
SetRuntimePrice(string itemId, float price) / ClearRuntimePrice(string itemId) |
Discounts / temporary price overrides. |
GetItemsInCategory(string category) |
Filter by ShopItemData.Category. |
ShopItemDatas, Bundles |
Catalog access. |
| Member | Behavior |
|---|---|
Id : int |
Proxy: IndexOfItemDataById(EquippedId) / Select(items[i].Id). |
PreviewId : int |
IndexOfItemDataById(PreviewIdString). |
Buy() |
Buys PreviewIdString with fallback to EquippedId. |
Buy(int id) |
Resolves _shopItemDatas[id].Id -> Buy(string). |
ShowPreview(int id) |
Resolves _shopItemDatas[id].Id -> ShowPreview(string). |
Prices : int[] |
Legacy array; ignored at runtime. |
| Event | Argument | When |
|---|---|---|
OnSelect |
int index |
Equip; legacy. |
OnSelectId |
string id |
Equip. |
OnPurchased |
int index |
Successful purchase; legacy. |
OnPurchasedId |
string id |
Successful item purchase. |
OnPurchaseFailed |
int index |
Insufficient funds; legacy. |
OnPurchaseFailedId |
string id |
Insufficient funds for item or bundle. |
OnPurchasedBundle |
ShopBundleData |
Bundle purchased after all items are granted. |
OnLoad |
none | Fired after Start(). |
Inventory grant events (OnGranted with (InventoryItemData, int)) live on ShopInventoryGrantBridge, not on Shop.
- Old serialized fields (
_prices,_keySaveEquipped,_useSetItem->_propagateSelectionVisualviaFormerlySerializedAs,_activateSavedEquipped) are kept so scenes preserve Inspector data. - Save format is a hard break: legacy
Shop0/Shop1keys are not read; on first launch the shop starts with an emptyShopProfileData. - UnityEvent subscriptions to
OnSelect<int>/OnPurchased<int>keep working:Buy(string)raises both int and string event variants.
Assets/Neoxider/Tests/Play/ShopPurchasePlayModeTests.cs- main PlayMode coverage for purchases, bundles, shop flows, multi-currency, inventory,ShopListView, and typed asset API.Assets/Neoxider/Tests/Edit/ShopProfileDataTests.cs- EditMode profile, JSON, sanitize, and runtime price override coverage.Assets/Neoxider/Tests/Edit/Save/ShopManagerTests.cs- legacy Shop/Save coverage.