[Refactor] Replace enum-driven modifier system with data-driven SO composition

Replace the entire static, enum-based modifier pipeline with a
composition-based, data-driven architecture using ScriptableObject
polymorphism. New modifiers can now be created by assembling SO building
blocks (Conditions + Effects + Behaviors) — no core code edits needed.

New architecture:
- Core/: TriggerType, ModifierPhase, ModifierContext, ICondition, IEffect
- Definition/: ModifierDefinitionSO, ModifierBehaviorSO, ConditionSO, EffectSO, ModifierCatalogSO
- Conditions/: DieValueCondition, CategoryCondition, MinScoreCondition, DiceCountCondition
- Effects/: AddFlat, AddPerDie, Multiply, MultiplyPerDie, PostMultiply, AddCurrency, ConsumeCharge
- Runtime/: ModifierInstance, ModifierRegistry (non-static service)
- Pipeline/: async ModifierPipeline with phase ordering, tracing, anti-recursion
- Editor/: ModifierDefinitionValidator with menu items
- Events/: GameEventBus (non-static typed dispatcher)
- DI/: GameLifetimeScope (VContainer composition root)

Deleted old system: ModifierData, ModifierEffect, ModifierEnums,
ModifierPipeline (static), ModifierRuntime, ModifierTarget, ShopCatalog,
ModifierAssetCreator.

Updated: ScoringSystem (VContainer + async), InventoryModel (delegates to
ModifierRegistry), ShopModel (uses ModifierDefinitionSO), GameController
(VContainer injection), SaveData (uses Runtime.ModifierSaveEntry), all
views/controllers, and all test files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 06:20:23 +07:00
parent 6e19de2f3d
commit 68c4abace3
57 changed files with 2227 additions and 912 deletions
+42 -31
View File
@@ -1,13 +1,15 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using VContainer;
using YachtDice.Game;
using YachtDice.Scoring;
using YachtDice.Economy;
using YachtDice.Shop;
using YachtDice.Inventory;
using YachtDice.Persistence;
using YachtDice.Modifiers;
using YachtDice.Modifiers.Definition;
using YachtDice.Modifiers.Runtime;
namespace YachtDice.UI
{
@@ -43,9 +45,16 @@ namespace YachtDice.UI
private int totalCategoryCount;
private ModifierRegistry modifierRegistry;
private InventoryModel inventoryModel;
private ShopModel shopModel;
[Inject]
public void Construct(ModifierRegistry modifierRegistry)
{
this.modifierRegistry = modifierRegistry;
}
// ── Lifecycle ──────────────────────────────────────────────
private void Awake()
@@ -70,7 +79,10 @@ namespace YachtDice.UI
// Currency
if (currencyBank != null)
currencyBank.OnBalanceChanged += HandleCurrencyChanged;
}
private void Start()
{
InitializeModifierSystems();
}
@@ -92,21 +104,26 @@ namespace YachtDice.UI
if (currencyBank != null)
currencyBank.OnBalanceChanged -= HandleCurrencyChanged;
if (inventoryModel != null)
inventoryModel.OnInventoryChanged -= HandleInventoryChangedForSave;
if (modifierRegistry != null)
modifierRegistry.OnChanged -= HandleInventoryChangedForSave;
}
// ── Modifier System Init ─────────────────────────────────
private void InitializeModifierSystems()
{
inventoryModel = new InventoryModel(maxActiveModifierSlots);
inventoryModel.OnInventoryChanged += HandleInventoryChangedForSave;
if (modifierRegistry == null)
modifierRegistry = new ModifierRegistry(maxActiveModifierSlots);
else
modifierRegistry.SetMaxActiveSlots(maxActiveModifierSlots);
ShopCatalog catalog = shopController != null ? shopController.Catalog : null;
modifierRegistry.OnChanged += HandleInventoryChangedForSave;
inventoryModel = new InventoryModel(modifierRegistry);
shopModel = new ShopModel(currencyBank, inventoryModel);
ModifierCatalogSO catalog = shopController != null ? shopController.Catalog : null;
LoadSaveData(catalog);
if (inventoryController != null)
@@ -119,7 +136,7 @@ namespace YachtDice.UI
gameInfoView.SetCurrencyText(currencyBank.Balance);
}
private void LoadSaveData(ShopCatalog catalog)
private void LoadSaveData(ModifierCatalogSO catalog)
{
SaveData save = SaveSystem.Load();
@@ -128,35 +145,34 @@ namespace YachtDice.UI
if (catalog != null && save.OwnedModifiers.Count > 0)
{
var runtimeList = new List<ModifierRuntime>();
var entries = new List<ModifierSaveEntry>();
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);
var oldEntry = save.OwnedModifiers[i];
var def = catalog.FindById(oldEntry.ModifierId);
if (data == null)
if (def == null)
{
Debug.LogWarning($"Modifier '{entry.ModifierId}' not found in catalog, skipping.");
Debug.LogWarning($"Modifier '{oldEntry.ModifierId}' not found in catalog, skipping.");
continue;
}
var runtime = new ModifierRuntime
entries.Add(new ModifierSaveEntry
{
ModifierId = entry.ModifierId,
IsActive = entry.IsActive,
RemainingUses = entry.RemainingUses,
Data = data
};
ModifierId = oldEntry.ModifierId,
IsActive = oldEntry.IsActive,
RemainingUses = oldEntry.RemainingUses,
Stacks = oldEntry.Stacks,
CustomState = oldEntry.CustomState,
});
runtimeList.Add(runtime);
if (data.Durability == ModifierDurability.Permanent)
permanentIds.Add(data.Id);
if (!def.HasLimitedUses)
permanentIds.Add(def.Id);
}
inventoryModel.LoadState(runtimeList);
modifierRegistry.LoadSaveData(entries, catalog);
shopModel.LoadPurchasedPermanentIds(permanentIds);
}
}
@@ -168,15 +184,10 @@ namespace YachtDice.UI
Currency = currencyBank != null ? currencyBank.Balance : 0
};
var owned = inventoryModel.GetAllForSave();
for (int i = 0; i < owned.Count; i++)
var entries = modifierRegistry.GetSaveData();
for (int i = 0; i < entries.Count; i++)
{
save.OwnedModifiers.Add(new ModifierSaveEntry
{
ModifierId = owned[i].ModifierId,
IsActive = owned[i].IsActive,
RemainingUses = owned[i].RemainingUses
});
save.OwnedModifiers.Add(entries[i]);
}
SaveSystem.Save(save);