Expose breacher DPS and volley to stats

This commit is contained in:
DarkPhoenix
2024-11-12 14:20:20 +01:00
parent 13f3793515
commit 5721beacf5
7 changed files with 123 additions and 92 deletions

View File

@@ -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):

View File

@@ -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:

View File

@@ -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):

View File

@@ -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)

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 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)

View File

@@ -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]

View File

@@ -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(),