Rework damage stats calculation to always expose full breacher info

This commit is contained in:
DarkPhoenix
2024-11-13 04:37:15 +01:00
parent f0a72f4307
commit 6074cfe15a
9 changed files with 166 additions and 192 deletions

View File

@@ -19,6 +19,7 @@
import math
from copy import deepcopy
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
@@ -29,7 +30,7 @@ from eos.saveddata.mutatedMixin import MutatedMixin, MutaError
from eos.saveddata.mutator import MutatorDrone
from eos.utils.cycles import CycleInfo
from eos.utils.default import DEFAULT
from eos.utils.stats import BaseVolleyStats, DmgTypes, RRTypes
from eos.utils.stats import DmgTypes, RRTypes
pyfalog = Logger(__name__)
@@ -165,12 +166,13 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
if self.__baseVolley is None:
dmgGetter = self.getModifiedChargeAttr if self.hasAmmo else self.getModifiedItemAttr
dmgMult = self.amountActive * (self.getModifiedItemAttr("damageMultiplier", 1))
self.__baseVolley = BaseVolleyStats(
self.__baseVolley = DmgTypes(
em=(dmgGetter("emDamage", 0)) * dmgMult,
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
volley = DmgTypes.from_base_and_profile(base=self.__baseVolley, tgtProfile=targetProfile)
volley = deepcopy(self.__baseVolley)
volley.profile = targetProfile
return {0: volley}
def getVolley(self, targetProfile=None):
@@ -184,12 +186,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
if cycleParams is None:
return DmgTypes.default()
dpsFactor = 1 / (cycleParams.averageTime / 1000)
dps = DmgTypes(
em=volley.em * dpsFactor,
thermal=volley.thermal * dpsFactor,
kinetic=volley.kinetic * dpsFactor,
explosive=volley.explosive * dpsFactor,
breacher=volley.breacher * dpsFactor)
dps = volley * dpsFactor
return dps
def isRemoteRepping(self, ignoreState=False):

View File

@@ -19,6 +19,7 @@
import math
from copy import deepcopy
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
@@ -198,17 +199,14 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
for ability in self.abilities:
# Not passing resists here as we want to calculate and store base volley
self.__baseVolley[ability.effectID] = {0: ability.getVolley()}
adjustedVolley = {}
adjustedVolleys = {}
for effectID, effectData in self.__baseVolley.items():
adjustedVolley[effectID] = {}
for volleyTime, volleyValue in effectData.items():
adjustedVolley[effectID][volleyTime] = DmgTypes(
em=volleyValue.em * (1 - getattr(targetProfile, "emAmount", 0)),
thermal=volleyValue.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=volleyValue.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=volleyValue.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)),
breacher=volleyValue.breacher)
return adjustedVolley
adjustedVolleys[effectID] = {}
for volleyTime, baseVolley in effectData.items():
adjustedVolley = deepcopy(baseVolley)
adjustedVolley.profile = targetProfile
adjustedVolleys[effectID][volleyTime] = adjustedVolley
return adjustedVolleys
def getVolleyPerEffect(self, targetProfile=None):
volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile)

View File

@@ -128,12 +128,8 @@ class FighterAbility:
kin = self.fighter.getModifiedItemAttr("{}DamageKin".format(self.attrPrefix), 0)
exp = self.fighter.getModifiedItemAttr("{}DamageExp".format(self.attrPrefix), 0)
dmgMult = self.fighter.amount * self.fighter.getModifiedItemAttr("{}DamageMultiplier".format(self.attrPrefix), 1)
volley = DmgTypes(
em=em * dmgMult * (1 - getattr(targetProfile, "emAmount", 0)),
thermal=therm * dmgMult * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=kin * dmgMult * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=exp * dmgMult * (1 - getattr(targetProfile, "explosiveAmount", 0)),
breacher=0)
volley = DmgTypes(em=em * dmgMult, thermal=therm * dmgMult, kinetic=kin * dmgMult, explosive=exp * dmgMult)
volley.profile = targetProfile
return volley
def getDps(self, targetProfile=None, cycleTimeOverride=None):
@@ -142,12 +138,7 @@ class FighterAbility:
return DmgTypes.default()
cycleTime = cycleTimeOverride if cycleTimeOverride is not None else self.cycleTime
dpsFactor = 1 / (cycleTime / 1000)
dps = DmgTypes(
em=volley.em * dpsFactor,
thermal=volley.thermal * dpsFactor,
kinetic=volley.kinetic * dpsFactor,
explosive=volley.explosive * dpsFactor,
breacher=volley.breacher * dpsFactor)
dps = volley * dpsFactor
return dps
def clear(self):

View File

@@ -1692,8 +1692,11 @@ class Fit:
weaponDps = DmgTypes.default()
for mod in self.modules:
weaponVolley += mod.getVolley(spoolOptions=spoolOptions, targetProfile=self.targetProfile)
weaponDps += mod.getDps(spoolOptions=spoolOptions, targetProfile=self.targetProfile)
weaponVolley += mod.getVolley(spoolOptions=spoolOptions)
weaponDps += mod.getDps(spoolOptions=spoolOptions)
weaponVolley.profile = self.targetProfile
weaponDps.profile = self.targetProfile
self.__weaponVolleyMap[spoolOptions] = weaponVolley
self.__weaponDpsMap[spoolOptions] = weaponDps
@@ -1703,12 +1706,15 @@ class Fit:
droneDps = DmgTypes.default()
for drone in self.drones:
droneVolley += drone.getVolley(targetProfile=self.targetProfile)
droneDps += drone.getDps(targetProfile=self.targetProfile)
droneVolley += drone.getVolley()
droneDps += drone.getDps()
for fighter in self.fighters:
droneVolley += fighter.getVolley(targetProfile=self.targetProfile)
droneDps += fighter.getDps(targetProfile=self.targetProfile)
droneVolley += fighter.getVolley()
droneDps += fighter.getDps()
droneVolley.profile = self.targetProfile
droneDps.profile = self.targetProfile
self.__droneDps = droneDps
self.__droneVolley = droneVolley

View File

