[Add] Universal modifier system, shop, inventory & persistence
Replace hardcoded BonusForOnes/MultiplierForSixes with data-driven modifier system supporting 2 scopes (SelectedCategory, AnyCategoryClosed), 4 effect types, durability modes (Permanent, LimitedUses), and configurable targets via ScriptableObject (ModifierData). - Modifier domain: ModifierEnums, ModifierTarget, ModifierData, ModifierRuntime, ModifierEffect (dict-based strategy), ModifierPipeline (4-pass: cat-additive → cat-multiplicative → final-additive → final-multiplicative) - ScoringSystem: replaced old modifier list with ModifierPipeline integration, added OnCategoryConfirmed event - Shop MVC: ShopCatalog (SO), ShopModel, ShopView, ShopItemView, ShopController - Inventory MVC: InventoryModel (activate/deactivate/sell/durability), InventoryView, InventorySlotView, InventoryController - CurrencyBank: editor-adjustable balance with events - Persistence: SaveData + SaveSystem (Newtonsoft JSON + PlayerPrefs) - Editor: ModifierAssetCreator menu item to generate 6 example modifiers + catalog - Tests: 6 test classes covering effects, pipeline, scoring, shop, inventory, save - GameController: wired shop/inventory/save lifecycle - GameInfoView: added currency display, shop/inventory toggle buttons Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,8 +14,14 @@ public sealed class GameController : MonoBehaviour
|
||||
[SerializeField] private DicePanelView dicePanelView;
|
||||
[SerializeField] private GameInfoView gameInfoView;
|
||||
|
||||
[Header("Economy & Modifiers")]
|
||||
[SerializeField] private CurrencyBank currencyBank;
|
||||
[SerializeField] private ShopController shopController;
|
||||
[SerializeField] private InventoryController inventoryController;
|
||||
|
||||
[Header("Settings")]
|
||||
[SerializeField] private int maxRollsPerTurn = 3;
|
||||
[SerializeField] private int maxActiveModifierSlots = 5;
|
||||
|
||||
private static readonly YachtCategory[] UpperCategories =
|
||||
{
|
||||
@@ -28,6 +34,9 @@ public sealed class GameController : MonoBehaviour
|
||||
|
||||
private int totalCategoryCount;
|
||||
|
||||
private InventoryModel inventoryModel;
|
||||
private ShopModel shopModel;
|
||||
|
||||
// ── Lifecycle ──────────────────────────────────────────────
|
||||
|
||||
private void Awake()
|
||||
@@ -46,6 +55,14 @@ public sealed class GameController : MonoBehaviour
|
||||
dicePanelView.OnRollClicked += HandleRollClicked;
|
||||
dicePanelView.OnDiceToggled += HandleDiceToggled;
|
||||
gameInfoView.OnNewGameClicked += HandleNewGameClicked;
|
||||
gameInfoView.OnShopClicked += HandleShopClicked;
|
||||
gameInfoView.OnInventoryClicked += HandleInventoryClicked;
|
||||
|
||||
// Currency
|
||||
if (currencyBank != null)
|
||||
currencyBank.OnBalanceChanged += HandleCurrencyChanged;
|
||||
|
||||
InitializeModifierSystems();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
@@ -60,6 +77,100 @@ public sealed class GameController : MonoBehaviour
|
||||
dicePanelView.OnRollClicked -= HandleRollClicked;
|
||||
dicePanelView.OnDiceToggled -= HandleDiceToggled;
|
||||
gameInfoView.OnNewGameClicked -= HandleNewGameClicked;
|
||||
gameInfoView.OnShopClicked -= HandleShopClicked;
|
||||
gameInfoView.OnInventoryClicked -= HandleInventoryClicked;
|
||||
|
||||
if (currencyBank != null)
|
||||
currencyBank.OnBalanceChanged -= HandleCurrencyChanged;
|
||||
|
||||
if (inventoryModel != null)
|
||||
inventoryModel.OnInventoryChanged -= HandleInventoryChangedForSave;
|
||||
}
|
||||
|
||||
// ── Modifier System Init ─────────────────────────────────
|
||||
|
||||
private void InitializeModifierSystems()
|
||||
{
|
||||
inventoryModel = new InventoryModel(maxActiveModifierSlots);
|
||||
inventoryModel.OnInventoryChanged += HandleInventoryChangedForSave;
|
||||
|
||||
ShopCatalog catalog = shopController != null ? shopController.Catalog : null;
|
||||
|
||||
shopModel = new ShopModel(currencyBank, inventoryModel);
|
||||
|
||||
LoadSaveData(catalog);
|
||||
|
||||
if (inventoryController != null)
|
||||
inventoryController.Initialize(inventoryModel);
|
||||
|
||||
if (shopController != null)
|
||||
shopController.Initialize(shopModel);
|
||||
|
||||
if (currencyBank != null)
|
||||
gameInfoView.SetCurrencyText(currencyBank.Balance);
|
||||
}
|
||||
|
||||
private void LoadSaveData(ShopCatalog catalog)
|
||||
{
|
||||
SaveData save = SaveSystem.Load();
|
||||
|
||||
if (currencyBank != null && save.Currency > 0)
|
||||
currencyBank.SetBalance(save.Currency);
|
||||
|
||||
if (catalog != null && save.OwnedModifiers.Count > 0)
|
||||
{
|
||||
var runtimeList = new List<ModifierRuntime>();
|
||||
var permanentIds = new HashSet<string>();
|
||||
|
||||
for (int i = 0; i < save.OwnedModifiers.Count; i++)
|
||||
{
|
||||
var entry = save.OwnedModifiers[i];
|
||||
ModifierData data = catalog.FindById(entry.ModifierId);
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
Debug.LogWarning($"Modifier '{entry.ModifierId}' not found in catalog, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var runtime = new ModifierRuntime
|
||||
{
|
||||
ModifierId = entry.ModifierId,
|
||||
IsActive = entry.IsActive,
|
||||
RemainingUses = entry.RemainingUses,
|
||||
Data = data
|
||||
};
|
||||
|
||||
runtimeList.Add(runtime);
|
||||
|
||||
if (data.Durability == ModifierDurability.Permanent)
|
||||
permanentIds.Add(data.Id);
|
||||
}
|
||||
|
||||
inventoryModel.LoadState(runtimeList);
|
||||
shopModel.LoadPurchasedPermanentIds(permanentIds);
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformSave()
|
||||
{
|
||||
var save = new SaveData
|
||||
{
|
||||
Currency = currencyBank != null ? currencyBank.Balance : 0
|
||||
};
|
||||
|
||||
var owned = inventoryModel.GetAllForSave();
|
||||
for (int i = 0; i < owned.Count; i++)
|
||||
{
|
||||
save.OwnedModifiers.Add(new ModifierSaveEntry
|
||||
{
|
||||
ModifierId = owned[i].ModifierId,
|
||||
IsActive = owned[i].IsActive,
|
||||
RemainingUses = owned[i].RemainingUses
|
||||
});
|
||||
}
|
||||
|
||||
SaveSystem.Save(save);
|
||||
}
|
||||
|
||||
// ── Model Event Handlers ──────────────────────────────────
|
||||
@@ -93,6 +204,7 @@ public sealed class GameController : MonoBehaviour
|
||||
{
|
||||
scoreCardView.SetCategoryScored(category, finalScore);
|
||||
UpdateTotalDisplay();
|
||||
PerformSave();
|
||||
}
|
||||
|
||||
private void HandleGameOver(int totalScore)
|
||||
@@ -103,6 +215,7 @@ public sealed class GameController : MonoBehaviour
|
||||
|
||||
int displayTotal = CalculateDisplayTotal();
|
||||
gameInfoView.ShowGameOver(displayTotal);
|
||||
PerformSave();
|
||||
}
|
||||
|
||||
// ── View Event Handlers ───────────────────────────────────
|
||||
@@ -145,6 +258,43 @@ public sealed class GameController : MonoBehaviour
|
||||
gameManager.StartNewGame();
|
||||
}
|
||||
|
||||
private void HandleShopClicked()
|
||||
{
|
||||
if (shopController != null)
|
||||
{
|
||||
var shopView = shopController.GetComponentInChildren<ShopView>(true);
|
||||
if (shopView != null)
|
||||
{
|
||||
if (shopView.IsVisible) shopView.Hide();
|
||||
else shopView.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleInventoryClicked()
|
||||
{
|
||||
if (inventoryController != null)
|
||||
{
|
||||
var inventoryView = inventoryController.GetComponentInChildren<InventoryView>(true);
|
||||
if (inventoryView != null)
|
||||
{
|
||||
if (inventoryView.IsVisible) inventoryView.Hide();
|
||||
else inventoryView.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleCurrencyChanged(int newBalance)
|
||||
{
|
||||
gameInfoView.SetCurrencyText(newBalance);
|
||||
PerformSave();
|
||||
}
|
||||
|
||||
private void HandleInventoryChangedForSave()
|
||||
{
|
||||
PerformSave();
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────
|
||||
|
||||
private void UpdatePreviewScores(int[] diceValues)
|
||||
|
||||
Reference in New Issue
Block a user