diff --git a/eos/saveddata/drone.py b/eos/saveddata/drone.py index f2f745d56..ba58a4ba8 100644 --- a/eos/saveddata/drone.py +++ b/eos/saveddata/drone.py @@ -153,7 +153,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): volley = self.getVolley(targetResists=targetResists) if not volley: return DmgTypes(0, 0, 0, 0) - dpsFactor = 1 / (self.cycleParameters.averageTime / 1000) + dpsFactor = 1 / (self.getCycleParameters().averageTime / 1000) dps = DmgTypes( em=volley.em * dpsFactor, thermal=volley.thermal * dpsFactor, @@ -161,8 +161,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): explosive=volley.explosive * dpsFactor) return dps - @property - def cycleParameters(self): + def getCycleParameters(self): return CycleInfo(self.cycleTime, 0, math.inf) def getRemoteReps(self, ignoreState=False): @@ -186,7 +185,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): rrAmount = 0 if rrAmount: droneAmount = self.amount if ignoreState else self.amountActive - rrAmount *= droneAmount / (self.cycleParameters.averageTime / 1000) + rrAmount *= droneAmount / (self.getCycleParameters().averageTime / 1000) self.__baseRemoteReps = (rrType, rrAmount) return self.__baseRemoteReps @@ -195,7 +194,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): if self.__miningyield is None: if self.mines is True and self.amountActive > 0: getter = self.getModifiedItemAttr - cycleTime = self.cycleParameters.averageTime + cycleTime = self.getCycleParameters().averageTime volley = sum([getter(d) for d in self.MINING_ATTRIBUTES]) * self.amountActive self.__miningyield = volley / (cycleTime / 1000.0) else: diff --git a/eos/saveddata/fighter.py b/eos/saveddata/fighter.py index 6ec2db424..7cc5849ea 100644 --- a/eos/saveddata/fighter.py +++ b/eos/saveddata/fighter.py @@ -17,16 +17,19 @@ # along with eos. If not, see . # =============================================================================== +import math from logbook import Logger - -from sqlalchemy.orm import validates, reconstructor +from sqlalchemy.orm import reconstructor, validates import eos.db -from eos.effectHandlerHelpers import HandledItem, HandledCharge -from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut -from eos.saveddata.fighterAbility import FighterAbility -from eos.utils.stats import DmgTypes from eos.const import FittingSlot +from eos.effectHandlerHelpers import HandledCharge, HandledItem +from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict +from eos.saveddata.fighterAbility import FighterAbility +from eos.utils.cycles import CycleInfo, CycleSequence +from eos.utils.stats import DmgTypes +from eos.utils.float import floatUnerr + pyfalog = Logger(__name__) @@ -207,44 +210,71 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): explosive += dps.explosive return DmgTypes(em=em, thermal=thermal, kinetic=kinetic, explosive=explosive) - def getUptime(self): - if not self.owner.factorReload: - return 1 - activeTimes = [] - reloadTimes = [] - for ability in self.abilities: - if ability.numShots > 0: - activeTimes.append(ability.numShots * ability.cycleTime) - reloadTimes.append(ability.reloadTime) - if not activeTimes: - return 1 - shortestActive = sorted(activeTimes)[0] - longestReload = sorted(reloadTimes, reverse=True)[0] - uptime = shortestActive / (shortestActive + longestReload) - return uptime - def getDpsPerEffect(self, targetResists=None): if not self.active or self.amountActive <= 0: return {} - uptime = self.getUptime() - if uptime == 1: - return {a.effectID: a.getDps(targetResists=targetResists) for a in self.abilities} + cycleParamsInfinite = self.getCycleParametersPerEffectInfinite() + cycleParamsReload = self.getCycleParametersPerEffect() + dpsMapOnlyInfinite = {} + dpsMapAllWithReloads = {} # Decide if it's better to keep steady dps up and never reload or reload from time to time - dpsMapSteady = {} - dpsMapPeakAdjusted = {} for ability in self.abilities: - abilityDps = ability.getDps(targetResists=targetResists) - dpsMapPeakAdjusted[ability.effectID] = DmgTypes( - em=abilityDps.em * uptime, - thermal=abilityDps.thermal * uptime, - kinetic=abilityDps.kinetic * uptime, - explosive=abilityDps.explosive * uptime) - # Infinite use - add to steady dps - if ability.numShots == 0: - dpsMapSteady[ability.effectID] = abilityDps - totalSteady = sum(i.total for i in dpsMapSteady.values()) - totalPeakAdjusted = sum(i.total for i in dpsMapPeakAdjusted.values()) - return dpsMapSteady if totalSteady >= totalPeakAdjusted else dpsMapPeakAdjusted + if ability.effectID in cycleParamsInfinite: + cycleTime = cycleParamsInfinite[ability.effectID].averageTime + dpsMapOnlyInfinite[ability.effectID] = ability.getDps(targetResists=targetResists, cycleTimeOverride=cycleTime) + if ability.effectID in cycleParamsReload: + cycleTime = cycleParamsReload[ability.effectID].averageTime + dpsMapAllWithReloads[ability.effectID] = ability.getDps(targetResists=targetResists, cycleTimeOverride=cycleTime) + totalOnlyInfinite = sum(i.total for i in dpsMapOnlyInfinite.values()) + totalAllWithReloads = sum(i.total for i in dpsMapAllWithReloads.values()) + return dpsMapOnlyInfinite if totalOnlyInfinite >= totalAllWithReloads else dpsMapAllWithReloads + + def getCycleParametersPerEffectInfinite(self): + return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.numShots == 0} + + def getCycleParametersPerEffect(self): + # Assume it can cycle infinitely + if not self.owner.factorReload: + return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities} + limitedAbilities = [a for a in self.abilities if a.numShots > 0] + if len(limitedAbilities) == 0: + return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities} + validAbilities = [a for a in self.abilities if a.cycleTime > 0] + if len(validAbilities) == 0: + return {} + mostLimitedAbility = min(limitedAbilities, key=lambda a: a.cycleTime * a.numShots) + durationToRefuel = mostLimitedAbility.cycleTime * mostLimitedAbility.numShots + # find out how many shots various abilities will do until reload, and how much time + # "extra" cycle will last (None for no extra cycle) + cyclesUntilRefuel = {mostLimitedAbility.effectID: (mostLimitedAbility.numShots, None)} + for ability in (a for a in validAbilities if a is not mostLimitedAbility): + fullCycles = int(floatUnerr(durationToRefuel / ability.cycleTime)) + extraShotTime = floatUnerr(durationToRefuel - (fullCycles * ability.cycleTime)) + if extraShotTime == 0: + extraShotTime = None + cyclesUntilRefuel[ability.effectID] = (fullCycles, extraShotTime) + refuelTimes = {} + for ability in validAbilities: + spentShots, extraShotTime = cyclesUntilRefuel[ability.effectID] + if extraShotTime is not None: + spentShots += 1 + refuelTimes[ability.effectID] = ability.getReloadTime(spentShots) + refuelTime = max(refuelTimes.values()) + cycleParams = {} + for ability in validAbilities: + regularShots, extraShotTime = cyclesUntilRefuel[ability.effectID] + sequence = [] + if extraShotTime is not None: + if regularShots > 0: + sequence.append(CycleInfo(ability.cycleTime, 0, regularShots)) + sequence.append(CycleInfo(extraShotTime, refuelTime, 1)) + else: + regularShotsNonReload = regularShots - 1 + if regularShotsNonReload > 0: + sequence.append(CycleInfo(ability.cycleTime, 0, regularShotsNonReload)) + sequence.append(CycleInfo(ability.cycleTime, refuelTime, 1)) + cycleParams[ability.effectID] = CycleSequence(sequence, math.inf) + return cycleParams @property def maxRange(self): diff --git a/eos/saveddata/fighterAbility.py b/eos/saveddata/fighterAbility.py index 88c5c65f6..d1f4d0020 100644 --- a/eos/saveddata/fighterAbility.py +++ b/eos/saveddata/fighterAbility.py @@ -95,8 +95,15 @@ class FighterAbility(object): @property def reloadTime(self): + return self.getReloadTime() + + def getReloadTime(self, spentShots=None): + if spentShots is not None: + spentShots = max(self.numShots, spentShots) + else: + spentShots = self.numShots rearm_time = (self.REARM_TIME_MAPPING[self.fighter.getModifiedItemAttr("fighterSquadronRole")] or 0 if self.hasCharges else 0) - return self.fighter.getModifiedItemAttr("fighterRefuelingTime") + rearm_time * self.numShots + return self.fighter.getModifiedItemAttr("fighterRefuelingTime") + rearm_time * spentShots @property def numShots(self): @@ -128,11 +135,12 @@ class FighterAbility(object): explosive=exp * dmgMult * (1 - getattr(targetResists, "explosiveAmount", 0))) return volley - def getDps(self, targetResists=None): + def getDps(self, targetResists=None, cycleTimeOverride=None): volley = self.getVolley(targetResists=targetResists) if not volley: return DmgTypes(0, 0, 0, 0) - dpsFactor = 1 / (self.cycleTime / 1000) + cycleTime = cycleTimeOverride if cycleTimeOverride is not None else self.cycleTime + dpsFactor = 1 / (cycleTime / 1000) dps = DmgTypes( em=volley.em * dpsFactor, thermal=volley.thermal * dpsFactor, diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index d6178946f..c8ddf691e 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -401,7 +401,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): volley = self.getModifiedItemAttr("specialtyMiningAmount") or self.getModifiedItemAttr( "miningAmount") or 0 if volley: - cycleTime = self.cycleParameters.averageTime + cycleTime = self.getCycleParameters().averageTime self.__miningyield = volley / (cycleTime / 1000.0) else: self.__miningyield = 0 @@ -440,7 +440,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): return DmgTypes(0, 0, 0, 0) # Some weapons repeat multiple times in one cycle (bosonic doomsdays). Get the number of times it fires off volleysPerCycle = max(self.getModifiedItemAttr("doomsdayDamageDuration", 1) / self.getModifiedItemAttr("doomsdayDamageCycleTime", 1), 1) - dpsFactor = volleysPerCycle / (self.cycleParameters.averageTime / 1000) + dpsFactor = volleysPerCycle / (self.getCycleParameters().averageTime / 1000) dps = DmgTypes( em=volley.em * dpsFactor, thermal=volley.thermal * dpsFactor, @@ -475,7 +475,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): else: return None, 0 if rrAmount: - rrAmount *= 1 / (self.cycleParameters.averageTime / 1000) + rrAmount *= 1 / (self.getCycleParameters().averageTime / 1000) if module.item.group.name == "Ancillary Remote Armor Repairer" and module.charge: rrAmount *= module.getModifiedItemAttr("chargedArmorDamageMultiplier", 1) @@ -820,8 +820,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): except: effect.handler(fit, self, context) - @property - def cycleParameters(self): + def getCycleParameters(self): """Copied from new eos as well""" # Determine if we'll take into account reload time or not factorReload = self.owner.factorReload if self.forceReload is None else self.forceReload @@ -896,7 +895,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def capUse(self): capNeed = self.getModifiedItemAttr("capacitorNeed") if capNeed and self.state >= FittingModuleState.ACTIVE: - cycleTime = self.cycleParameters.averageTime + cycleTime = self.getCycleParameters().averageTime if cycleTime > 0: capUsed = capNeed / (cycleTime / 1000.0) return capUsed diff --git a/gui/builtinViewColumns/misc.py b/gui/builtinViewColumns/misc.py index 731d273c2..106109d71 100644 --- a/gui/builtinViewColumns/misc.py +++ b/gui/builtinViewColumns/misc.py @@ -140,7 +140,7 @@ class Miscellanea(ViewColumn): return "+ " + ", ".join(info), "Slot Modifiers" elif itemGroup == "Energy Neutralizer": neutAmount = stuff.getModifiedItemAttr("energyNeutralizerAmount") - cycleTime = stuff.cycleParameters.averageTime + cycleTime = stuff.getCycleParameters().averageTime if not neutAmount or not cycleTime: return "", None capPerSec = float(-neutAmount) * 1000 / cycleTime @@ -149,7 +149,7 @@ class Miscellanea(ViewColumn): return text, tooltip elif itemGroup == "Energy Nosferatu": neutAmount = stuff.getModifiedItemAttr("powerTransferAmount") - cycleTime = stuff.cycleParameters.averageTime + cycleTime = stuff.getCycleParameters().averageTime if not neutAmount or not cycleTime: return "", None capPerSec = float(-neutAmount) * 1000 / cycleTime diff --git a/service/port/efs.py b/service/port/efs.py index 1712ec99b..3fb771e56 100755 --- a/service/port/efs.py +++ b/service/port/efs.py @@ -356,7 +356,7 @@ class EfsPort: "dps": stats.getDps(spoolOptions=spoolOptions).total * n, "capUse": stats.capUse * n, "falloff": stats.falloff, "type": typeing, "name": name, "optimal": maxRange, "numCharges": stats.numCharges, "numShots": stats.numShots, "reloadTime": stats.reloadTime, - "cycleTime": stats.cycleParameters.averageTime, "volley": stats.getVolley(spoolOptions=spoolOptions).total * n, "tracking": tracking, + "cycleTime": stats.getCycleParameters().averageTime, "volley": stats.getVolley(spoolOptions=spoolOptions).total * n, "tracking": tracking, "maxVelocity": maxVelocity, "explosionDelay": explosionDelay, "damageReductionFactor": damageReductionFactor, "explosionRadius": explosionRadius, "explosionVelocity": explosionVelocity, "aoeFieldRange": aoeFieldRange, "damageMultiplierBonusMax": stats.getModifiedItemAttr("damageMultiplierBonusMax"), @@ -369,7 +369,7 @@ class EfsPort: # Drones are using the old tracking formula for trackingSpeed. This updates it to match turrets. newTracking = droneAttr("trackingSpeed") / (droneAttr("optimalSigRadius") / 40000) statDict = { - "dps": drone.getDps().total, "cycleTime": drone.cycleParameters.averageTime, "type": "Drone", + "dps": drone.getDps().total, "cycleTime": drone.getCycleParameters().averageTime, "type": "Drone", "optimal": drone.maxRange, "name": drone.item.name, "falloff": drone.falloff, "maxSpeed": droneAttr("maxVelocity"), "tracking": newTracking, "volley": drone.getVolley().total @@ -498,11 +498,11 @@ class EfsPort: fitMultipliers["drones"] = list(map(getDroneMulti, tf.drones)) getFitTurrets = lambda f: filter(lambda mod: mod.hardpoint == FittingHardpoint.TURRET, f.modules) - getTurretMulti = lambda mod: mod.getModifiedItemAttr("damageMultiplier") / mod.cycleParameters.averageTime + getTurretMulti = lambda mod: mod.getModifiedItemAttr("damageMultiplier") / mod.getCycleParameters().averageTime fitMultipliers["turrets"] = list(map(getTurretMulti, getFitTurrets(tf))) getFitLaunchers = lambda f: filter(lambda mod: mod.hardpoint == FittingHardpoint.MISSILE, f.modules) - getLauncherMulti = lambda mod: sumDamage(mod.getModifiedChargeAttr) / mod.cycleParameters.averageTime + getLauncherMulti = lambda mod: sumDamage(mod.getModifiedChargeAttr) / mod.getCycleParameters().averageTime fitMultipliers["launchers"] = list(map(getLauncherMulti, getFitLaunchers(tf))) return fitMultipliers