@@ -33,7 +33,7 @@ from eos.utils.cycles import CycleInfo, CycleSequence
from eos.utils.default import DEFAULT
from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import calculateSpoolup, resolveSpoolOptions
from eos.utils.stats import BaseVolleyStats, BreacherInfo, DmgTypes, RRTypes
from eos.utils.stats import BreacherInfo, DmgTypes, RRTypes
pyfalog = Logger(__name__)
@@ -483,7 +483,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
absolute=self.getModifiedChargeAttr("dotMaxDamagePerTick", 0),
relative=self.getModifiedChargeAttr("dotMaxHPPercentagePerTick", 0) / 100)
for i in range(subcycles):
self.__baseVolley[dmgDelay + i] = BaseVolleyStats(0, 0, 0, 0, breachers=[breacher_info])
volley = DmgTypes.default()
volley.add_breacher(dmgDelay + i, breacher_info)
self.__baseVolley[dmgDelay + i] = volley
else:
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
dmgMult = self.getModifiedItemAttr("damageMultiplier", 1)
@@ -502,7 +504,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
else:
subcycles = 1
for i in range(subcycles):
self.__baseVolley[dmgDelay + dmgSubcycle * i] = BaseVolleyStats(
self.__baseVolley[dmgDelay + dmgSubcycle * i] = DmgTypes(
em=(dmgGetter("emDamage", 0)) * dmgMult,
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
@@ -513,11 +515,12 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
self.getModifiedItemAttr("damageMultiplierBonusPerCycle", 0),
self.rawCycleTime / 1000, spoolType, spoolAmount)[0]
spoolMultiplier = 1 + spoolBoost
adjustedVolley = {}
for volleyTime, baseValue in self.__baseVolley.items():
adjustedVolley[volleyTime] = DmgTypes.from_base_and_profile(
base=baseValue, tgtProfile=targetProfile, mult=spoolMultiplier)
return adjustedVolley
adjustedVolleys = {}
for volleyTime, baseVolley in self.__baseVolley.items():
adjustedVolley = baseVolley * spoolMultiplier
adjustedVolley.profile = targetProfile
adjustedVolleys[volleyTime] = adjustedVolley
return adjustedVolleys
def getVolley(self, spoolOptions=None, targetProfile=None, ignoreState=False):
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
@@ -525,31 +528,22 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
return DmgTypes.default()
return volleyParams[min(volleyParams)]
def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False, getSpreadDPS=False):
dmgDuringCycle = DmgTypes.default()
cycleParams = self.getCycleParametersForDps()
def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False):
dps = DmgTypes.default()
cycleParams = self.getCycleParameters()
if cycleParams is None:
return dmgDuringCycle
return dps
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
avgCycleTime = cycleParams.averageTime
if len(volleyParams) == 0 or avgCycleTime == 0:
return dmgDuringCycle
for volleyValue in volleyParams.values():
dmgDuringCycle += volleyValue
dpsFactor = 1 / (avgCycleTime / 1000)
dps = DmgTypes(
em=dmgDuringCycle.em * dpsFactor,
thermal=dmgDuringCycle.thermal * dpsFactor,
kinetic=dmgDuringCycle.kinetic * dpsFactor,
explosive=dmgDuringCycle.explosive * dpsFactor,
breacher=dmgDuringCycle.breacher * dpsFactor)
if not getSpreadDPS:
return dps
return {'em': dmgDuringCycle.em * dpsFactor,
'therm': dmgDuringCycle.thermal * dpsFactor,
'kin': dmgDuringCycle.kinetic * dpsFactor,
'exp': dmgDuringCycle.explosive * dpsFactor,
'breach': dmgDuringCycle.breach * dpsFactor}
if self.isBreacher:
return volleyParams[min(volleyParams)]
for volleyValue in volleyParams.values():
dps += volleyValue
dpsFactor = 1 / (avgCycleTime / 1000)
dps *= dpsFactor
return dps
def isRemoteRepping(self, ignoreState=False):
repParams = self.getRepAmountParameters(ignoreState=ignoreState)
@@ -961,13 +955,6 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
and ((gang and effect.isType("gang")) or not gang):
effect.handler(fit, self, context, projectionRange, effect=effect)
def getCycleParametersForDps(self, reloadOverride=None):
# Special hack for breachers, since those are DoT and work independently of gun cycle
if self.isBreacher:
return CycleInfo(activeTime=1000, inactiveTime=0, quantity=math.inf, isInactivityReload=False)
else:
return self.getCycleParameters(reloadOverride=reloadOverride)
def getCycleParameters(self, reloadOverride=None):
"""Copied from new eos as well"""
# Determine if we'll take into account reload time or not

View File

