Survival mode bugfixes and revive mechanism
This commit is contained in:
parent
d701598350
commit
2fa85e8b1f
14 changed files with 529 additions and 35 deletions
|
|
@ -100,13 +100,43 @@ public class BalanceInvariantTests
|
|||
[Fact]
|
||||
public void Per_mode_override_applies_only_named_fields()
|
||||
{
|
||||
// The survival override down-weights the skill factors so the WAVE FLOOR drives difficulty; everything else inherits.
|
||||
// The survival override zeroes every skill factor so the WAVE FLOOR alone drives difficulty; everything else inherits.
|
||||
var baseH = T.Hcap();
|
||||
var ov = T.Surv().Handicap!; // the default survival override
|
||||
var eff = ov.ApplyTo(baseH);
|
||||
Assert.Equal(0.3, eff.KdWeight); // overridden
|
||||
Assert.Equal(0.0, eff.KdWeight); // overridden -> in-run performance cannot move the handicap
|
||||
Assert.Equal(0.0, eff.HsWeight);
|
||||
Assert.Equal(0.0, eff.StreakWeight);
|
||||
Assert.Equal(0.0, eff.LevelWeight);
|
||||
Assert.Equal(0.0, eff.ProgressWeight);
|
||||
Assert.Equal(10.0, eff.MTakeCeiling); // overridden
|
||||
Assert.Equal(baseH.MasterDifficulty, eff.MasterDifficulty); // inherited from base (not set in the override)
|
||||
Assert.Equal(baseH.Curve, eff.Curve); // inherited
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Survival_handicap_is_wave_only_and_invariant_to_performance()
|
||||
{
|
||||
// Bug fix: survival's handicap must depend ONLY on the wave (via the floor), never on K/D / headshots / streak /
|
||||
// level. A dominant player and a struggling player on the SAME wave get the SAME t, and killing more bots inside a
|
||||
// wave never changes it — so the 1st and the 100th kill of a wave take identical shots. (Before, K/D + HS saturated
|
||||
// after a handful of kills and drove a good player straight to the floor mid-wave-1.)
|
||||
var rh = T.Resolved(T.Surv().Handicap!.ApplyTo(T.Hcap()));
|
||||
|
||||
foreach (var wave in new[] { 1, 5, 12, 25 })
|
||||
{
|
||||
double floor = SurvivalEconomy.HandicapFloor(wave, T.Surv());
|
||||
double dominant = HandicapModel.ComputeT(
|
||||
T.Snap(level: 100, kills: 60, deaths: 0, headshotKills: 60, streak: 50, floor: floor), rh);
|
||||
double struggling = HandicapModel.ComputeT(
|
||||
T.Snap(level: 1, kills: 0, deaths: 20, headshotKills: 0, streak: 0, floor: floor), rh);
|
||||
double freshMidWave = HandicapModel.ComputeT(
|
||||
T.Snap(level: 1, kills: 7, deaths: 0, headshotKills: 7, streak: 7, floor: floor), rh);
|
||||
|
||||
Assert.Equal(dominant, struggling, Eps); // same wave -> same handicap, regardless of performance
|
||||
Assert.Equal(dominant, freshMidWave, Eps);
|
||||
// it IS just the eased wave floor (floor >= 0 for any real wave, so Ease reduces to Pow)
|
||||
Assert.Equal(Math.Pow(floor, rh.Config.Curve), dominant, Eps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,4 +110,79 @@ public class SurvivalEconomyTests
|
|||
[InlineData(2, 50, true, 2.25)]
|
||||
public void TeamMult_matches(int level, double perPick, bool increase, double expected) =>
|
||||
T.Close(expected, SurvivalEconomy.TeamMult(level, perPick, increase));
|
||||
|
||||
// ---- Field Medic revive scaling (defaults: channel 4.0 -0.7/lvl min 1.5; HP 0.35 +0.1/lvl; charges = lvl*1) ----
|
||||
// channelSeconds = max(ReviveChannelMin, ReviveChannelSeconds - (lvl-1)*ReviveChannelPerLevel).
|
||||
[Theory]
|
||||
[InlineData(1, 4.0)]
|
||||
[InlineData(2, 3.3)]
|
||||
[InlineData(3, 2.6)]
|
||||
[InlineData(10, 1.5)] // -0.7*9 undershoots the 1.5 floor -> clamped
|
||||
public void ReviveChannelSeconds_scales_and_floors(int level, double expected) =>
|
||||
T.Close(expected, SurvivalEconomy.ReviveChannelSeconds(level, T.Surv()));
|
||||
|
||||
// hpFraction = clamp(ReviveHpBase + (lvl-1)*ReviveHpPerLevel, 0.05, 1.0).
|
||||
[Theory]
|
||||
[InlineData(1, 0.35)]
|
||||
[InlineData(2, 0.45)]
|
||||
[InlineData(3, 0.55)]
|
||||
[InlineData(20, 1.0)] // upper clamp
|
||||
public void ReviveHpFraction_scales_and_clamps(int level, double expected) =>
|
||||
T.Close(expected, SurvivalEconomy.ReviveHpFraction(level, T.Surv()));
|
||||
|
||||
// chargesForWave = lvl <= 0 ? 0 : lvl * ReviveChargesPerLevel. Card-only: level 0 (no card) grants nothing.
|
||||
[Theory]
|
||||
[InlineData(0, 0)] // not a medic -> no revives
|
||||
[InlineData(1, 1)]
|
||||
[InlineData(2, 2)]
|
||||
[InlineData(3, 3)]
|
||||
public void ReviveChargesForWave_scales(int level, int expected) =>
|
||||
Assert.Equal(expected, SurvivalEconomy.ReviveChargesForWave(level, T.Surv()));
|
||||
|
||||
[Fact]
|
||||
public void FieldMedic_card_is_in_the_catalog() // card-only unlock lives on the draft catalog, cap 3
|
||||
{
|
||||
var card = T.Surv().Cards.Find(c => c.Key == CardKeys.FieldMedic);
|
||||
Assert.NotNull(card);
|
||||
Assert.Equal(3, card!.Cap);
|
||||
}
|
||||
|
||||
// ---- HUD locator math (the arrow direction players are sent + the distance readout) ----
|
||||
// NormalizeDeg wraps to the canonical (-180, 180] (so -180 -> +180).
|
||||
[Theory]
|
||||
[InlineData(0, 0)]
|
||||
[InlineData(45, 45)]
|
||||
[InlineData(180, 180)]
|
||||
[InlineData(-180, 180)]
|
||||
[InlineData(270, -90)]
|
||||
[InlineData(-270, 90)]
|
||||
[InlineData(360, 0)]
|
||||
[InlineData(450, 90)]
|
||||
public void NormalizeDeg_wraps_to_half_open(double input, double expected) =>
|
||||
T.Close(expected, SurvivalEconomy.NormalizeDeg(input));
|
||||
|
||||
// ArrowIndex: relative bearing -> sector. arrows table = { ↑ ↖ ← ↙ ↓ ↘ → ↗ } indexed 0..7; 0 = ahead, CCW = left.
|
||||
[Theory]
|
||||
[InlineData(0, 0)] // ↑ ahead
|
||||
[InlineData(45, 1)] // ↖ ahead-left
|
||||
[InlineData(90, 2)] // ← left
|
||||
[InlineData(135, 3)] // ↙ behind-left
|
||||
[InlineData(180, 4)] // ↓ behind
|
||||
[InlineData(-135, 5)] // ↘ behind-right
|
||||
[InlineData(-90, 6)] // → right
|
||||
[InlineData(-45, 7)] // ↗ ahead-right
|
||||
[InlineData(360, 0)] // wraps to ahead
|
||||
[InlineData(23, 1)] // rounds up into the ↖ sector
|
||||
[InlineData(22, 0)] // rounds down into the ↑ sector
|
||||
public void ArrowIndex_maps_bearing_to_sector(double relDeg, int expected) =>
|
||||
Assert.Equal(expected, SurvivalEconomy.ArrowIndex(relDeg));
|
||||
|
||||
// LocatorMeters = max(1, round(dist/divisor)); floors at 1m so a downed ally never reads "0m".
|
||||
[Theory]
|
||||
[InlineData(0, 40, 1)] // floor
|
||||
[InlineData(20, 40, 1)] // round(0.5)=0 -> floored to 1
|
||||
[InlineData(120, 40, 3)]
|
||||
[InlineData(200, 40, 5)]
|
||||
public void LocatorMeters_floors_at_one(double dist, double divisor, int expected) =>
|
||||
Assert.Equal(expected, SurvivalEconomy.LocatorMeters(dist, divisor));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue