74 lines
4.5 KiB
C#
74 lines
4.5 KiB
C#
using Outnumbered.Config;
|
|
|
|
namespace Outnumbered.Domain;
|
|
|
|
// Pure survival run-economy + escalation curves. The wave state machine + run banking stay engine-side; this owns the
|
|
// numbers they read.
|
|
public static class SurvivalEconomy
|
|
{
|
|
// Bots ALIVE at once this wave (simultaneous pressure), ramping from AliveBase to AliveCap.
|
|
public static int AliveForWave(int wave, SurvivalConfig c) =>
|
|
Math.Clamp(c.AliveBase + c.AlivePerWave * (wave - 1), 1, c.AliveCap);
|
|
|
|
// Total kills to clear a wave.
|
|
public static int WaveBudget(int wave, int aliveHumans, SurvivalConfig c) =>
|
|
Math.Max(1, c.BudgetBase + c.BudgetPerWave * (wave - 1) + c.BudgetPerPlayer * aliveHumans);
|
|
|
|
// Monotonic escalate-only handicap floor in t-space; -1 = no floor (idle / between runs).
|
|
public static double HandicapFloor(int wave, SurvivalConfig c) =>
|
|
// wave>=1 and Max(1,MaxNerfWave)>=1 => quotient>=0, so the lower clamp can never bind; Min(1,x) == Clamp(x,0,1) here.
|
|
wave <= 0 ? -1.0 : Math.Min(1.0, wave / (double)Math.Max(1, c.MaxNerfWave));
|
|
|
|
// Per-wave XP multiplier: ramps exponentially from x1 (wave 1) to xWinMult (the final wave). WinMult is the single
|
|
// knob (the FINAL-wave / "win" multiplier). Back-loaded by design so early waves pay ~nothing and the last few pay big.
|
|
// waveMult(w) = WinMult ^ ((w-1)/(WaveCount-1))
|
|
public static double WaveMult(int wave, SurvivalConfig c) =>
|
|
Math.Pow(c.WinMult, (wave - 1) / (double)Math.Max(1, c.WaveCount - 1));
|
|
|
|
// XP granted at the END of ONE cleared wave: raw wave XP (HP-damage + HS/crit, already xp_mult-card-scaled when banked)
|
|
// x prestige x the wave multiplier. NO per-run cap, NO XpBoost stat, NO handicap mult here — depth is the gate (you must
|
|
// survive + contribute to reach the big back-wave multipliers), and the handicap mult stays excluded so run XP can't
|
|
// re-couple to gameable K/D. 0 for a non-positive wave.
|
|
public static long WaveXpLump(double rawWaveXp, int wave, int prestige, SurvivalConfig c, ProgressionConfig p)
|
|
{
|
|
if (rawWaveXp <= 0) return 0;
|
|
double lump = rawWaveXp * ProgressionModel.PrestigeXpMultiplier(prestige, p) * WaveMult(wave, c);
|
|
return (long)Math.Floor(lump);
|
|
}
|
|
|
|
// Raw combat XP banked into the CURRENT wave's accumulator, scaled by the xp_mult card (so it compounds with the
|
|
// per-wave prestige x waveMult chain applied at grant time).
|
|
public static double AccrueWaveXp(double amount, double xpMultCardPct) =>
|
|
amount * (1.0 + xpMultCardPct / 100.0);
|
|
|
|
// Team-card squad multiplier (compounding): global_deal increases, global_take decreases.
|
|
public static double TeamMult(int level, double perPickPct, bool increase) =>
|
|
increase ? Math.Pow(1.0 + perPickPct / 100.0, level)
|
|
: Math.Pow(Math.Max(0.0, 1.0 - perPickPct / 100.0), level);
|
|
|
|
// ---- Field Medic teammate-revive scaling (all keyed on the medic's card LEVEL = FieldMedic picks; 0 = not a medic) ----
|
|
// Kept pure here so the numbers are testable and identical to what the engine-side channel machine (Survival.cs) reads.
|
|
public static double ReviveChannelSeconds(int medicLevel, SurvivalConfig c) =>
|
|
Math.Max(c.ReviveChannelMin, c.ReviveChannelSeconds - Math.Max(0, medicLevel - 1) * c.ReviveChannelPerLevel);
|
|
public static double ReviveHpFraction(int medicLevel, SurvivalConfig c) =>
|
|
Math.Clamp(c.ReviveHpBase + Math.Max(0, medicLevel - 1) * c.ReviveHpPerLevel, 0.05, 1.0);
|
|
public static int ReviveChargesForWave(int medicLevel, SurvivalConfig c) =>
|
|
medicLevel <= 0 ? 0 : medicLevel * Math.Max(0, c.ReviveChargesPerLevel);
|
|
|
|
// ---- HUD locator math (pure so the arrow direction + distance readout are golden-tested, not hidden behind a pawn) ----
|
|
// Wrap a degree delta into (-180, 180].
|
|
public static double NormalizeDeg(double d)
|
|
{
|
|
d %= 360.0;
|
|
if (d > 180) return d - 360;
|
|
if (d <= -180) return d + 360;
|
|
return d;
|
|
}
|
|
|
|
// Map a RELATIVE bearing (target bearing - view yaw, degrees) to one of 8 compass sectors: 0 = straight ahead, then
|
|
// counter-clockwise (positive = to the left). Indexes the caller's arrow table `{↑ ↖ ← ↙ ↓ ↘ → ↗}`.
|
|
public static int ArrowIndex(double relDeg) => ((int)Math.Round(NormalizeDeg(relDeg) / 45.0) % 8 + 8) % 8;
|
|
|
|
// The HUD "distance to the downed teammate" readout: game units -> whole metres, floored at 1 (a divisor <= 0 is guarded).
|
|
public static int LocatorMeters(double dist, double divisor) => Math.Max(1, (int)Math.Round(dist / Math.Max(1e-9, divisor)));
|
|
}
|