@@ -19,7 +19,7 @@
import math
from typing import NamedTuple
from collections import defaultdict
from eos.utils.float import floatUnerr
from utils.repr import makeReprStr
@@ -29,76 +29,85 @@ def _t(x):
return x
class BreacherInfo(NamedTuple):
absolute: float
relative: float
class BreacherInfo:
def __init__(self, absolute, relative):
self.absolute = absolute
self.relative = relative
def __mul__(self, mul):
return type(self)(absolute=self.absolute * mul, relative=self.relative * mul)
def __imul__(self, mul):
if mul == 1:
return self
self.absolute *= mul
self.relative *= mul
return self
class BaseVolleyStats:
class DmgTypes:
"""
Container for volley stats, which stores breacher pod data
in raw form, before application of it to target profile.
"""
def __init__(self, em, thermal, kinetic, explosive, breachers=None):
self.em = em
self.thermal = thermal
self.kinetic = kinetic
self.explosive = explosive
self.breachers = [] if breachers is None else breachers
def __init__(self, em, thermal, kinetic, explosive):
self._em = em
self._thermal = thermal
self._kinetic = kinetic
self._explosive = explosive
self._breachers = defaultdict(lambda: [])
self.profile = None
def add_breacher(self, key, data):
self._breachers[key].append(data)
@classmethod
def default(cls):
return cls(0, 0, 0, 0)
def __eq__(self, other):
if not isinstance(other, BaseVolleyStats):
return NotImplemented
# Round for comparison's sake because often damage profiles are
# generated from data which includes float errors
return (
floatUnerr(self.em) == floatUnerr(other.em) and
floatUnerr(self.thermal) == floatUnerr(other.thermal) and
floatUnerr(self.kinetic) == floatUnerr(other.kinetic) and
floatUnerr(self.explosive) == floatUnerr(other.explosive) and
sorted(self.breachers) == sorted(other.breachers))
@property
def em(self):
dmg = self._em
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "emAmount", 0)
return dmg
def __bool__(self):
return any((
self.em, self.thermal, self.kinetic, self.explosive,
any(b.absolute or b.relative for b in self.breachers)))
@property
def thermal(self):
dmg = self._thermal
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "thermalAmount", 0)
return dmg
def __repr__(self):
class_name = type(self).__name__
return (f'<{class_name}(em={self.em}, thermal={self.thermal}, kinetic={self.kinetic}, '
f'explosive={self.explosive}, breachers={len(self.breachers)})>')
@property
def kinetic(self):
dmg = self._kinetic
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "kineticAmount", 0)
return dmg
@property
def explosive(self):
dmg = self._explosive
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "explosiveAmount", 0)
return dmg
class DmgTypes:
"""Container for damage data stats."""
@property
def pure(self):
if self.profile is None:
return sum(
max((b.absolute for b in bs), default=0)
for bs in self._breachers.values())
return sum(
max((min(b.absolute, b.relative * getattr(self.profile, "hp", math.inf)) for b in bs), default=0)
for bs in self._breachers.values())
def __init__(self, em, thermal, kinetic, explosive, breacher):
self.em = em
self.thermal = thermal
self.kinetic = kinetic
self.explosive = explosive
self.breacher = breacher
self._calcTotal()
@classmethod
def default(cls):
return cls(0, 0, 0, 0, 0)
@classmethod
def from_base_and_profile(cls, base, tgtProfile, mult=1):
return cls(
em=base.em * mult * (1 - getattr(tgtProfile, "emAmount", 0)),
thermal=base.thermal * mult * (1 - getattr(tgtProfile, "thermalAmount", 0)),
kinetic=base.kinetic * mult * (1 - getattr(tgtProfile, "kineticAmount", 0)),
explosive=base.explosive * mult * (1 - getattr(tgtProfile, "explosiveAmount", 0)),
breacher=max(
(min(b.absolute, b.relative * getattr(tgtProfile, "hp", math.inf)) for b in base.breachers),
default=0) * mult)
@property
def total(self):
return self.em + self.thermal + self.kinetic + self.explosive + self.pure
# Iterator is needed to support tuple-style unpacking
def __iter__(self):
@@ -109,88 +118,73 @@ class DmgTypes:
yield self.pure
yield self.total
@property
def pure(self):
return self.breacher
def __eq__(self, other):
if not isinstance(other, DmgTypes):
return NotImplemented
# Round for comparison's sake because often damage profiles are
# generated from data which includes float errors
return (
floatUnerr(self.em) == floatUnerr(other.em) and
floatUnerr(self.thermal) == floatUnerr(other.thermal) and
floatUnerr(self.kinetic) == floatUnerr(other.kinetic) and
floatUnerr(self.explosive) == floatUnerr(other.explosive) and
floatUnerr(self.breacher) == floatUnerr(other.breacher) and
floatUnerr(self.total) == floatUnerr(other.total))
def __bool__(self):
return any((
self.em, self.thermal, self.kinetic, self.explosive,
self.breacher, self.total))
def _calcTotal(self):
self.total = self.em + self.thermal + self.kinetic + self.explosive + self.breacher
floatUnerr(self._em) == floatUnerr(other._em) and
floatUnerr(self._thermal) == floatUnerr(other._thermal) and
floatUnerr(self._kinetic) == floatUnerr(other._kinetic) and
floatUnerr(self._explosive) == floatUnerr(other._explosive) and
sorted(self._breachers) == sorted(other._breachers),
self.profile == other.profile)
def __add__(self, other):
return type(self)(
em=self.em + other.em,
thermal=self.thermal + other.thermal,
kinetic=self.kinetic + other.kinetic,
explosive=self.explosive + other.explosive,
breacher=max(self.breacher, other.breacher))
new = type(self)(
em=self._em + other._em,
thermal=self._thermal + other._thermal,
kinetic=self._kinetic + other._kinetic,
explosive=self._explosive + other._explosive)
new.profile = self.profile
for k, v in self._breachers.items():
new._breachers[k].extend(v)
for k, v in other._breachers.items():
new._breachers[k].extend(v)
return new
def __iadd__(self, other):
self.em += other.em
self.thermal += other.thermal
self.kinetic += other.kinetic
self.explosive += other.explosive
self.breacher = max(self.breacher, other.breacher)
self._calcTotal()
self._em += other._em
self._thermal += other._thermal
self._kinetic += other._kinetic
self._explosive += other._explosive
for k, v in other._breachers.items():
self._breachers[k].extend(v)
return self
def __mul__(self, mul):
return type(self)(
em=self.em * mul,
thermal=self.thermal * mul,
kinetic=self.kinetic * mul,
explosive=self.explosive * mul,
breacher=self.breacher * mul)
new = type(self)(
em=self._em * mul,
thermal=self._thermal * mul,
kinetic=self._kinetic * mul,
explosive=self._explosive * mul)
new.profile = self.profile
for k, v in self._breachers.items():
new._breachers[k] = [b * mul for b in v]
return new
def __imul__(self, mul):
if mul == 1:
return
self.em *= mul
self.thermal *= mul
self.kinetic *= mul
self.explosive *= mul
self.breacher *= mul
self._calcTotal()
return self
self._em *= mul
self._thermal *= mul
self._kinetic *= mul
self._explosive *= mul
for v in self._breachers.values():
for b in v:
b *= mul
return self
def __truediv__(self, div):
return type(self)(
em=self.em / div,
thermal=self.thermal / div,
kinetic=self.kinetic / div,
explosive=self.explosive / div,
breacher=self.breacher / div)
def __itruediv__(self, div):
if div == 1:
return
self.em /= div
self.thermal /= div
self.kinetic /= div
self.explosive /= div
self.breacher /= div
self._calcTotal()
return self
def __bool__(self):
return any((
self._em, self._thermal, self._kinetic, self._explosive,
any(b.absolute or b.relative for b in self._breachers)))
def __repr__(self):
return makeReprStr(self, spec=['em', 'thermal', 'kinetic', 'explosive', 'breacher', 'total'])
class_name = type(self).__name__
return (f'<{class_name}(em={self._em}, thermal={self._thermal}, kinetic={self._kinetic}, '
f'explosive={self._explosive}, breachers={len(self._breachers)})>')
@staticmethod
def names(short=None, postProcessor=None, includePure=False):

