namespace Cthangover.Mods.Stats.Tests { using System; using System.Collections.Generic; using Cthangover.Core.Mods.Testing; /// /// How a sheet is computed: own value over archetype over formula over default, then /// bonuses and traits, then the bounds — and derived stats reading other stats. /// public class StatCalculatorTests { private static Dictionary Defs(params StatInfo[] stats) { var defs = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var s in stats) defs[s.ID] = s; return defs; } [ModTest("calculator/precedence_own_archetype_formula_default")] static void Precedence(ModTestContext ctx) { var inputs = new StatInputs { Level = 3, Definitions = Defs( new StatInfo { ID = "str", Default = 5 }, new StatInfo { ID = "agi", Default = 5, Formula = "4 + level" }, new StatInfo { ID = "int", Default = 5, Formula = "2 + level" }, new StatInfo { ID = "luk", Default = 5 }), Own = new Dictionary { ["str"] = "12" }, Archetype = new ArchetypeInfo { ID = "rogue", Stats = new Dictionary { ["agi"] = "6 + level * 2", ["str"] = "1" } }, }; var sheet = StatCalculator.Calculate("hero", inputs); ModAssert.CloseTo(12, sheet.Get("str"), what: "own value beats the archetype"); ModAssert.CloseTo(12, sheet.Get("agi"), what: "archetype beats the stat formula"); ModAssert.CloseTo(5, sheet.Get("int"), what: "stat formula beats the default"); ModAssert.CloseTo(5, sheet.Get("luk"), what: "default"); ModAssert.Equal("rogue", sheet.ArchetypeId, "archetype id"); ModAssert.Equal(3, sheet.Level, "level"); } [ModTest("calculator/derived_stats_read_others_in_any_order")] static void DerivedStats(ModTestContext ctx) { var inputs = new StatInputs { Level = 2, Definitions = Defs( new StatInfo { ID = "max_hp", Formula = "30 + vit * 6 + level * 4" }, new StatInfo { ID = "atk", Formula = "str * 2" }, new StatInfo { ID = "vit", Default = 3 }, new StatInfo { ID = "str", Default = 4 }), }; var sheet = StatCalculator.Calculate("hero", inputs); ModAssert.CloseTo(56, sheet.Get("max_hp"), what: "30 + 18 + 8"); ModAssert.CloseTo(8, sheet.Get("atk"), what: "atk from str"); } [ModTest("calculator/bonus_traits_and_bounds")] static void BonusTraitsAndBounds(ModTestContext ctx) { var inputs = new StatInputs { Level = 4, Definitions = Defs( new StatInfo { ID = "atk", Default = 10 }, new StatInfo { ID = "luk", Default = 5, Max = 9 }, new StatInfo { ID = "def", Default = 2, Min = 0 }), Bonus = new Dictionary { ["atk"] = 2 }, Traits = new List { new() { Stat = "atk", Add = 3 }, new() { Stat = "atk", Percent = 50 }, new() { Stat = "atk", Formula = "level" }, new() { Stat = "luk", Add = 10 }, new() { Stat = "def", Add = -8 }, }, }; var sheet = StatCalculator.Calculate("hero", inputs); ModAssert.CloseTo(12, sheet.GetBase("atk"), what: "base = default + bonus"); ModAssert.CloseTo(28.5, sheet.Get("atk"), what: "(12 + 3 + 4) * 1.5"); ModAssert.Equal(29, sheet.Rounded("atk"), "rounded half away from zero"); ModAssert.CloseTo(9, sheet.Get("luk"), what: "clamped to Max"); ModAssert.CloseTo(0, sheet.Get("def"), what: "clamped to Min"); } [ModTest("calculator/undefined_stats_still_count")] static void UndefinedStatsStillCount(ModTestContext ctx) { var inputs = new StatInputs { Own = new Dictionary { ["courage"] = "3" }, Traits = new List { new() { Stat = "trust", Add = 2 } }, }; var sheet = StatCalculator.Calculate("player", inputs); ModAssert.True(sheet.Has("courage") && sheet.Has("trust"), "own and trait stats are on the sheet"); ModAssert.CloseTo(3, sheet.Get("courage"), what: "own undefined stat"); ModAssert.CloseTo(2, sheet.Get("trust"), what: "trait-only stat"); ModAssert.CloseTo(0, sheet.Get("nothing"), what: "unknown stat reads 0"); } [ModTest("calculator/cycle_reads_zero_instead_of_hanging")] static void CycleReadsZero(ModTestContext ctx) { var inputs = new StatInputs { Definitions = Defs( new StatInfo { ID = "a", Formula = "b + 1" }, new StatInfo { ID = "b", Formula = "a + 1" }), }; var sheet = StatCalculator.Calculate("loop", inputs); ModAssert.True(sheet.Has("a") && sheet.Has("b"), "both computed"); ModAssert.True(sheet.Get("a") <= 2 && sheet.Get("b") <= 2, "the looping read counted as 0"); } [ModTest("calculator/sheet_is_a_formula_scope")] static void SheetIsAScope(ModTestContext ctx) { var sheet = StatCalculator.Calculate("hero", new StatInputs { Level = 5, Definitions = Defs(new StatInfo { ID = "str", Default = 7 }), }); ModAssert.CloseTo(17, Formulas.Formula.Evaluate("str + level * 2", sheet), what: "sheet answers stats and level"); } } }