using Outnumbered.Domain; using Xunit; namespace Outnumbered.Tests; // StatResolver: effective stat values from invested levels + run-card bonuses, plus MaxHp/MaxArmor and the missing-HP // fraction (Berserk driver). Stat defaults (StatsConfig): damage Base0/+10, crit_damage Base100/+10, max_hp Base0/+25. public class StatResolverTests { private static Dictionary Up(params (string key, int lvl)[] e) => e.ToDictionary(x => x.key, x => x.lvl, StringComparer.Ordinal); [Theory] [InlineData(0, 0)] // base 0 [InlineData(3, 30)] // +10/lvl [InlineData(5, 50)] // maxed public void Eff_damage_is_base_plus_level(int level, double expected) => Assert.Equal(expected, StatResolver.Eff(T.Snap(upgrades: Up((StatKeys.Damage, level))), StatKeys.Damage, T.StatDefs())); [Theory] [InlineData(0, 100)] // crit_damage has Base=100 [InlineData(10, 200)] // +10/lvl maxed public void Eff_honours_base_value(int level, double expected) => Assert.Equal(expected, StatResolver.Eff(T.Snap(upgrades: Up((StatKeys.CritDamage, level))), StatKeys.CritDamage, T.StatDefs())); [Fact] public void Eff_unknown_key_is_zero() => Assert.Equal(0.0, StatResolver.Eff(T.Snap(), "no_such_stat", T.StatDefs())); [Fact] public void Eff_null_upgrades_yields_base_only() { // The pd-less / no-investment snapshot: Upgrades null -> LevelOf returns 0 -> Base only (crit_damage Base=100). var s = T.Snap() with { Upgrades = null! }; Assert.Equal(100.0, StatResolver.Eff(s, StatKeys.CritDamage, T.StatDefs())); } [Fact] public void EffRun_adds_card_bonus_on_top_of_permanent() { var s = T.Snap(upgrades: Up((StatKeys.Damage, 5)), cards: new Cards((StatKeys.Damage, 100))); Assert.Equal(50.0, StatResolver.Eff(s, StatKeys.Damage, T.StatDefs())); // permanent only Assert.Equal(150.0, StatResolver.EffRun(s, StatKeys.Damage, T.StatDefs())); // + card } [Fact] public void EffRun_without_cards_equals_eff() { var s = T.Snap(upgrades: Up((StatKeys.Damage, 3))); Assert.Equal(StatResolver.Eff(s, StatKeys.Damage, T.StatDefs()), StatResolver.EffRun(s, StatKeys.Damage, T.StatDefs())); } [Theory] [InlineData(0, 100)] // base 100 HP, no investment [InlineData(10, 350)] // +25/lvl * 10 = +250 public void MaxHp_is_base_plus_effrun(int level, int expected) => Assert.Equal(expected, StatResolver.MaxHp(T.Snap(upgrades: Up((StatKeys.MaxHp, level))), T.StatDefs(), T.BaseMaxHp)); [Fact] public void MaxHp_includes_card_points_and_truncates() { // max_hp level 10 (=250) + a flat +40 card -> 100 + (int)290 = 390. var s = T.Snap(upgrades: Up((StatKeys.MaxHp, 10)), cards: new Cards((StatKeys.MaxHp, 40))); Assert.Equal(390, StatResolver.MaxHp(s, T.StatDefs(), T.BaseMaxHp)); } [Theory] [InlineData(0, 100)] [InlineData(10, 350)] public void MaxArmor_is_base_plus_effrun(int level, int expected) => Assert.Equal(expected, StatResolver.MaxArmor(T.Snap(upgrades: Up((StatKeys.MaxArmor, level))), T.StatDefs(), T.BaseMaxArmor)); [Fact] public void CritChance_reads_the_crit_chance_stat() => Assert.Equal(50.0, CombatResolver.CritChance(T.Snap(upgrades: Up((StatKeys.CritChance, 20))), T.StatDefs())); // +2.5/lvl * 20 [Fact] public void CardMag_returns_effect_card_value_or_zero() { var s = T.Snap(cards: new Cards((CardKeys.BerserkPassive, 120))); Assert.Equal(120.0, StatResolver.CardMag(s, CardKeys.BerserkPassive)); Assert.Equal(0.0, StatResolver.CardMag(s, CardKeys.HsReduction)); // key not present Assert.Equal(0.0, StatResolver.CardMag(T.Snap(), CardKeys.BerserkPassive)); // no cards at all } // ---- MissingHpFraction: 0 at full, ->1 near death, and the §3 settled delta: 0 (not 1) for a dead / pawn-less snapshot ---- [Theory] [InlineData(100, 100, 0.0)] // full [InlineData(50, 100, 0.5)] [InlineData(1, 100, 0.99)] // near death public void MissingHpFraction_scales_with_missing_health(int health, int maxHp, double expected) => T.Close(expected, StatResolver.MissingHpFraction(T.Snap(health: health), maxHp)); [Theory] [InlineData(0)] // §3 DELTA #2: Health<=0 (no live pawn captured) -> 0, NOT 1. Missing-HP scaling needs a live attacker. [InlineData(-5)] public void MissingHpFraction_is_zero_for_dead_or_pawnless(int health) => Assert.Equal(0.0, StatResolver.MissingHpFraction(T.Snap(health: health), 100)); [Fact] public void MissingHpFraction_zero_for_nonpositive_maxhp() => Assert.Equal(0.0, StatResolver.MissingHpFraction(T.Snap(health: 50), 0)); [Fact] public void MissingHpFraction_clamps_overheal_to_zero() => Assert.Equal(0.0, StatResolver.MissingHpFraction(T.Snap(health: 150), 100)); // 1 - 1.5 = -0.5 -> 0 }