initial commit
This commit is contained in:
commit
d701598350
67 changed files with 9351 additions and 0 deletions
136
Outnumbered.Tests/CombatChainTests.cs
Normal file
136
Outnumbered.Tests/CombatChainTests.cs
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
using Outnumbered.Domain;
|
||||
using Xunit;
|
||||
|
||||
namespace Outnumbered.Tests;
|
||||
|
||||
// CombatResolver's offense/defense multiplier chains — the single source of truth shared by the damage hook + the HUD.
|
||||
// Anchors are hand-traced from the documented chain (dmgMul*hsMul*critMul*abilityMul*mDeal etc.) with mDeal/mTake passed
|
||||
// explicitly so the handicap band is isolated from the stat/ability composition. Ability defaults: Overcharge.Magnitude=50,
|
||||
// Adrenaline.Magnitude=35, Berserk.Magnitude=1.5/Magnitude2=1.5. crit_damage Base=100 (so a no-investment crit is x2).
|
||||
public class CombatChainTests
|
||||
{
|
||||
|
||||
private static readonly Dictionary<string, int> MaxedOffense =
|
||||
new(StringComparer.Ordinal) { [StatKeys.Damage] = 5, [StatKeys.HeadshotDamage] = 5, [StatKeys.CritDamage] = 10 };
|
||||
|
||||
// ---- OffenseMultiplier (precomputed-mDeal overload) ----
|
||||
[Fact]
|
||||
public void Offense_maxed_headshot_crit()
|
||||
{
|
||||
// dmg +50% (1.5) * hs +50% (1.5) * crit +200% (3.0) * 1 * mDeal(1) = 6.75
|
||||
var s = T.Snap(upgrades: MaxedOffense);
|
||||
T.Close(6.75, CombatResolver.OffenseMultiplier(s, headshot: true, crit: true, T.StatDefs(), T.Abil(), T.BaseMaxHp, mDeal: 1.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Offense_maxed_bodyshot_no_crit_only_damage_applies()
|
||||
{
|
||||
var s = T.Snap(upgrades: MaxedOffense);
|
||||
T.Close(1.5, CombatResolver.OffenseMultiplier(s, headshot: false, crit: false, T.StatDefs(), T.Abil(), T.BaseMaxHp, mDeal: 1.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Offense_overcharge_multiplies_by_magnitude()
|
||||
{
|
||||
// no stats, Overcharge active -> abilityMul 1.5
|
||||
var s = T.Snap(overcharge: true);
|
||||
T.Close(1.5, CombatResolver.OffenseMultiplier(s, false, false, T.StatDefs(), T.Abil(), T.BaseMaxHp, 1.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Offense_berserk_passive_card_scales_with_missing_hp()
|
||||
{
|
||||
// berserk_passive card 120, Health 50/maxHp 100 -> missing 0.5 -> abilityMul 1 + 0.5*120/100 = 1.6
|
||||
var s = T.Snap(health: 50, cards: new Cards((CardKeys.BerserkPassive, 120)));
|
||||
T.Close(1.6, CombatResolver.OffenseMultiplier(s, false, false, T.StatDefs(), T.Abil(), T.BaseMaxHp, 1.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Offense_berserk_ability_scales_with_missing_hp()
|
||||
{
|
||||
// Berserk ability active, Health 25 -> missing 0.75; abilityMul 1 + 0.75*1.5 = 2.125
|
||||
var s = T.Snap(health: 25, berserk: true);
|
||||
T.Close(2.125, CombatResolver.OffenseMultiplier(s, false, false, T.StatDefs(), T.Abil(), T.BaseMaxHp, 1.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Offense_berserk_ability_crit_adds_to_crit_damage()
|
||||
{
|
||||
// crit (no crit_damage stat -> base x2 = 2.0) + Berserk crit bonus missing*Magnitude2 = 0.75*1.5 -> critMul 3.125;
|
||||
// abilityMul 2.125 -> 3.125 * 2.125 = 6.640625
|
||||
var s = T.Snap(health: 25, berserk: true);
|
||||
T.Close(6.640625, CombatResolver.OffenseMultiplier(s, false, crit: true, T.StatDefs(), T.Abil(), T.BaseMaxHp, 1.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Offense_abilities_disabled_ignores_active_flags()
|
||||
{
|
||||
var ab = T.Abil();
|
||||
ab.Enabled = false;
|
||||
var s = T.Snap(overcharge: true, berserk: true, health: 25); // flags set but abilities off
|
||||
T.Close(1.0, CombatResolver.OffenseMultiplier(s, false, false, T.StatDefs(), ab, T.BaseMaxHp, 1.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Offense_passes_mDeal_through_linearly()
|
||||
{
|
||||
var s = T.Snap(upgrades: MaxedOffense);
|
||||
// 6.75 * 0.1 = 0.675 (a dominant player's compressed deal band)
|
||||
T.Close(0.675, CombatResolver.OffenseMultiplier(s, true, true, T.StatDefs(), T.Abil(), T.BaseMaxHp, 0.1));
|
||||
}
|
||||
|
||||
// ---- DefenseMultiplier (precomputed-mTake overload) ----
|
||||
[Fact]
|
||||
public void Defense_plain_is_just_mTake() =>
|
||||
T.Close(1.0, CombatResolver.DefenseMultiplier(T.Snap(), headshot: false, T.Abil(), mTake: 1.0));
|
||||
|
||||
[Fact]
|
||||
public void Defense_adrenaline_reduces_take()
|
||||
{
|
||||
var s = T.Snap(adrenaline: true);
|
||||
T.Close(0.65, CombatResolver.DefenseMultiplier(s, false, T.Abil(), 1.0)); // *(1 - 35/100)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Defense_headshot_reduction_card_only_on_headshots()
|
||||
{
|
||||
var s = T.Snap(cards: new Cards((CardKeys.HsReduction, 45)));
|
||||
T.Close(1.0, CombatResolver.DefenseMultiplier(s, headshot: false, T.Abil(), 1.0)); // body: card dormant
|
||||
T.Close(0.55, CombatResolver.DefenseMultiplier(s, headshot: true, T.Abil(), 1.0)); // *(1 - 45/100)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Defense_headshot_reduction_over_hundred_clamps_to_zero()
|
||||
{
|
||||
var s = T.Snap(cards: new Cards((CardKeys.HsReduction, 120)));
|
||||
T.Close(0.0, CombatResolver.DefenseMultiplier(s, headshot: true, T.Abil(), 1.0)); // max(0, 1-1.2)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Defense_stacks_adrenaline_and_hs_card_on_mTake()
|
||||
{
|
||||
var s = T.Snap(adrenaline: true, cards: new Cards((CardKeys.HsReduction, 45)));
|
||||
T.Close(0.715, CombatResolver.DefenseMultiplier(s, headshot: true, T.Abil(), mTake: 2.0)); // 2 * 0.65 * 0.55
|
||||
}
|
||||
|
||||
// ---- the rh-overloads must equal "compute the band then call the precomputed overload" (same code path) ----
|
||||
[Fact]
|
||||
public void Offense_rh_overload_equals_precomputed_mDeal()
|
||||
{
|
||||
var s = T.Snap(level: 50, kills: 10, deaths: 4, streak: 5, upgrades: MaxedOffense);
|
||||
var rh = T.Resolved();
|
||||
double viaRh = CombatResolver.OffenseMultiplier(s, true, true, T.StatDefs(), rh, T.Abil(), T.BaseMaxHp);
|
||||
double viaBand = CombatResolver.OffenseMultiplier(s, true, true, T.StatDefs(), T.Abil(), T.BaseMaxHp, HandicapModel.MDeal(s, rh));
|
||||
Assert.Equal(viaBand, viaRh);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Defense_rh_overload_equals_precomputed_mTake()
|
||||
{
|
||||
var s = T.Snap(level: 50, kills: 10, deaths: 4, streak: 5, adrenaline: true);
|
||||
var rh = T.Resolved();
|
||||
double viaRh = CombatResolver.DefenseMultiplier(s, true, rh, T.Abil());
|
||||
double viaBand = CombatResolver.DefenseMultiplier(s, true, T.Abil(), HandicapModel.MTake(s, rh));
|
||||
Assert.Equal(viaBand, viaRh);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue