78 lines
4.8 KiB
C#
78 lines
4.8 KiB
C#
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;
|
||
}
|