initial commit
This commit is contained in:
commit
d701598350
67 changed files with 9351 additions and 0 deletions
78
Outnumbered/Domain/CombatResolver.cs
Normal file
78
Outnumbered/Domain/CombatResolver.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue