108 lines
4.9 KiB
C#
108 lines
4.9 KiB
C#
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<string, int> 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
|
|
}
|