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 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 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 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; }