diff --git a/eos/graph/__init__.py b/eos/graph/__init__.py index 2af918fa4..474a1f5b1 100644 --- a/eos/graph/__init__.py +++ b/eos/graph/__init__.py @@ -20,7 +20,7 @@ import itertools -class Graph(object): +class Graph: def __init__(self, fit, function, data=None): self.fit = fit @@ -54,7 +54,7 @@ class Graph(object): yield point, self.function(point) -class Data(object): +class Data: def __init__(self, name, dataString, step=None): self.name = name self.step = step @@ -83,7 +83,7 @@ class Data(object): return len(self.data) == 1 and self.data[0].isConstant() -class Constant(object): +class Constant: def __init__(self, const): if isinstance(const, str): self.value = None if const == "" else float(const) @@ -98,7 +98,7 @@ class Constant(object): return True -class Range(object): +class Range: def __init__(self, string, step): start, end = string.split("-") diff --git a/eos/graph/fitDmgTime.py b/eos/graph/fitDmgTime.py index 01da4a0ee..f776cb13b 100644 --- a/eos/graph/fitDmgTime.py +++ b/eos/graph/fitDmgTime.py @@ -31,13 +31,63 @@ class FitDmgTimeGraph(Graph): defaults = {"time": 0} def __init__(self, fit, data=None): - Graph.__init__(self, fit, self.calcDps, data if data is not None else self.defaults) + Graph.__init__(self, fit, self.calcDmg, data if data is not None else self.defaults) self.fit = fit - def calcDps(self, data): + def calcDmg(self, data): fit = self.fit - time = data["time"] - dmg = 0 - for i in range(round(time) + 1): - dmg += fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.TIME, i, True)).total - return dmg + # We'll handle calculations in milliseconds + maxTime = data["time"] * 1000 + currentDmg = 0 + for mod in fit.modules: + cycleParams = mod.getCycleParameters(reloadOverride=True) + if cycleParams is None: + continue + currentTime = 0 + nonstopCycles = 0 + for cycleTime, inactiveTime in cycleParams.iterCycles(): + volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True)) + for volleyTime, volley in volleyParams.items(): + if currentTime + volleyTime <= maxTime: + currentDmg += volley.total + currentTime += cycleTime + currentTime += inactiveTime + if inactiveTime == 0: + nonstopCycles += 1 + else: + nonstopCycles = 0 + if currentTime > maxTime: + break + for drone in fit.drones: + cycleParams = drone.getCycleParameters(reloadOverride=True) + if cycleParams is None: + continue + currentTime = 0 + volleyParams = drone.getVolleyParameters() + for cycleTime, inactiveTime in cycleParams.iterCycles(): + for volleyTime, volley in volleyParams.items(): + if currentTime + volleyTime <= maxTime: + currentDmg += volley.total + currentTime += cycleTime + currentTime += inactiveTime + if currentTime > maxTime: + break + for fighter in fit.fighters: + cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True) + if cycleParams is None: + continue + volleyParams = fighter.getVolleyParametersPerEffect() + for effectID, abilityCycleParams in cycleParams.items(): + if effectID not in volleyParams: + continue + currentTime = 0 + abilityVolleyParams = volleyParams[effectID] + for cycleTime, inactiveTime in abilityCycleParams.iterCycles(): + for volleyTime, volley in abilityVolleyParams.items(): + if currentTime + volleyTime <= maxTime: + currentDmg += volley.total + currentTime += cycleTime + currentTime += inactiveTime + if currentTime > maxTime: + break + return currentDmg diff --git a/eos/saveddata/fighter.py b/eos/saveddata/fighter.py index 71787fdf1..3cd604323 100644 --- a/eos/saveddata/fighter.py +++ b/eos/saveddata/fighter.py @@ -222,8 +222,17 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def getDpsPerEffect(self, targetResists=None): if not self.active or self.amountActive <= 0: return {} + cycleParams = self.getCycleParametersPerEffectOptimizedDps(targetResists=targetResists) + dpsMap = {} + for ability in self.abilities: + if ability.effectID in cycleParams: + cycleTime = cycleParams[ability.effectID].averageTime + dpsMap[ability.effectID] = ability.getDps(targetResists=targetResists, cycleTimeOverride=cycleTime) + return dpsMap + + def getCycleParametersPerEffectOptimizedDps(self, targetResists=None, reloadOverride=None): cycleParamsInfinite = self.getCycleParametersPerEffectInfinite() - cycleParamsReload = self.getCycleParametersPerEffect() + cycleParamsReload = self.getCycleParametersPerEffect(reloadOverride=reloadOverride) dpsMapOnlyInfinite = {} dpsMapAllWithReloads = {} # Decide if it's better to keep steady dps up and never reload or reload from time to time @@ -236,19 +245,19 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): 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 + return cycleParamsInfinite if totalOnlyInfinite >= totalAllWithReloads else cycleParamsReload def getCycleParametersPerEffectInfinite(self): - return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.numShots == 0} + return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.numShots == 0 and a.cycleTime > 0} def getCycleParametersPerEffect(self, reloadOverride=None): factorReload = reloadOverride if reloadOverride is not None else self.owner.factorReload # Assume it can cycle infinitely if not 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] + return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.cycleTime > 0} + limitedAbilities = [a for a in self.abilities if a.numShots > 0 and a.cycleTime > 0] if len(limitedAbilities) == 0: - return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities} + return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.cycleTime > 0} validAbilities = [a for a in self.abilities if a.cycleTime > 0] if len(validAbilities) == 0: return {} diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index 41d7fc4ed..bf6eeffe3 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -401,8 +401,12 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): volley = self.getModifiedItemAttr("specialtyMiningAmount") or self.getModifiedItemAttr( "miningAmount") or 0 if volley: - cycleTime = self.getCycleParameters().averageTime - self.__miningyield = volley / (cycleTime / 1000.0) + cycleParams = self.getCycleParameters() + if cycleParams is None: + self.__miningyield = 0 + else: + cycleTime = cycleParams.averageTime + self.__miningyield = volley / (cycleTime / 1000.0) else: self.__miningyield = 0 else: @@ -452,9 +456,12 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): return volleyParams[min(volleyParams)] def getDps(self, spoolOptions=None, targetResists=None, ignoreState=False): - volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetResists=targetResists, ignoreState=ignoreState) - avgCycleTime = self.getCycleParameters().averageTime dmgDuringCycle = DmgTypes(0, 0, 0, 0) + cycleParams = self.getCycleParameters() + if cycleParams is None: + return dmgDuringCycle + volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetResists=targetResists, ignoreState=ignoreState) + avgCycleTime = cycleParams.averageTime if len(volleyParams) == 0 or avgCycleTime == 0: return dmgDuringCycle for volleyValue in volleyParams.values(): @@ -494,7 +501,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): else: return None, 0 if rrAmount: - rrAmount *= 1 / (self.getCycleParameters().averageTime / 1000) + cycleParams = self.getCycleParameters() + if cycleParams is None: + return None, 0 + rrAmount *= 1 / (cycleParams.averageTime / 1000) if module.item.group.name == "Ancillary Remote Armor Repairer" and module.charge: rrAmount *= module.getModifiedItemAttr("chargedArmorDamageMultiplier", 1) @@ -852,6 +862,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): cycles_until_reload = math.inf active_time = self.rawCycleTime + if active_time == 0: + return None forced_inactive_time = self.reactivationDelay reload_time = self.reloadTime # Effects which cannot be reloaded have the same processing whether @@ -917,7 +929,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def capUse(self): capNeed = self.getModifiedItemAttr("capacitorNeed") if capNeed and self.state >= FittingModuleState.ACTIVE: - cycleTime = self.getCycleParameters().averageTime + cycleParams = self.getCycleParameters() + if cycleParams is None: + return 0 + cycleTime = cycleParams.averageTime if cycleTime > 0: capUsed = capNeed / (cycleTime / 1000.0) return capUsed diff --git a/eos/utils/cycles.py b/eos/utils/cycles.py index faef2ec18..792e816f3 100644 --- a/eos/utils/cycles.py +++ b/eos/utils/cycles.py @@ -15,6 +15,12 @@ class CycleInfo: def averageTime(self): return self.activeTime + self.inactiveTime + def iterCycles(self): + i = 0 + while i < self.quantity: + yield self.activeTime, self.inactiveTime + i += 1 + def _getCycleQuantity(self): return self.quantity @@ -37,6 +43,14 @@ class CycleSequence: """Get average time between cycles.""" return self._getTime() / self._getCycleQuantity() + def iterCycles(self): + i = 0 + while i < self.quantity: + for cycleInfo in self.sequence: + for cycleTime, inactiveTime in cycleInfo.iterCycles(): + yield cycleTime, inactiveTime + i += 1 + def _getCycleQuantity(self): quantity = 0 for item in self.sequence: diff --git a/gui/builtinViewColumns/misc.py b/gui/builtinViewColumns/misc.py index 106109d71..1babb9799 100644 --- a/gui/builtinViewColumns/misc.py +++ b/gui/builtinViewColumns/misc.py @@ -140,7 +140,10 @@ class Miscellanea(ViewColumn): return "+ " + ", ".join(info), "Slot Modifiers" elif itemGroup == "Energy Neutralizer": neutAmount = stuff.getModifiedItemAttr("energyNeutralizerAmount") - cycleTime = stuff.getCycleParameters().averageTime + cycleParams = stuff.getCycleParameters() + if cycleParams is None: + return "", None + cycleTime = cycleParams.averageTime if not neutAmount or not cycleTime: return "", None capPerSec = float(-neutAmount) * 1000 / cycleTime @@ -149,7 +152,10 @@ class Miscellanea(ViewColumn): return text, tooltip elif itemGroup == "Energy Nosferatu": neutAmount = stuff.getModifiedItemAttr("powerTransferAmount") - cycleTime = stuff.getCycleParameters().averageTime + cycleParams = stuff.getCycleParameters() + if cycleParams is None: + return "", None + cycleTime = cycleParams.averageTime if not neutAmount or not cycleTime: return "", None capPerSec = float(-neutAmount) * 1000 / cycleTime diff --git a/gui/graphFrame.py b/gui/graphFrame.py index b878f0dfc..22243fbaa 100644 --- a/gui/graphFrame.py +++ b/gui/graphFrame.py @@ -275,6 +275,7 @@ class GraphFrame(wx.Frame): x, y = success, status self.subplot.plot(x, y) + self.subplot.set_ylim(bottom=0) legend.append(fit.name) except Exception as ex: pyfalog.warning("Invalid values in '{0}'", fit.name)