diff --git a/eos/saveddata/drone.py b/eos/saveddata/drone.py index 8fc0a58e8..3fac30257 100644 --- a/eos/saveddata/drone.py +++ b/eos/saveddata/drone.py @@ -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): diff --git a/eos/saveddata/fighter.py b/eos/saveddata/fighter.py index c07e36cd2..ca7d1789b 100644 --- a/eos/saveddata/fighter.py +++ b/eos/saveddata/fighter.py @@ -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) diff --git a/eos/saveddata/fighterAbility.py b/eos/saveddata/fighterAbility.py index f4ffb2058..57e950aef 100644 --- a/eos/saveddata/fighterAbility.py +++ b/eos/saveddata/fighterAbility.py @@ -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): diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index a2ac3f557..9abf73e50 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -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 diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index 89bb9ef55..302c670cb 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 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 diff --git a/eos/utils/stats.py b/eos/utils/stats.py index 26d75cbd2..a64cd31cb 100644 --- a/eos/utils/stats.py +++ b/eos/utils/stats.py @@ -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): diff --git a/graphs/data/fitDamageStats/cache/time.py b/graphs/data/fitDamageStats/cache/time.py index 667a299ed..16ee6a595 100644 --- a/graphs/data/fitDamageStats/cache/time.py +++ b/graphs/data/fitDamageStats/cache/time.py @@ -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 diff --git a/gui/builtinStatsViews/firepowerViewFull.py b/gui/builtinStatsViews/firepowerViewFull.py index cd164b542..364760f3e 100644 --- a/gui/builtinStatsViews/firepowerViewFull.py +++ b/gui/builtinStatsViews/firepowerViewFull.py @@ -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() diff --git a/service/port/efs.py b/service/port/efs.py index 8cd466271..55358b644 100755 --- a/service/port/efs.py +++ b/service/port/efs.py @@ -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 = {