[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:
@@ -0,0 +1,224 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using YachtDice.Modifiers.Definition;
|
||||
|
||||
namespace YachtDice.Modifiers.Editor
|
||||
{
|
||||
public static class ModifierDefinitionValidator
|
||||
{
|
||||
[MenuItem("YachtDice/Validate All Modifier Definitions")]
|
||||
public static void ValidateAll()
|
||||
{
|
||||
string[] guids = AssetDatabase.FindAssets("t:ModifierDefinitionSO");
|
||||
|
||||
if (guids.Length == 0)
|
||||
{
|
||||
Debug.Log("[ModifierValidator] No ModifierDefinitionSO assets found.");
|
||||
return;
|
||||
}
|
||||
|
||||
int errorCount = 0;
|
||||
int warnCount = 0;
|
||||
var usedIds = new Dictionary<string, string>(); // id -> asset path
|
||||
|
||||
for (int i = 0; i < guids.Length; i++)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
|
||||
var def = AssetDatabase.LoadAssetAtPath<ModifierDefinitionSO>(path);
|
||||
|
||||
if (def == null)
|
||||
{
|
||||
Debug.LogError($"[ModifierValidator] Failed to load asset at {path}");
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── Id checks ────────────────────────────────────────
|
||||
|
||||
if (string.IsNullOrWhiteSpace(def.Id))
|
||||
{
|
||||
Debug.LogError($"[ModifierValidator] {path}: Id is empty.", def);
|
||||
errorCount++;
|
||||
}
|
||||
else if (usedIds.TryGetValue(def.Id, out string existingPath))
|
||||
{
|
||||
Debug.LogError($"[ModifierValidator] {path}: Duplicate Id '{def.Id}' (also used by {existingPath}).", def);
|
||||
errorCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
usedIds[def.Id] = path;
|
||||
}
|
||||
|
||||
// ── Economy checks ───────────────────────────────────
|
||||
|
||||
if (def.ShopPrice < 0)
|
||||
{
|
||||
Debug.LogError($"[ModifierValidator] {path}: ShopPrice is negative ({def.ShopPrice}).", def);
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
if (def.SellPrice < 0)
|
||||
{
|
||||
Debug.LogError($"[ModifierValidator] {path}: SellPrice is negative ({def.SellPrice}).", def);
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
if (def.SellPrice > def.ShopPrice && def.ShopPrice > 0)
|
||||
{
|
||||
Debug.LogWarning($"[ModifierValidator] {path}: SellPrice ({def.SellPrice}) > ShopPrice ({def.ShopPrice}). Infinite money exploit?", def);
|
||||
warnCount++;
|
||||
}
|
||||
|
||||
// ── Durability checks ────────────────────────────────
|
||||
|
||||
if (def.HasLimitedUses && def.MaxUses <= 0)
|
||||
{
|
||||
Debug.LogError($"[ModifierValidator] {path}: HasLimitedUses is true but MaxUses is {def.MaxUses}.", def);
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
if (!def.HasLimitedUses && def.MaxUses > 0)
|
||||
{
|
||||
Debug.LogWarning($"[ModifierValidator] {path}: HasLimitedUses is false but MaxUses is {def.MaxUses}. Ignored at runtime.", def);
|
||||
warnCount++;
|
||||
}
|
||||
|
||||
if (def.MaxStacks < 1)
|
||||
{
|
||||
Debug.LogError($"[ModifierValidator] {path}: MaxStacks must be >= 1 (is {def.MaxStacks}).", def);
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
// ── Behavior checks ──────────────────────────────────
|
||||
|
||||
if (def.Behaviors == null || def.Behaviors.Count == 0)
|
||||
{
|
||||
Debug.LogWarning($"[ModifierValidator] {path}: No behaviors assigned. Modifier will do nothing.", def);
|
||||
warnCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int b = 0; b < def.Behaviors.Count; b++)
|
||||
{
|
||||
var behavior = def.Behaviors[b];
|
||||
|
||||
if (behavior == null)
|
||||
{
|
||||
Debug.LogError($"[ModifierValidator] {path}: Behavior slot [{b}] is null.", def);
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for null conditions
|
||||
if (behavior.Conditions != null)
|
||||
{
|
||||
for (int c = 0; c < behavior.Conditions.Count; c++)
|
||||
{
|
||||
if (behavior.Conditions[c] == null)
|
||||
{
|
||||
Debug.LogWarning($"[ModifierValidator] {path}: Behavior '{behavior.name}' has null condition at slot [{c}].", behavior);
|
||||
warnCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for null or empty effects
|
||||
if (behavior.Effects == null || behavior.Effects.Count == 0)
|
||||
{
|
||||
Debug.LogWarning($"[ModifierValidator] {path}: Behavior '{behavior.name}' has no effects.", behavior);
|
||||
warnCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int e = 0; e < behavior.Effects.Count; e++)
|
||||
{
|
||||
if (behavior.Effects[e] == null)
|
||||
{
|
||||
Debug.LogError($"[ModifierValidator] {path}: Behavior '{behavior.name}' has null effect at slot [{e}].", behavior);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Display checks ───────────────────────────────────
|
||||
|
||||
if (string.IsNullOrWhiteSpace(def.DisplayName))
|
||||
{
|
||||
Debug.LogWarning($"[ModifierValidator] {path}: DisplayName is empty.", def);
|
||||
warnCount++;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(def.Description))
|
||||
{
|
||||
Debug.LogWarning($"[ModifierValidator] {path}: Description is empty.", def);
|
||||
warnCount++;
|
||||
}
|
||||
}
|
||||
|
||||
string summary = $"[ModifierValidator] Validated {guids.Length} modifier(s): {errorCount} error(s), {warnCount} warning(s).";
|
||||
|
||||
if (errorCount > 0)
|
||||
Debug.LogError(summary);
|
||||
else if (warnCount > 0)
|
||||
Debug.LogWarning(summary);
|
||||
else
|
||||
Debug.Log(summary);
|
||||
}
|
||||
|
||||
[MenuItem("YachtDice/Validate Modifier Catalog")]
|
||||
public static void ValidateCatalog()
|
||||
{
|
||||
string[] guids = AssetDatabase.FindAssets("t:ModifierCatalogSO");
|
||||
|
||||
if (guids.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("[ModifierValidator] No ModifierCatalogSO asset found. Create one via Create > YachtDice/Modifiers/Catalog.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < guids.Length; i++)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
|
||||
var catalog = AssetDatabase.LoadAssetAtPath<ModifierCatalogSO>(path);
|
||||
|
||||
if (catalog == null)
|
||||
{
|
||||
Debug.LogError($"[ModifierValidator] Failed to load catalog at {path}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (catalog.All == null || catalog.All.Count == 0)
|
||||
{
|
||||
Debug.LogWarning($"[ModifierValidator] Catalog at {path} is empty.");
|
||||
continue;
|
||||
}
|
||||
|
||||
int nullCount = 0;
|
||||
var catalogIds = new HashSet<string>();
|
||||
|
||||
for (int j = 0; j < catalog.All.Count; j++)
|
||||
{
|
||||
if (catalog.All[j] == null)
|
||||
{
|
||||
nullCount++;
|
||||
Debug.LogError($"[ModifierValidator] Catalog {path}: Null entry at index [{j}].", catalog);
|
||||
continue;
|
||||
}
|
||||
|
||||
string id = catalog.All[j].Id;
|
||||
if (!catalogIds.Add(id))
|
||||
{
|
||||
Debug.LogError($"[ModifierValidator] Catalog {path}: Duplicate modifier Id '{id}' in catalog.", catalog);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[ModifierValidator] Catalog {path}: {catalog.All.Count} entries, {nullCount} null.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user