using System.Collections.Generic; using NUnit.Framework; using UnityEngine; using YachtDice.Modifiers.Conditions; using YachtDice.Modifiers.Core; using YachtDice.Modifiers.Definition; using YachtDice.Modifiers.Effects; using YachtDice.Modifiers.Pipeline; using YachtDice.Modifiers.Runtime; using YachtDice.Scoring; namespace YachtDice.Tests { public class ModifierPipelineTests { private ModifierRegistry registry; private ModifierPipeline pipeline; [SetUp] public void SetUp() { registry = new ModifierRegistry(10); pipeline = new ModifierPipeline(registry); pipeline.TracingEnabled = false; // disable debug logs during tests } private ModifierDefinitionSO CreateDef(string id, TriggerType trigger, List conditions, List effects) { var behavior = ModifierBehaviorSO.CreateForTest(trigger, conditions, effects); return ModifierDefinitionSO.CreateForTest(id, new List { behavior }); } private void RegisterAndActivate(ModifierDefinitionSO def) { var inst = registry.Add(def); registry.TryActivate(inst); } private ModifierContext CreateScoringContext(int baseScore, int[] dice, YachtCategory category) { return new ModifierContext { BaseScore = baseScore, DiceValues = dice, Category = category, AllActiveModifiers = registry.Active, }; } // ── Phase Ordering ────────────────────────────────────────── [Test] public void Execute_AdditiveBeforeMultiplicative() { var addEffect = AddFlatScoreEffect.CreateForTest(10, ModifierPhase.Additive); var mulEffect = MultiplyScoreEffect.CreateForTest(2f, ModifierPhase.Multiplicative); var addDef = CreateDef("add", TriggerType.OnCategoryScored, null, new List { addEffect }); var mulDef = CreateDef("mul", TriggerType.OnCategoryScored, null, new List { mulEffect }); RegisterAndActivate(mulDef); // registered first, but multiplicative phase RegisterAndActivate(addDef); // registered second, but additive phase var ctx = CreateScoringContext(20, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); // (20 + 10) * 2 = 60 Assert.AreEqual(60, result.FinalScore); } [Test] public void Execute_PostMultiplicativeAfterMultiplicative() { var mulEffect = MultiplyScoreEffect.CreateForTest(2f, ModifierPhase.Multiplicative); var postMulEffect = PostMultiplyEffect.CreateForTest(3f, ModifierPhase.PostMultiplicative); var mulDef = CreateDef("mul", TriggerType.OnCategoryScored, null, new List { mulEffect }); var postDef = CreateDef("post", TriggerType.OnCategoryScored, null, new List { postMulEffect }); RegisterAndActivate(postDef); RegisterAndActivate(mulDef); var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); // (10 + 0) * 2 * 3 = 60 Assert.AreEqual(60, result.FinalScore); } // ── Condition Filtering ───────────────────────────────────── [Test] public void Execute_ConditionFails_SkipsEffect() { var condition = CategoryCondition.CreateForTest(YachtCategory.FullHouse); var effect = AddFlatScoreEffect.CreateForTest(100); var def = CreateDef("fh-bonus", TriggerType.OnCategoryScored, new List { condition }, new List { effect }); RegisterAndActivate(def); // Scoring Ones, not FullHouse — condition should fail var ctx = CreateScoringContext(5, new[] { 1, 1, 1, 1, 1 }, YachtCategory.Ones); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(0, result.FlatBonus); Assert.AreEqual(5, result.FinalScore); } [Test] public void Execute_ConditionPasses_AppliesEffect() { var condition = CategoryCondition.CreateForTest(YachtCategory.FullHouse); var effect = AddFlatScoreEffect.CreateForTest(15); var def = CreateDef("fh-bonus", TriggerType.OnCategoryScored, new List { condition }, new List { effect }); RegisterAndActivate(def); var ctx = CreateScoringContext(25, new[] { 3, 3, 3, 2, 2 }, YachtCategory.FullHouse); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(15, result.FlatBonus); Assert.AreEqual(40, result.FinalScore); } // ── Trigger Filtering ─────────────────────────────────────── [Test] public void Execute_WrongTrigger_SkipsModifier() { var effect = AddFlatScoreEffect.CreateForTest(999); var def = CreateDef("turn-bonus", TriggerType.OnTurnStart, null, new List { effect }); RegisterAndActivate(def); // Fire OnCategoryScored, not OnTurnStart var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(0, result.FlatBonus); Assert.AreEqual(10, result.FinalScore); } // ── Multiple Modifiers ────────────────────────────────────── [Test] public void Execute_MultipleModifiers_CorrectOrder() { var perDieAdd = AddPerDieEffect.CreateForTest(2, targetDieValue: 3, phase: ModifierPhase.Additive); var perDieMul = MultiplyPerDieEffect.CreateForTest(1.5f, targetDieValue: 3, phase: ModifierPhase.Multiplicative); var flatAdd = AddFlatScoreEffect.CreateForTest(10, ModifierPhase.Additive, priority: 10); var finalMul = MultiplyScoreEffect.CreateForTest(2f, ModifierPhase.Multiplicative, priority: 10); var def1 = CreateDef("pda", TriggerType.OnCategoryScored, null, new List { perDieAdd }); var def2 = CreateDef("pdm", TriggerType.OnCategoryScored, null, new List { perDieMul }); var def3 = CreateDef("fa", TriggerType.OnCategoryScored, null, new List { flatAdd }); var def4 = CreateDef("fm", TriggerType.OnCategoryScored, null, new List { finalMul }); RegisterAndActivate(def4); RegisterAndActivate(def3); RegisterAndActivate(def2); RegisterAndActivate(def1); // dice: [3, 3, 3, 1, 2] — 3 threes var ctx = CreateScoringContext(9, new[] { 3, 3, 3, 1, 2 }, YachtCategory.Threes); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); // Additive phase: perDieAdd (+2*3=6) + flatAdd (+10) → FlatBonus = 16 // Multiplicative phase: perDieMul (1.5^3=3.375) then finalMul (*2) → Multiplier = 6.75 // FinalScore = floor((9 + 16) * 6.75) = floor(168.75) = 168 Assert.AreEqual(16, result.FlatBonus); Assert.AreEqual(168, result.FinalScore); } // ── Empty / Null Cases ────────────────────────────────────── [Test] public void Execute_NoActiveModifiers_NoChange() { var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(10, result.FinalScore); Assert.AreEqual(0, result.FlatBonus); Assert.AreEqual(1f, result.Multiplier); } [Test] public void Execute_InactiveModifier_Skipped() { var effect = AddFlatScoreEffect.CreateForTest(50); var def = CreateDef("inactive", TriggerType.OnCategoryScored, null, new List { effect }); // Add but don't activate registry.Add(def); var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(0, result.FlatBonus); Assert.AreEqual(10, result.FinalScore); } // ── Side Effects ──────────────────────────────────────────── [Test] public void Execute_SideEffectsInSideEffectPhase() { var scoreEffect = AddFlatScoreEffect.CreateForTest(10, ModifierPhase.Additive); var currencyEffect = AddCurrencyEffect.CreateForTest(25, ModifierPhase.SideEffect); var def = CreateDef("rewards", TriggerType.OnCategoryScored, null, new List { scoreEffect, currencyEffect }); RegisterAndActivate(def); var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(10, result.FlatBonus); Assert.AreEqual(25, result.CurrencyDelta); Assert.AreEqual(20, result.FinalScore); } // ── Tracing ───────────────────────────────────────────────── [Test] public void Execute_TracingEnabled_PopulatesDebugLog() { pipeline.TracingEnabled = true; var effect = AddFlatScoreEffect.CreateForTest(10); var def = CreateDef("traced", TriggerType.OnCategoryScored, null, new List { effect }); RegisterAndActivate(def); var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.IsNotNull(result.DebugLog); Assert.IsTrue(result.DebugLog.Count > 0); } // ── DieValue Condition ────────────────────────────────────── [Test] public void Execute_DieValueCondition_OnlyTriggersOnMatch() { var condition = DieValueCondition.CreateForTest(6, minCount: 3); var effect = AddFlatScoreEffect.CreateForTest(100); var def = CreateDef("sixes-bonus", TriggerType.OnCategoryScored, new List { condition }, new List { effect }); RegisterAndActivate(def); // Only 2 sixes — condition requires 3 var ctx = CreateScoringContext(12, new[] { 6, 6, 1, 2, 3 }, YachtCategory.Sixes); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(0, result.FlatBonus); // 3 sixes — condition passes var ctx2 = CreateScoringContext(18, new[] { 6, 6, 6, 1, 2 }, YachtCategory.Sixes); var result2 = pipeline.Execute(TriggerType.OnCategoryScored, ctx2).GetAwaiter().GetResult(); Assert.AreEqual(100, result2.FlatBonus); } // ── MinScore Condition ────────────────────────────────────── [Test] public void Execute_MinScoreCondition_ThresholdWorks() { var condition = MinScoreCondition.CreateForTest(20); var effect = MultiplyScoreEffect.CreateForTest(2f); var def = CreateDef("high-score-bonus", TriggerType.OnCategoryScored, new List { condition }, new List { effect }); RegisterAndActivate(def); // Below threshold var ctx = CreateScoringContext(15, new[] { 3, 3, 3, 3, 3 }, YachtCategory.Threes); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(1f, result.Multiplier); // At threshold var ctx2 = CreateScoringContext(20, new[] { 4, 4, 4, 4, 4 }, YachtCategory.Fours); var result2 = pipeline.Execute(TriggerType.OnCategoryScored, ctx2).GetAwaiter().GetResult(); Assert.AreEqual(2f, result2.Multiplier); } // ── ToScoreResult ─────────────────────────────────────────── [Test] public void ToScoreResult_ConvertsCorrectly() { var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); ctx.FlatBonus = 5; ctx.Multiplier = 2f; ctx.PostMultiplier = 1.5f; ScoreResult sr = ctx.ToScoreResult(); Assert.AreEqual(10, sr.BaseScore); Assert.AreEqual(5, sr.FlatBonus); Assert.AreEqual(3f, sr.Multiplier, 0.001f); // 2 * 1.5 Assert.AreEqual(YachtCategory.Chance, sr.Category); } } }