diff --git a/eos/saveddata/drone.py b/eos/saveddata/drone.py index cdefc1799..8fc0a58e8 100644 --- a/eos/saveddata/drone.py +++ b/eos/saveddata/drone.py @@ -29,7 +29,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 DmgTypes, RRTypes +from eos.utils.stats import BaseVolleyStats, DmgTypes, RRTypes pyfalog = Logger(__name__) @@ -161,20 +161,16 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu def getVolleyParameters(self, targetProfile=None): if not self.dealsDamage or self.amountActive <= 0: - return {0: DmgTypes(0, 0, 0, 0)} + return {0: DmgTypes.default()} if self.__baseVolley is None: dmgGetter = self.getModifiedChargeAttr if self.hasAmmo else self.getModifiedItemAttr dmgMult = self.amountActive * (self.getModifiedItemAttr("damageMultiplier", 1)) - self.__baseVolley = DmgTypes( + self.__baseVolley = BaseVolleyStats( em=(dmgGetter("emDamage", 0)) * dmgMult, thermal=(dmgGetter("thermalDamage", 0)) * dmgMult, kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult, explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult) - volley = DmgTypes( - em=self.__baseVolley.em * (1 - getattr(targetProfile, "emAmount", 0)), - thermal=self.__baseVolley.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)), - kinetic=self.__baseVolley.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)), - explosive=self.__baseVolley.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0))) + volley = DmgTypes.from_base_and_profile(base=self.__baseVolley, tgtProfile=targetProfile) return {0: volley} def getVolley(self, targetProfile=None): @@ -183,16 +179,17 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu def getDps(self, targetProfile=None): volley = self.getVolley(targetProfile=targetProfile) if not volley: - return DmgTypes(0, 0, 0, 0) + return DmgTypes.default() cycleParams = self.getCycleParameters() if cycleParams is None: - return DmgTypes(0, 0, 0, 0) + 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) + explosive=volley.explosive * dpsFactor, + breacher=volley.breacher * dpsFactor) return dps def isRemoteRepping(self, ignoreState=False): diff --git a/eos/saveddata/fighter.py b/eos/saveddata/fighter.py index 012e0b8ce..c07e36cd2 100644 --- a/eos/saveddata/fighter.py +++ b/eos/saveddata/fighter.py @@ -206,7 +206,8 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): 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))) + explosive=volleyValue.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)), + breacher=volleyValue.breacher) return adjustedVolley def getVolleyPerEffect(self, targetProfile=None): @@ -218,28 +219,16 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def getVolley(self, targetProfile=None): volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile) - em = 0 - therm = 0 - kin = 0 - exp = 0 + volley = DmgTypes.default() for volleyData in volleyParams.values(): - em += volleyData[0].em - therm += volleyData[0].thermal - kin += volleyData[0].kinetic - exp += volleyData[0].explosive - return DmgTypes(em, therm, kin, exp) + volley += volleyData[0] + return volley def getDps(self, targetProfile=None): - em = 0 - thermal = 0 - kinetic = 0 - explosive = 0 - for dps in self.getDpsPerEffect(targetProfile=targetProfile).values(): - em += dps.em - thermal += dps.thermal - kinetic += dps.kinetic - explosive += dps.explosive - return DmgTypes(em=em, thermal=thermal, kinetic=kinetic, explosive=explosive) + dps = DmgTypes.default() + for subdps in self.getDpsPerEffect(targetProfile=targetProfile).values(): + dps += subdps + return dps def getDpsPerEffect(self, targetProfile=None): if not self.active or self.amount <= 0: diff --git a/eos/saveddata/fighterAbility.py b/eos/saveddata/fighterAbility.py index a69daf6d3..f4ffb2058 100644 --- a/eos/saveddata/fighterAbility.py +++ b/eos/saveddata/fighterAbility.py @@ -116,7 +116,7 @@ class FighterAbility: def getVolley(self, targetProfile=None): if not self.dealsDamage or not self.active: - return DmgTypes(0, 0, 0, 0) + return DmgTypes.default() if self.attrPrefix == "fighterAbilityLaunchBomb": em = self.fighter.getModifiedChargeAttr("emDamage", 0) therm = self.fighter.getModifiedChargeAttr("thermalDamage", 0) @@ -132,20 +132,22 @@ class FighterAbility: 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))) + explosive=exp * dmgMult * (1 - getattr(targetProfile, "explosiveAmount", 0)), + breacher=0) return volley def getDps(self, targetProfile=None, cycleTimeOverride=None): volley = self.getVolley(targetProfile=targetProfile) if not volley: - return DmgTypes(0, 0, 0, 0) + 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) + explosive=volley.explosive * dpsFactor, + breacher=volley.breacher * dpsFactor) return dps def clear(self): diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 69859ec4a..a2ac3f557 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -1688,8 +1688,8 @@ class Fit: self.__droneWaste = droneWaste def calculateWeaponDmgStats(self, spoolOptions): - weaponVolley = DmgTypes(0, 0, 0, 0) - weaponDps = DmgTypes(0, 0, 0, 0) + weaponVolley = DmgTypes.default() + weaponDps = DmgTypes.default() for mod in self.modules: weaponVolley += mod.getVolley(spoolOptions=spoolOptions, targetProfile=self.targetProfile) @@ -1699,8 +1699,8 @@ class Fit: self.__weaponDpsMap[spoolOptions] = weaponDps def calculateDroneDmgStats(self): - droneVolley = DmgTypes(0, 0, 0, 0) - droneDps = DmgTypes(0, 0, 0, 0) + droneVolley = DmgTypes.default() + droneDps = DmgTypes.default() for drone in self.drones: droneVolley += drone.getVolley(targetProfile=self.targetProfile) diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index 27ce2d81b..49b01bf8e 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -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 BreacherInfo, DmgTypes, RRTypes +from eos.utils.stats import BaseVolleyStats, BreacherInfo, DmgTypes, RRTypes pyfalog = Logger(__name__) @@ -453,6 +453,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M return True return False + @property + def isBreacher(self): + return self.charge and 'dotMissileLaunching' in self.charge.effects + def canDealDamage(self, ignoreState=False): if self.isEmpty: return False @@ -469,17 +473,17 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M def getVolleyParameters(self, spoolOptions=None, targetProfile=None, ignoreState=False): if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState): - return {0: DmgTypes(0, 0, 0, 0)} + return {0: DmgTypes.default()} if self.__baseVolley is None: self.__baseVolley = {} - if self.charge and 'dotMissileLaunching' in self.charge.effects: + if self.isBreacher: dmgDelay = 1 subcycles = math.floor(self.getModifiedChargeAttr("dotDuration", 0) / 1000) breacher_info = BreacherInfo( absolute=self.getModifiedChargeAttr("dotMaxDamagePerTick", 0), - relative=self.getModifiedChargeAttr("dotMaxHPPercentagePerTick", 0)) + relative=self.getModifiedChargeAttr("dotMaxHPPercentagePerTick", 0) / 100) for i in range(subcycles): - self.__baseVolley[dmgDelay + i] = DmgTypes(0, 0, 0, 0, breachers=[breacher_info]) + self.__baseVolley[dmgDelay + i] = BaseVolleyStats(0, 0, 0, 0, breachers=[breacher_info]) else: dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr dmgMult = self.getModifiedItemAttr("damageMultiplier", 1) @@ -498,7 +502,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M else: subcycles = 1 for i in range(subcycles): - self.__baseVolley[dmgDelay + dmgSubcycle * i] = DmgTypes( + self.__baseVolley[dmgDelay + dmgSubcycle * i] = BaseVolleyStats( em=(dmgGetter("emDamage", 0)) * dmgMult, thermal=(dmgGetter("thermalDamage", 0)) * dmgMult, kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult, @@ -510,24 +514,24 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M self.rawCycleTime / 1000, spoolType, spoolAmount)[0] spoolMultiplier = 1 + spoolBoost adjustedVolley = {} - for volleyTime, volleyValue in self.__baseVolley.items(): - adjustedVolley[volleyTime] = DmgTypes( - em=volleyValue.em * spoolMultiplier * (1 - getattr(targetProfile, "emAmount", 0)), - thermal=volleyValue.thermal * spoolMultiplier * (1 - getattr(targetProfile, "thermalAmount", 0)), - kinetic=volleyValue.kinetic * spoolMultiplier * (1 - getattr(targetProfile, "kineticAmount", 0)), - explosive=volleyValue.explosive * spoolMultiplier * (1 - getattr(targetProfile, "explosiveAmount", 0)), - breachers=volleyValue.breachers) + for volleyTime, baseValue in self.__baseVolley.items(): + adjustedVolley[volleyTime] = DmgTypes.from_base_and_profile( + base=baseValue, tgtProfile=targetProfile, mult=spoolMultiplier) return adjustedVolley def getVolley(self, spoolOptions=None, targetProfile=None, ignoreState=False): volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState) if len(volleyParams) == 0: - return DmgTypes(0, 0, 0, 0) + return DmgTypes.default() return volleyParams[min(volleyParams)] def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False, getSpreadDPS=False): - dmgDuringCycle = DmgTypes(0, 0, 0, 0) - cycleParams = self.getCycleParameters() + dmgDuringCycle = DmgTypes.default() + # Special hack for breachers, since those are DoT and work independently of gun cycle + if self.isBreacher: + cycleParams = CycleInfo(activeTime=1000, inactiveTime=0, quantity=math.inf, isInactivityReload=False) + else: + cycleParams = self.getCycleParameters() if cycleParams is None: return dmgDuringCycle volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState) @@ -541,13 +545,15 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M em=dmgDuringCycle.em * dpsFactor, thermal=dmgDuringCycle.thermal * dpsFactor, kinetic=dmgDuringCycle.kinetic * dpsFactor, - explosive=dmgDuringCycle.explosive * 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} + 'exp': dmgDuringCycle.explosive * dpsFactor, + 'breach': dmgDuringCycle.breach * dpsFactor} def isRemoteRepping(self, ignoreState=False): repParams = self.getRepAmountParameters(ignoreState=ignoreState) diff --git a/eos/utils/stats.py b/eos/utils/stats.py index ccb6cdb2e..8df2515b5 100644 --- a/eos/utils/stats.py +++ b/eos/utils/stats.py @@ -18,6 +18,7 @@ # =============================================================================== +import math from typing import NamedTuple from eos.utils.float import floatUnerr @@ -32,29 +33,12 @@ class BreacherInfo(NamedTuple): absolute: float relative: float - def __mul__(self, mul): - return type(self)(absolute=self.absolute * mul, relative=self.relative * mul) - def __imul__(self, mul): - if mul == 1: - return - self.absolute *= mul - self.relative *= mul - return self - - def __truediv__(self, div): - return type(self)(absolute=self.absolute / div, relative=self.relative / div) - - def __itruediv__(self, div): - if div == 1: - return - self.absolute /= div - self.relative /= div - return self - - -class DmgTypes: - """Container for damage data stats.""" +class BaseVolleyStats: + """ + 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 @@ -62,14 +46,67 @@ class DmgTypes: self.kinetic = kinetic self.explosive = explosive self.breachers = [] if breachers is None else breachers + + @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)) + + 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): + 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)})>') + + +class DmgTypes: + """Container for damage data stats.""" + + 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) + # Iterator is needed to support tuple-style unpacking def __iter__(self): yield self.em yield self.thermal yield self.kinetic yield self.explosive + yield self.breacher yield self.total def __eq__(self, other): @@ -82,15 +119,16 @@ class DmgTypes: 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.total)) + 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.total = self.em + self.thermal + self.kinetic + self.explosive + self.breacher def __add__(self, other): return type(self)( @@ -98,14 +136,14 @@ class DmgTypes: thermal=self.thermal + other.thermal, kinetic=self.kinetic + other.kinetic, explosive=self.explosive + other.explosive, - breachers=self.breachers + other.breachers) + breacher=max(self.breacher, other.breacher)) def __iadd__(self, other): self.em += other.em self.thermal += other.thermal self.kinetic += other.kinetic self.explosive += other.explosive - self.breachers += other.breachers + self.breacher = max(self.breacher, other.breacher) self._calcTotal() return self @@ -115,7 +153,7 @@ class DmgTypes: thermal=self.thermal * mul, kinetic=self.kinetic * mul, explosive=self.explosive * mul, - breachers=[b * mul for b in self.breachers]) + breacher=self.breacher * mul) def __imul__(self, mul): if mul == 1: @@ -124,7 +162,7 @@ class DmgTypes: self.thermal *= mul self.kinetic *= mul self.explosive *= mul - self.breachers = [b * mul for b in self.breachers] + self.breacher *= mul self._calcTotal() return self @@ -134,7 +172,7 @@ class DmgTypes: thermal=self.thermal / div, kinetic=self.kinetic / div, explosive=self.explosive / div, - breachers=[b / div for b in self.breachers]) + breacher=self.breacher / div) def __itruediv__(self, div): if div == 1: @@ -143,18 +181,16 @@ class DmgTypes: self.thermal /= div self.kinetic /= div self.explosive /= div - self.breachers = [b / div for b in self.breachers] + self.breacher /= div self._calcTotal() return self 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)}, total={self.total})>') + return makeReprStr(self, spec=['em', 'thermal', 'kinetic', 'explosive', 'breacher', 'total']) @staticmethod def names(short=None, postProcessor=None): - value = [_t('em'), _t('th'), _t('kin'), _t('exp')] if short else [_t('em'), _t('thermal'), _t('kinetic'), _t('explosive')] + value = [_t('em'), _t('th'), _t('kin'), _t('exp'), _t('breacher')] if short else [_t('em'), _t('thermal'), _t('kinetic'), _t('explosive'), _t('breacher')] if postProcessor: value = [postProcessor(x) for x in value] diff --git a/gui/builtinStatsViews/firepowerViewFull.py b/gui/builtinStatsViews/firepowerViewFull.py index 8540accc6..f290d5cd5 100644 --- a/gui/builtinStatsViews/firepowerViewFull.py +++ b/gui/builtinStatsViews/firepowerViewFull.py @@ -176,6 +176,7 @@ class FirepowerViewFull(StatsView): for dmgType in normal.names(): val = getattr(normal, dmgType, None) if val: + dmgType = {'breacher': 'pure'}.get(dmgType, dmgType) lines.append("{}{}: {}%".format( " " if hasSpool else "", _t(dmgType).capitalize(),