[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,29 @@
|
||||
using System.Collections.Generic;
|
||||
using YachtDice.Modifiers.Definition;
|
||||
|
||||
namespace YachtDice.Modifiers.Runtime
|
||||
{
|
||||
public class ModifierInstance
|
||||
{
|
||||
public ModifierDefinitionSO Definition { get; }
|
||||
public bool IsActive { get; set; }
|
||||
public int RemainingUses { get; set; }
|
||||
public int Stacks { get; set; } = 1;
|
||||
public Dictionary<string, float> CustomState { get; } = new();
|
||||
|
||||
public bool IsExpired => Definition.HasLimitedUses && RemainingUses <= 0;
|
||||
|
||||
public ModifierInstance(ModifierDefinitionSO definition)
|
||||
{
|
||||
Definition = definition;
|
||||
RemainingUses = definition.HasLimitedUses ? definition.MaxUses : -1;
|
||||
}
|
||||
|
||||
public void ConsumeCharge(int amount = 1)
|
||||
{
|
||||
if (!Definition.HasLimitedUses) return;
|
||||
RemainingUses -= amount;
|
||||
if (RemainingUses < 0) RemainingUses = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using YachtDice.Modifiers.Definition;
|
||||
|
||||
namespace YachtDice.Modifiers.Runtime
|
||||
{
|
||||
public class ModifierRegistry
|
||||
{
|
||||
private readonly List<ModifierInstance> instances = new();
|
||||
private readonly List<ModifierInstance> activeCache = new();
|
||||
private int maxActiveSlots;
|
||||
private bool activeCacheDirty = true;
|
||||
|
||||
public event Action OnChanged;
|
||||
public event Action<IReadOnlyList<ModifierInstance>> OnActiveModifiersChanged;
|
||||
|
||||
public IReadOnlyList<ModifierInstance> All => instances;
|
||||
public int MaxActiveSlots => maxActiveSlots;
|
||||
|
||||
public ModifierRegistry(int maxActiveSlots = 5)
|
||||
{
|
||||
this.maxActiveSlots = maxActiveSlots;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ModifierInstance> Active
|
||||
{
|
||||
get
|
||||
{
|
||||
if (activeCacheDirty)
|
||||
RebuildActiveCache();
|
||||
return activeCache;
|
||||
}
|
||||
}
|
||||
|
||||
public int ActiveCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int count = 0;
|
||||
for (int i = 0; i < instances.Count; i++)
|
||||
if (instances[i].IsActive) count++;
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetMaxActiveSlots(int slots)
|
||||
{
|
||||
maxActiveSlots = slots;
|
||||
}
|
||||
|
||||
public ModifierInstance Add(ModifierDefinitionSO definition)
|
||||
{
|
||||
var instance = new ModifierInstance(definition);
|
||||
instances.Add(instance);
|
||||
activeCacheDirty = true;
|
||||
OnChanged?.Invoke();
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void Remove(ModifierInstance instance)
|
||||
{
|
||||
if (!instances.Contains(instance)) return;
|
||||
|
||||
bool wasActive = instance.IsActive;
|
||||
instance.IsActive = false;
|
||||
instances.Remove(instance);
|
||||
activeCacheDirty = true;
|
||||
|
||||
if (wasActive)
|
||||
OnActiveModifiersChanged?.Invoke(Active);
|
||||
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
public bool TryActivate(ModifierInstance instance)
|
||||
{
|
||||
if (instance.IsActive) return false;
|
||||
if (!instances.Contains(instance)) return false;
|
||||
if (ActiveCount >= maxActiveSlots) return false;
|
||||
|
||||
instance.IsActive = true;
|
||||
activeCacheDirty = true;
|
||||
OnActiveModifiersChanged?.Invoke(Active);
|
||||
OnChanged?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Deactivate(ModifierInstance instance)
|
||||
{
|
||||
if (!instance.IsActive) return;
|
||||
|
||||
instance.IsActive = false;
|
||||
activeCacheDirty = true;
|
||||
OnActiveModifiersChanged?.Invoke(Active);
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void ConsumeChargesOnActive()
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
for (int i = instances.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var inst = instances[i];
|
||||
if (!inst.IsActive) continue;
|
||||
if (!inst.Definition.HasLimitedUses) continue;
|
||||
|
||||
inst.ConsumeCharge();
|
||||
|
||||
if (inst.IsExpired)
|
||||
{
|
||||
instances.RemoveAt(i);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
activeCacheDirty = true;
|
||||
OnActiveModifiersChanged?.Invoke(Active);
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public List<ModifierSaveEntry> GetSaveData()
|
||||
{
|
||||
var entries = new List<ModifierSaveEntry>();
|
||||
for (int i = 0; i < instances.Count; i++)
|
||||
{
|
||||
var inst = instances[i];
|
||||
var entry = new ModifierSaveEntry
|
||||
{
|
||||
ModifierId = inst.Definition.Id,
|
||||
IsActive = inst.IsActive,
|
||||
RemainingUses = inst.RemainingUses,
|
||||
Stacks = inst.Stacks,
|
||||
};
|
||||
|
||||
foreach (var kvp in inst.CustomState)
|
||||
{
|
||||
entry.CustomState.Add(new CustomStateEntry
|
||||
{
|
||||
Key = kvp.Key,
|
||||
Value = kvp.Value,
|
||||
});
|
||||
}
|
||||
|
||||
entries.Add(entry);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
public void LoadSaveData(List<ModifierSaveEntry> entries, ModifierCatalogSO catalog)
|
||||
{
|
||||
instances.Clear();
|
||||
activeCacheDirty = true;
|
||||
|
||||
if (entries == null) return;
|
||||
|
||||
for (int i = 0; i < entries.Count; i++)
|
||||
{
|
||||
var entry = entries[i];
|
||||
var definition = catalog.FindById(entry.ModifierId);
|
||||
|
||||
if (definition == null)
|
||||
{
|
||||
Debug.LogWarning($"Modifier '{entry.ModifierId}' not found in catalog, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var instance = new ModifierInstance(definition)
|
||||
{
|
||||
IsActive = entry.IsActive,
|
||||
RemainingUses = entry.RemainingUses,
|
||||
Stacks = entry.Stacks,
|
||||
};
|
||||
|
||||
if (entry.CustomState != null)
|
||||
{
|
||||
foreach (var cs in entry.CustomState)
|
||||
instance.CustomState[cs.Key] = cs.Value;
|
||||
}
|
||||
|
||||
instances.Add(instance);
|
||||
}
|
||||
|
||||
OnActiveModifiersChanged?.Invoke(Active);
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
instances.Clear();
|
||||
activeCacheDirty = true;
|
||||
OnActiveModifiersChanged?.Invoke(Active);
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void RebuildActiveCache()
|
||||
{
|
||||
activeCache.Clear();
|
||||
for (int i = 0; i < instances.Count; i++)
|
||||
{
|
||||
if (instances[i].IsActive)
|
||||
activeCache.Add(instances[i]);
|
||||
}
|
||||
activeCacheDirty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YachtDice.Modifiers.Runtime
|
||||
{
|
||||
[Serializable]
|
||||
public class ModifierSaveEntry
|
||||
{
|
||||
public string ModifierId;
|
||||
public bool IsActive;
|
||||
public int RemainingUses;
|
||||
public int Stacks;
|
||||
public List<CustomStateEntry> CustomState = new();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class CustomStateEntry
|
||||
{
|
||||
public string Key;
|
||||
public float Value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user