cs2-outnumbered/Outnumbered/Domain/CombatResolver.cs
Kamal Tufekcic d701598350
All checks were successful
CI / build (push) Successful in 32s
CI / release (push) Successful in 32s
CI / lint (push) Successful in 30s
initial commit
2026-07-05 13:28:35 +03:00

78 lines
4.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Frozen;
using Outnumbered.Config;
namespace Outnumbered.Domain;
// The offense + defense multiplier chains — the SINGLE source of truth shared by the damage hook (applies them) and the
// HUD readout (shows them). Pure: the crit decision (which has engine side effects: a sound + the crit-XP marker) is made
// by the caller and passed in as `crit`; the HUD passes crit:false. Lifesteal/thorns amounts are here too so the reactive
// sustain glue applies identical numbers.
public static class CombatResolver
{
public static double CritChance(in PlayerSnapshot s, FrozenDictionary<string, StatDef> defs) =>
StatResolver.EffRun(s, StatKeys.CritChance, defs);
// The damage hook path: computes its own MDeal (one ComputeT per hit). Delegates to the mDeal-taking overload.
public static double OffenseMultiplier(in PlayerSnapshot s, bool headshot, bool crit,
FrozenDictionary<string, StatDef> defs, in ResolvedHandicap rh, AbilitiesConfig ab, int baseMaxHp) =>
OffenseMultiplier(s, headshot, crit, defs, ab, baseMaxHp, HandicapModel.MDeal(s, rh));
// Overload taking a PRECOMPUTED MDeal — lets the HUD share ONE ComputeT across deal/take/xp (via HandicapModel.Bands).
// The missing-HP fraction (an EffRun lookup + a divide) is computed only when a Berserk consumer is actually active.
public static double OffenseMultiplier(in PlayerSnapshot s, bool headshot, bool crit,
FrozenDictionary<string, StatDef> defs, AbilitiesConfig ab, int baseMaxHp, double mDeal)
{
double dmgMul = 1.0 + StatResolver.EffRun(s, StatKeys.Damage, defs) / 100.0;
double hsMul = headshot ? 1.0 + StatResolver.EffRun(s, StatKeys.HeadshotDamage, defs) / 100.0 : 1.0;
double critMul = crit ? 1.0 + StatResolver.EffRun(s, StatKeys.CritDamage, defs) / 100.0 : 1.0;
double abilityMul = 1.0;
double berserkCard = StatResolver.CardMag(s, CardKeys.BerserkPassive); // always-on passive card
bool needMissing = berserkCard > 0 || (ab.Enabled && s.BerserkActive); // the only consumers of `missing`
double missing = needMissing ? StatResolver.MissingHpFraction(s, StatResolver.MaxHp(s, defs, baseMaxHp)) : 0.0;
if (ab.Enabled)
{
if (s.OverchargeActive) abilityMul *= 1.0 + ab.Overcharge.Magnitude / 100.0;
if (s.BerserkActive)
{
abilityMul *= 1.0 + missing * ab.Berserk.Magnitude;
if (crit) critMul += missing * ab.Berserk.Magnitude2;
}
}
if (berserkCard > 0) abilityMul *= 1.0 + missing * berserkCard / 100.0;
return dmgMul * hsMul * critMul * abilityMul * mDeal;
}
// The damage hook path: computes its own MTake. Delegates to the mTake-taking overload.
public static double DefenseMultiplier(in PlayerSnapshot s, bool headshot, in ResolvedHandicap rh, AbilitiesConfig ab) =>
DefenseMultiplier(s, headshot, ab, HandicapModel.MTake(s, rh));
// Overload taking a PRECOMPUTED MTake (the global_take team card is already folded in by HandicapModel).
public static double DefenseMultiplier(in PlayerSnapshot s, bool headshot, AbilitiesConfig ab, double mTake)
{
double take = mTake;
if (ab.Enabled && s.AdrenalineActive) take *= 1.0 - ab.Adrenaline.Magnitude / 100.0;
if (headshot)
{
double hsCut = StatResolver.CardMag(s, CardKeys.HsReduction); // armor doesn't help vs HS; this does
if (hsCut > 0) take *= Math.Max(0.0, 1.0 - hsCut / 100.0);
}
return take;
}
// ---- pure amounts the reactive-sustain glue applies (it still does the engine HP/armor writes) ----
// dmgHealth is a double so BOTH callers share one formula bit-identically: the on-hit path passes the int
// ev.DmgHealth (widens exactly) with the crit multiplier; the thorns-reflect path passes the FLOAT `dealt`
// (the offense-scaled reflected damage — keeps full precision, no pre-truncation) with critMult=1.0 (×1.0 is exact).
public static int LifestealHeal(double dmgHealth, double lifestealPct, double critMult, int minHeal) =>
Math.Max(minHeal, (int)Math.Round(dmgHealth * lifestealPct / 100.0 * critMult));
public static int ArmorLifestealGain(double dmgHealth, double armorLifestealPct, double critMult) =>
(int)Math.Round(dmgHealth * armorLifestealPct / 100.0 * critMult);
// % of the damage ACTUALLY taken (health + armor, so it already includes the MTake handicap that sized the hit — a 5x
// handicap 250 HP hit at 10% is 25). The caller deals this FLAT (DamageDealer flat) so the bot eats exactly this: no
// build and no MDeal are re-applied on the way out.
public static double ThornsReflect(int dmgHealth, int dmgArmor, double thornsPct) => (dmgHealth + dmgArmor) * thornsPct / 100.0;
}