View File

@@ -176,7 +176,7 @@ class TimeCache(FitDataCache):
for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
cycleParams = mod.getCycleParametersForDps(reloadOverride=True)
cycleParams = mod.getCycleParameters(reloadOverride=True)
if cycleParams is None:
continue
currentTime = 0

View File

@@ -215,13 +215,13 @@ class FirepowerViewFull(StatsView):
val = val() if fit is not None else None
preSpoolVal = preSpoolVal() if fit is not None else None
fullSpoolVal = fullSpoolVal() if fit is not None else None
if self._cachedValues[counter] != val:
if self._cachedValues[counter] != getattr(val, 'total', None):
tooltipText = dpsToolTip(val, preSpoolVal, fullSpoolVal, prec, lowest, highest)
label.SetLabel(valueFormat.format(
formatAmount(0 if val is None else val.total, prec, lowest, highest),
"\u02e2" if hasSpoolUp(preSpoolVal, fullSpoolVal) else ""))
label.SetToolTip(wx.ToolTip(tooltipText))
self._cachedValues[counter] = val
self._cachedValues[counter] = getattr(val, 'total', None)
counter += 1
self.panel.Layout()

View File

@@ -423,7 +423,8 @@ class EfsPort:
else:
maxRange = stats.maxRange
dps_spread_dict = stats.getDps(spoolOptions=spoolOptions, getSpreadDPS=True)
dps = stats.getDps(spoolOptions=spoolOptions)
dps_spread_dict = {'em': dps.em, 'therm': dps.thermal, 'kin': dps.kinetic, 'exp': dps.explosive, 'pure': dps.pure}
dps_spread_dict.update((x, y*n) for x, y in dps_spread_dict.items())
statDict = {