[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
-130
View File
@@ -1,130 +0,0 @@
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using YachtDice.Modifiers;
using YachtDice.Scoring;
using YachtDice.Shop;
namespace YachtDice.Editor
{
public static class ModifierAssetCreator
{
private const string BasePath = "Assets/ScriptableObjects/Modifiers";
private const string CatalogPath = "Assets/ScriptableObjects";
[MenuItem("YachtDice/Create Example Modifiers + Catalog")]
public static void CreateAll()
{
EnsureFolder(BasePath);
EnsureFolder(CatalogPath);
var m1 = CreateModifier("BonusPerOne", "Bonus Per One",
"+10 за каждый кубик со значением 1",
ModifierRarity.Common, 100, 50,
ModifierScope.SelectedCategory, ModifierEffectType.AddPerDieValue,
10f, 1, YachtCategory.Ones, false,
ModifierDurability.Permanent, 0);
var m2 = CreateModifier("MultiplierPerSix", "Multiplier Per Six",
"x6 за каждый кубик со значением 6",
ModifierRarity.Rare, 200, 100,
ModifierScope.SelectedCategory, ModifierEffectType.MultiplyPerDieValue,
6f, 6, YachtCategory.Ones, false,
ModifierDurability.Permanent, 0);
var m3 = CreateModifier("FullHouseFlat", "Full House Flat Bonus",
"+15 при закрытии Full House",
ModifierRarity.Uncommon, 150, 75,
ModifierScope.SelectedCategory, ModifierEffectType.AddFlatToFinalScore,
15f, 0, YachtCategory.FullHouse, true,
ModifierDurability.Permanent, 0);
var m4 = CreateModifier("YachtDoubler", "Yacht Doubler",
"x2 при закрытии Yacht (3 использования)",
ModifierRarity.Epic, 300, 150,
ModifierScope.SelectedCategory, ModifierEffectType.MultiplyFinalScore,
2f, 0, YachtCategory.Yacht, true,
ModifierDurability.LimitedUses, 3);
var m5 = CreateModifier("FiveBonusGlobal", "Five Bonus Global",
"+5 за каждую пятёрку при закрытии любой категории",
ModifierRarity.Uncommon, 250, 125,
ModifierScope.AnyCategoryClosed, ModifierEffectType.AddPerDieValue,
5f, 5, YachtCategory.Ones, false,
ModifierDurability.Permanent, 0);
var m6 = CreateModifier("CloseMultiplier", "Close Multiplier",
"x1.1 при закрытии любой категории (5 использований)",
ModifierRarity.Rare, 350, 175,
ModifierScope.AnyCategoryClosed, ModifierEffectType.MultiplyFinalScore,
1.1f, 0, YachtCategory.Ones, false,
ModifierDurability.LimitedUses, 5);
// Create ShopCatalog
var catalog = ScriptableObject.CreateInstance<ShopCatalog>();
var catalogSO = new SerializedObject(catalog);
var listProp = catalogSO.FindProperty("availableModifiers");
listProp.arraySize = 6;
listProp.GetArrayElementAtIndex(0).objectReferenceValue = m1;
listProp.GetArrayElementAtIndex(1).objectReferenceValue = m2;
listProp.GetArrayElementAtIndex(2).objectReferenceValue = m3;
listProp.GetArrayElementAtIndex(3).objectReferenceValue = m4;
listProp.GetArrayElementAtIndex(4).objectReferenceValue = m5;
listProp.GetArrayElementAtIndex(5).objectReferenceValue = m6;
catalogSO.ApplyModifiedProperties();
AssetDatabase.CreateAsset(catalog, $"{CatalogPath}/ShopCatalog.asset");
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("Created 6 example modifiers and ShopCatalog.");
}
private static ModifierData CreateModifier(
string id, string displayName, string description,
ModifierRarity rarity, int shopPrice, int sellPrice,
ModifierScope scope, ModifierEffectType effectType,
float effectValue, int dieValue, YachtCategory targetCategory, bool hasCategoryFilter,
ModifierDurability durability, int maxUses)
{
var data = ScriptableObject.CreateInstance<ModifierData>();
var so = new SerializedObject(data);
so.FindProperty("id").stringValue = id;
so.FindProperty("displayName").stringValue = displayName;
so.FindProperty("description").stringValue = description;
so.FindProperty("rarity").enumValueIndex = (int)rarity;
so.FindProperty("shopPrice").intValue = shopPrice;
so.FindProperty("sellPrice").intValue = sellPrice;
so.FindProperty("scope").enumValueIndex = (int)scope;
so.FindProperty("effectType").enumValueIndex = (int)effectType;
so.FindProperty("effectValue").floatValue = effectValue;
so.FindProperty("durability").enumValueIndex = (int)durability;
so.FindProperty("maxUses").intValue = maxUses;
var targetProp = so.FindProperty("target");
targetProp.FindPropertyRelative("DieValue").intValue = dieValue;
targetProp.FindPropertyRelative("TargetCategory").enumValueIndex = (int)targetCategory;
targetProp.FindPropertyRelative("HasCategoryFilter").boolValue = hasCategoryFilter;
so.ApplyModifiedProperties();
AssetDatabase.CreateAsset(data, $"{BasePath}/{id}.asset");
return data;
}
private static void EnsureFolder(string path)
{
string[] parts = path.Split('/');
string current = parts[0];
for (int i = 1; i < parts.Length; i++)
{
string next = current + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(next))
AssetDatabase.CreateFolder(current, parts[i]);
current = next;
}
}
}
}
#endif