Merge branch 'dps_sim_graph' of github.com:pyfa-org/Pyfa into dps_sim_graph
This commit is contained in:
@@ -1,76 +0,0 @@
|
||||
# ===============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of eos.
|
||||
#
|
||||
# eos is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# eos is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
import math
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
||||
class Graph(metaclass=ABCMeta):
|
||||
|
||||
def __init__(self):
|
||||
self._cache = {}
|
||||
|
||||
@abstractmethod
|
||||
def getPlotPoints(self, fit, extraData, xRange, xAmount):
|
||||
raise NotImplementedError
|
||||
|
||||
def getYForX(self, fit, extraData, x):
|
||||
raise NotImplementedError
|
||||
|
||||
def _xIter(self, fit, extraData, xRange, xAmount):
|
||||
rangeLow, rangeHigh = self._limitXRange(xRange, fit, extraData)
|
||||
# Amount is amount of ranges between points here, not amount of points
|
||||
step = (rangeHigh - rangeLow) / xAmount
|
||||
if step == 0:
|
||||
yield xRange[0]
|
||||
else:
|
||||
current = rangeLow
|
||||
# Take extra half step to make sure end of range is always included
|
||||
# despite any possible float errors
|
||||
while current <= (rangeHigh + step / 2):
|
||||
yield current
|
||||
current += step
|
||||
|
||||
def _limitXRange(self, xRange, fit, extraData):
|
||||
rangeLow, rangeHigh = sorted(xRange)
|
||||
limitLow, limitHigh = self._getXLimits(fit, extraData)
|
||||
rangeLow = max(limitLow, rangeLow)
|
||||
rangeHigh = min(limitHigh, rangeHigh)
|
||||
return rangeLow, rangeHigh
|
||||
|
||||
def _getXLimits(self, fit, extraData):
|
||||
return -math.inf, math.inf
|
||||
|
||||
def clearCache(self, key=None):
|
||||
if key is None:
|
||||
self._cache.clear()
|
||||
elif key in self._cache:
|
||||
del self._cache[key]
|
||||
|
||||
|
||||
class SmoothGraph(Graph, metaclass=ABCMeta):
|
||||
|
||||
def getPlotPoints(self, fit, extraData, xRange, xAmount):
|
||||
xs = []
|
||||
ys = []
|
||||
for x in self._xIter(fit, extraData, xRange, xAmount):
|
||||
xs.append(x)
|
||||
ys.append(self.getYForX(fit, extraData, x))
|
||||
return xs, ys
|
||||
@@ -1,15 +0,0 @@
|
||||
import math
|
||||
|
||||
from eos.graph import SmoothGraph
|
||||
|
||||
|
||||
class FitCapAmountVsTimeGraph(SmoothGraph):
|
||||
|
||||
def getYForX(self, fit, extraData, time):
|
||||
if time < 0:
|
||||
return 0
|
||||
maxCap = fit.ship.getModifiedItemAttr('capacitorCapacity')
|
||||
regenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000
|
||||
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
|
||||
cap = maxCap * (1 + math.exp(5 * -time / regenTime) * -1) ** 2
|
||||
return cap
|
||||
@@ -1,14 +0,0 @@
|
||||
import math
|
||||
|
||||
from eos.graph import SmoothGraph
|
||||
|
||||
|
||||
class FitCapRegenVsCapPercGraph(SmoothGraph):
|
||||
|
||||
def getYForX(self, fit, extraData, perc):
|
||||
maxCap = fit.ship.getModifiedItemAttr('capacitorCapacity')
|
||||
regenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000
|
||||
currentCap = maxCap * perc / 100
|
||||
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
|
||||
regen = 10 * maxCap / regenTime * (math.sqrt(currentCap / maxCap) - currentCap / maxCap)
|
||||
return regen
|
||||
@@ -1,17 +0,0 @@
|
||||
import math
|
||||
|
||||
from eos.graph import SmoothGraph
|
||||
|
||||
|
||||
class FitDistanceVsTimeGraph(SmoothGraph):
|
||||
|
||||
def getYForX(self, fit, extraData, time):
|
||||
maxSpeed = fit.ship.getModifiedItemAttr('maxVelocity')
|
||||
mass = fit.ship.getModifiedItemAttr('mass')
|
||||
agility = fit.ship.getModifiedItemAttr('agility')
|
||||
# Definite integral of:
|
||||
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
|
||||
distance_t = maxSpeed * time + (maxSpeed * agility * mass * math.exp((-time * 1000000) / (agility * mass)) / 1000000)
|
||||
distance_0 = maxSpeed * 0 + (maxSpeed * agility * mass * math.exp((-0 * 1000000) / (agility * mass)) / 1000000)
|
||||
distance = distance_t - distance_0
|
||||
return distance
|
||||
@@ -1,151 +0,0 @@
|
||||
# ===============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of eos.
|
||||
#
|
||||
# eos is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# eos is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
from eos.graph import Graph
|
||||
from eos.utils.spoolSupport import SpoolType, SpoolOptions
|
||||
from gui.utils.numberFormatter import roundToPrec
|
||||
|
||||
|
||||
class FitDmgVsTimeGraph(Graph):
|
||||
|
||||
def getPlotPoints(self, fit, extraData, xRange, xAmount):
|
||||
# We deliberately ignore xAmount here to build graph which will reflect
|
||||
# all steps of building up the damage
|
||||
minX, maxX = self._limitXRange(xRange, fit, extraData)
|
||||
if fit.ID not in self._cache:
|
||||
self.__generateCache(fit, maxX)
|
||||
currentY = None
|
||||
xs = []
|
||||
ys = []
|
||||
cache = self._cache[fit.ID]
|
||||
for time in sorted(cache):
|
||||
prevY = currentY
|
||||
currentX = time / 1000
|
||||
currentY = roundToPrec(cache[time], 6)
|
||||
if currentX < minX:
|
||||
continue
|
||||
# First set of data points
|
||||
if not xs:
|
||||
# Start at exactly requested time, at last known value
|
||||
initialY = prevY or 0
|
||||
xs.append(minX)
|
||||
ys.append(initialY)
|
||||
# If current time is bigger then starting, extend plot to that time with old value
|
||||
if currentX > minX:
|
||||
xs.append(currentX)
|
||||
ys.append(initialY)
|
||||
# If new value is different, extend it with new point to the new value
|
||||
if currentY != prevY:
|
||||
xs.append(currentX)
|
||||
ys.append(currentY)
|
||||
continue
|
||||
# Last data point
|
||||
if currentX >= maxX:
|
||||
xs.append(maxX)
|
||||
ys.append(prevY)
|
||||
break
|
||||
# Anything in-between
|
||||
if currentY != prevY:
|
||||
if prevY is not None:
|
||||
xs.append(currentX)
|
||||
ys.append(prevY)
|
||||
xs.append(currentX)
|
||||
ys.append(currentY)
|
||||
return xs, ys
|
||||
|
||||
def getYForX(self, fit, extraData, x):
|
||||
time = x * 1000
|
||||
cache = self._cache[fit.ID]
|
||||
closestTime = max((t for t in cache if t <= time), default=None)
|
||||
if closestTime is None:
|
||||
return 0
|
||||
return roundToPrec(cache[closestTime], 6)
|
||||
|
||||
def _getXLimits(self, fit, extraData):
|
||||
return 0, 2500
|
||||
|
||||
def __generateCache(self, fit, maxTime):
|
||||
cache = self._cache[fit.ID] = {}
|
||||
|
||||
def addDmg(addedTime, addedDmg):
|
||||
if addedDmg == 0:
|
||||
return
|
||||
if addedTime not in cache:
|
||||
prevTime = max((t for t in cache if t < addedTime), default=None)
|
||||
if prevTime is None:
|
||||
cache[addedTime] = 0
|
||||
else:
|
||||
cache[addedTime] = cache[prevTime]
|
||||
for time in (t for t in cache if t >= addedTime):
|
||||
cache[time] += addedDmg
|
||||
|
||||
# We'll handle calculations in milliseconds
|
||||
maxTime = maxTime * 1000
|
||||
for mod in fit.modules:
|
||||
if not mod.isDealingDamage():
|
||||
continue
|
||||
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():
|
||||
addDmg(currentTime + volleyTime, volley.total)
|
||||
if inactiveTime == 0:
|
||||
nonstopCycles += 1
|
||||
else:
|
||||
nonstopCycles = 0
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
currentTime += cycleTime + inactiveTime
|
||||
for drone in fit.drones:
|
||||
if not drone.isDealingDamage():
|
||||
continue
|
||||
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():
|
||||
addDmg(currentTime + volleyTime, volley.total)
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
currentTime += cycleTime + inactiveTime
|
||||
for fighter in fit.fighters:
|
||||
if not fighter.isDealingDamage():
|
||||
continue
|
||||
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():
|
||||
addDmg(currentTime + volleyTime, volley.total)
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
currentTime += cycleTime + inactiveTime
|
||||
@@ -1,197 +0,0 @@
|
||||
# ===============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of eos.
|
||||
#
|
||||
# eos is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# eos is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
from math import exp, log, radians, sin, inf
|
||||
|
||||
from logbook import Logger
|
||||
|
||||
import eos.config
|
||||
from eos.const import FittingHardpoint, FittingModuleState
|
||||
from eos.graph import SmoothGraph
|
||||
from eos.utils.spoolSupport import SpoolType, SpoolOptions
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class FitDpsVsRangeGraph(SmoothGraph):
|
||||
|
||||
def getYForX(self, fit, extraData, distance):
|
||||
tgtSpeed = extraData['speed']
|
||||
tgtSigRad = extraData['signatureRadius'] if extraData['signatureRadius'] is not None else inf
|
||||
angle = extraData['angle']
|
||||
tgtSigRadMods = []
|
||||
tgtSpeedMods = []
|
||||
total = 0
|
||||
distance = distance * 1000
|
||||
|
||||
for mod in fit.modules:
|
||||
if not mod.isEmpty and mod.state >= FittingModuleState.ACTIVE:
|
||||
if "remoteTargetPaintFalloff" in mod.item.effects or "structureModuleEffectTargetPainter" in mod.item.effects:
|
||||
tgtSigRadMods.append(
|
||||
1 + (mod.getModifiedItemAttr("signatureRadiusBonus") / 100)
|
||||
* self.calculateModuleMultiplier(mod, distance))
|
||||
if "remoteWebifierFalloff" in mod.item.effects or "structureModuleEffectStasisWebifier" in mod.item.effects:
|
||||
if distance <= mod.getModifiedItemAttr("maxRange"):
|
||||
tgtSpeedMods.append(1 + (mod.getModifiedItemAttr("speedFactor") / 100))
|
||||
elif mod.getModifiedItemAttr("falloffEffectiveness") > 0:
|
||||
# I am affected by falloff
|
||||
tgtSpeedMods.append(
|
||||
1 + (mod.getModifiedItemAttr("speedFactor") / 100) *
|
||||
self.calculateModuleMultiplier(mod, distance))
|
||||
|
||||
tgtSpeed = self.penalizeModChain(tgtSpeed, tgtSpeedMods)
|
||||
tgtSigRad = self.penalizeModChain(tgtSigRad, tgtSigRadMods)
|
||||
attRad = fit.ship.getModifiedItemAttr('radius', 0)
|
||||
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
|
||||
|
||||
for mod in fit.modules:
|
||||
dps = mod.getDps(targetResists=fit.targetResists, spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)).total
|
||||
if mod.hardpoint == FittingHardpoint.TURRET:
|
||||
if mod.state >= FittingModuleState.ACTIVE:
|
||||
total += dps * self.calculateTurretMultiplier(fit, mod, distance, angle, tgtSpeed, tgtSigRad)
|
||||
|
||||
elif mod.hardpoint == FittingHardpoint.MISSILE:
|
||||
if mod.state >= FittingModuleState.ACTIVE and mod.maxRange is not None and (mod.maxRange - attRad) >= distance:
|
||||
total += dps * self.calculateMissileMultiplier(mod, tgtSpeed, tgtSigRad)
|
||||
|
||||
if distance <= fit.extraAttributes['droneControlRange']:
|
||||
for drone in fit.drones:
|
||||
multiplier = 1 if drone.getModifiedItemAttr('maxVelocity') > 1 else self.calculateTurretMultiplier(
|
||||
fit, drone, distance, angle, tgtSpeed, tgtSigRad)
|
||||
dps = drone.getDps(targetResists=fit.targetResists).total
|
||||
total += dps * multiplier
|
||||
|
||||
# this is janky as fuck
|
||||
for fighter in fit.fighters:
|
||||
if not fighter.active:
|
||||
continue
|
||||
fighterDpsMap = fighter.getDpsPerEffect(targetResists=fit.targetResists)
|
||||
for ability in fighter.abilities:
|
||||
if ability.dealsDamage and ability.active:
|
||||
if ability.effectID not in fighterDpsMap:
|
||||
continue
|
||||
multiplier = self.calculateFighterMissileMultiplier(tgtSpeed, tgtSigRad, ability)
|
||||
dps = fighterDpsMap[ability.effectID].total
|
||||
total += dps * multiplier
|
||||
|
||||
return total
|
||||
|
||||
@staticmethod
|
||||
def calculateMissileMultiplier(mod, tgtSpeed, tgtSigRad):
|
||||
explosionRadius = mod.getModifiedChargeAttr('aoeCloudSize')
|
||||
explosionVelocity = mod.getModifiedChargeAttr('aoeVelocity')
|
||||
damageReductionFactor = mod.getModifiedChargeAttr('aoeDamageReductionFactor')
|
||||
|
||||
sigRadiusFactor = tgtSigRad / explosionRadius
|
||||
if tgtSpeed:
|
||||
velocityFactor = (explosionVelocity / explosionRadius * tgtSigRad / tgtSpeed) ** damageReductionFactor
|
||||
else:
|
||||
velocityFactor = 1
|
||||
|
||||
return min(sigRadiusFactor, velocityFactor, 1)
|
||||
|
||||
@classmethod
|
||||
def calculateTurretMultiplier(cls, fit, mod, distance, angle, tgtSpeed, tgtSigRad):
|
||||
# Source for most of turret calculation info: http://wiki.eveonline.com/en/wiki/Falloff
|
||||
chanceToHit = cls.calculateTurretChanceToHit(fit, mod, distance, angle, tgtSpeed, tgtSigRad)
|
||||
if chanceToHit > 0.01:
|
||||
# AvgDPS = Base Damage * [ ( ChanceToHit^2 + ChanceToHit + 0.0499 ) / 2 ]
|
||||
multiplier = (chanceToHit ** 2 + chanceToHit + 0.0499) / 2
|
||||
else:
|
||||
# All hits are wreckings
|
||||
multiplier = chanceToHit * 3
|
||||
dmgScaling = mod.getModifiedItemAttr('turretDamageScalingRadius')
|
||||
if dmgScaling:
|
||||
multiplier = min(1, (float(tgtSigRad) / dmgScaling) ** 2)
|
||||
return multiplier
|
||||
|
||||
@staticmethod
|
||||
def calculateFighterMissileMultiplier(tgtSpeed, tgtSigRad, ability):
|
||||
prefix = ability.attrPrefix
|
||||
|
||||
explosionRadius = ability.fighter.getModifiedItemAttr('{}ExplosionRadius'.format(prefix))
|
||||
explosionVelocity = ability.fighter.getModifiedItemAttr('{}ExplosionVelocity'.format(prefix))
|
||||
damageReductionFactor = ability.fighter.getModifiedItemAttr('{}ReductionFactor'.format(prefix), None)
|
||||
|
||||
# the following conditionals are because CCP can't keep a decent naming convention, as if fighter implementation
|
||||
# wasn't already fucked.
|
||||
if damageReductionFactor is None:
|
||||
damageReductionFactor = ability.fighter.getModifiedItemAttr('{}DamageReductionFactor'.format(prefix))
|
||||
|
||||
damageReductionSensitivity = ability.fighter.getModifiedItemAttr('{}ReductionSensitivity'.format(prefix), None)
|
||||
if damageReductionSensitivity is None:
|
||||
damageReductionSensitivity = ability.fighter.getModifiedItemAttr('{}DamageReductionSensitivity'.format(prefix))
|
||||
|
||||
sigRadiusFactor = tgtSigRad / explosionRadius
|
||||
|
||||
if tgtSpeed:
|
||||
velocityFactor = (explosionVelocity / explosionRadius * tgtSigRad / tgtSpeed) ** (
|
||||
log(damageReductionFactor) / log(damageReductionSensitivity))
|
||||
else:
|
||||
velocityFactor = 1
|
||||
|
||||
return min(sigRadiusFactor, velocityFactor, 1)
|
||||
|
||||
@staticmethod
|
||||
def calculateTurretChanceToHit(fit, mod, distance, angle, tgtSpeed, tgtSigRad):
|
||||
tracking = mod.getModifiedItemAttr('trackingSpeed')
|
||||
turretOptimal = mod.maxRange
|
||||
turretFalloff = mod.falloff
|
||||
turretSigRes = mod.getModifiedItemAttr('optimalSigRadius')
|
||||
transversal = sin(radians(angle)) * tgtSpeed
|
||||
|
||||
# Angular velocity is calculated using range from ship center to target center.
|
||||
# We do not know target radius but we know attacker radius
|
||||
angDistance = distance + fit.ship.getModifiedItemAttr('radius', 0)
|
||||
if angDistance == 0 and transversal == 0:
|
||||
angularVelocity = 0
|
||||
elif angDistance == 0 and transversal != 0:
|
||||
angularVelocity = inf
|
||||
else:
|
||||
angularVelocity = transversal / angDistance
|
||||
trackingEq = (((angularVelocity / tracking) *
|
||||
(turretSigRes / tgtSigRad)) ** 2)
|
||||
rangeEq = ((max(0, distance - turretOptimal)) / turretFalloff) ** 2
|
||||
|
||||
return 0.5 ** (trackingEq + rangeEq)
|
||||
|
||||
@staticmethod
|
||||
def calculateModuleMultiplier(mod, distance):
|
||||
# Simplified formula, we make some assumptions about the module
|
||||
# This is basically the calculateTurretChanceToHit without tracking values
|
||||
turretOptimal = mod.maxRange
|
||||
turretFalloff = mod.falloff
|
||||
rangeEq = ((max(0, distance - turretOptimal)) / turretFalloff) ** 2
|
||||
|
||||
return 0.5 ** rangeEq
|
||||
|
||||
@staticmethod
|
||||
def penalizeModChain(value, mods):
|
||||
mods.sort(key=lambda v: -abs(v - 1))
|
||||
try:
|
||||
for i in range(len(mods)):
|
||||
bonus = mods[i]
|
||||
value *= 1 + (bonus - 1) * exp(- i ** 2 / 7.1289)
|
||||
return value
|
||||
except Exception as e:
|
||||
pyfalog.critical('Caught exception when penalizing modifier chain.')
|
||||
pyfalog.critical(e)
|
||||
return value
|
||||
@@ -1,165 +0,0 @@
|
||||
# ===============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of eos.
|
||||
#
|
||||
# eos is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# eos is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from eos.graph import Graph
|
||||
from eos.utils.spoolSupport import SpoolType, SpoolOptions
|
||||
from gui.utils.numberFormatter import roundToPrec
|
||||
|
||||
|
||||
class FitDpsVsTimeGraph(Graph):
|
||||
|
||||
def getPlotPoints(self, fit, extraData, xRange, xAmount):
|
||||
# We deliberately ignore xAmount here to build graph which will reflect
|
||||
# all steps of building up the damage
|
||||
minX, maxX = self._limitXRange(xRange, fit, extraData)
|
||||
if fit.ID not in self._cache:
|
||||
self.__generateCache(fit, maxX)
|
||||
currentY = None
|
||||
prevY = None
|
||||
xs = []
|
||||
ys = []
|
||||
cache = self._cache[fit.ID]
|
||||
for time in sorted(cache):
|
||||
prevY = currentY
|
||||
currentX = time / 1000
|
||||
currentY = roundToPrec(cache[time], 6)
|
||||
if currentX < minX:
|
||||
continue
|
||||
# First set of data points
|
||||
if not xs:
|
||||
# Start at exactly requested time, at last known value
|
||||
initialY = prevY or 0
|
||||
xs.append(minX)
|
||||
ys.append(initialY)
|
||||
# If current time is bigger then starting, extend plot to that time with old value
|
||||
if currentX > minX:
|
||||
xs.append(currentX)
|
||||
ys.append(initialY)
|
||||
# If new value is different, extend it with new point to the new value
|
||||
if currentY != prevY:
|
||||
xs.append(currentX)
|
||||
ys.append(currentY)
|
||||
continue
|
||||
# Last data point
|
||||
if currentX >= maxX:
|
||||
xs.append(maxX)
|
||||
ys.append(prevY)
|
||||
break
|
||||
# Anything in-between
|
||||
if currentY != prevY:
|
||||
if prevY is not None:
|
||||
xs.append(currentX)
|
||||
ys.append(prevY)
|
||||
xs.append(currentX)
|
||||
ys.append(currentY)
|
||||
if max(xs) < maxX:
|
||||
xs.append(maxX)
|
||||
ys.append(currentY or 0)
|
||||
return xs, ys
|
||||
|
||||
def getYForX(self, fit, extraData, x):
|
||||
time = x * 1000
|
||||
cache = self._cache[fit.ID]
|
||||
closestTime = max((t for t in cache if t <= time), default=None)
|
||||
if closestTime is None:
|
||||
return 0
|
||||
return roundToPrec(cache[closestTime], 6)
|
||||
|
||||
def _getXLimits(self, fit, extraData):
|
||||
return 0, 2500
|
||||
|
||||
def __generateCache(self, fit, maxTime):
|
||||
cache = []
|
||||
|
||||
def addDmg(addedTimeStart, addedTimeFinish, addedDmg):
|
||||
if addedDmg == 0:
|
||||
return
|
||||
addedDps = 1000 * addedDmg / (addedTimeFinish - addedTimeStart)
|
||||
cache.append((addedTimeStart, addedTimeFinish, addedDps))
|
||||
|
||||
# We'll handle calculations in milliseconds
|
||||
maxTime = maxTime * 1000
|
||||
for mod in fit.modules:
|
||||
if not mod.isDealingDamage():
|
||||
continue
|
||||
cycleParams = mod.getCycleParameters(reloadOverride=True)
|
||||
if cycleParams is None:
|
||||
continue
|
||||
currentTime = 0
|
||||
nonstopCycles = 0
|
||||
for cycleTime, inactiveTime in cycleParams.iterCycles():
|
||||
cycleDamage = 0
|
||||
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
|
||||
for volleyTime, volley in volleyParams.items():
|
||||
cycleDamage += volley.total
|
||||
addDmg(currentTime, currentTime + cycleTime, cycleDamage)
|
||||
currentTime += cycleTime + inactiveTime
|
||||
if inactiveTime > 0:
|
||||
nonstopCycles = 0
|
||||
else:
|
||||
nonstopCycles += 1
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
for drone in fit.drones:
|
||||
if not drone.isDealingDamage():
|
||||
continue
|
||||
cycleParams = drone.getCycleParameters(reloadOverride=True)
|
||||
if cycleParams is None:
|
||||
continue
|
||||
currentTime = 0
|
||||
for cycleTime, inactiveTime in cycleParams.iterCycles():
|
||||
cycleDamage = 0
|
||||
volleyParams = drone.getVolleyParameters()
|
||||
for volleyTime, volley in volleyParams.items():
|
||||
cycleDamage += volley.total
|
||||
addDmg(currentTime, currentTime + cycleTime, cycleDamage)
|
||||
currentTime += cycleTime + inactiveTime
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
for fighter in fit.fighters:
|
||||
if not fighter.isDealingDamage():
|
||||
continue
|
||||
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
|
||||
abilityVolleyParams = volleyParams[effectID]
|
||||
currentTime = 0
|
||||
for cycleTime, inactiveTime in abilityCycleParams.iterCycles():
|
||||
cycleDamage = 0
|
||||
for volleyTime, volley in abilityVolleyParams.items():
|
||||
cycleDamage += volley.total
|
||||
addDmg(currentTime, currentTime + cycleTime, cycleDamage)
|
||||
currentTime += cycleTime + inactiveTime
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
|
||||
# Post-process cache
|
||||
finalCache = {}
|
||||
for time in sorted(set(chain((i[0] for i in cache), (i[1] for i in cache)))):
|
||||
entries = (e for e in cache if e[0] <= time < e[1])
|
||||
dps = sum(e[2] for e in entries)
|
||||
finalCache[time] = dps
|
||||
self._cache[fit.ID] = finalCache
|
||||
@@ -1,27 +0,0 @@
|
||||
import math
|
||||
from logbook import Logger
|
||||
|
||||
from eos.graph import SmoothGraph
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class FitShieldAmountVsTimeGraph(SmoothGraph):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
import gui.mainFrame
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
|
||||
def getYForX(self, fit, extraData, time):
|
||||
if time < 0:
|
||||
return 0
|
||||
maxShield = fit.ship.getModifiedItemAttr('shieldCapacity')
|
||||
regenTime = fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000
|
||||
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate (shield is similar to cap)
|
||||
shield = maxShield * (1 + math.exp(5 * -time / regenTime) * -1) ** 2
|
||||
useEhp = self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective
|
||||
if fit.damagePattern is not None and useEhp:
|
||||
shield = fit.damagePattern.effectivify(fit, shield, 'shield')
|
||||
return shield
|
||||
@@ -1,22 +0,0 @@
|
||||
import math
|
||||
|
||||
from eos.graph import SmoothGraph
|
||||
|
||||
|
||||
class FitShieldRegenVsShieldPercGraph(SmoothGraph):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
import gui.mainFrame
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
|
||||
def getYForX(self, fit, extraData, perc):
|
||||
maxShield = fit.ship.getModifiedItemAttr('shieldCapacity')
|
||||
regenTime = fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000
|
||||
currentShield = maxShield * perc / 100
|
||||
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate (shield is similar to cap)
|
||||
regen = 10 * maxShield / regenTime * (math.sqrt(currentShield / maxShield) - currentShield / maxShield)
|
||||
useEhp = self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective
|
||||
if fit.damagePattern is not None and useEhp:
|
||||
regen = fit.damagePattern.effectivify(fit, regen, 'shield')
|
||||
return regen
|
||||
@@ -1,14 +0,0 @@
|
||||
import math
|
||||
|
||||
from eos.graph import SmoothGraph
|
||||
|
||||
|
||||
class FitSpeedVsTimeGraph(SmoothGraph):
|
||||
|
||||
def getYForX(self, fit, extraData, time):
|
||||
maxSpeed = fit.ship.getModifiedItemAttr('maxVelocity')
|
||||
mass = fit.ship.getModifiedItemAttr('mass')
|
||||
agility = fit.ship.getModifiedItemAttr('agility')
|
||||
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
|
||||
speed = maxSpeed * (1 - math.exp((-time * 1000000) / (agility * mass)))
|
||||
return speed
|
||||
@@ -1,98 +0,0 @@
|
||||
import math
|
||||
|
||||
from eos.const import FittingModuleState
|
||||
from eos.graph import SmoothGraph
|
||||
|
||||
|
||||
AU_METERS = 149597870700
|
||||
|
||||
|
||||
class FitWarpTimeVsDistanceGraph(SmoothGraph):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.subwarpSpeed = None
|
||||
|
||||
def getYForX(self, fit, extraData, distance):
|
||||
if distance == 0:
|
||||
return 0
|
||||
if fit.ID not in self._cache:
|
||||
self.__generateCache(fit)
|
||||
maxWarpSpeed = fit.warpSpeed
|
||||
subwarpSpeed = self._cache[fit.ID]['cleanSubwarpSpeed']
|
||||
time = calculate_time_in_warp(maxWarpSpeed, subwarpSpeed, distance * AU_METERS)
|
||||
return time
|
||||
|
||||
def _getXLimits(self, fit, extraData):
|
||||
return 0, fit.maxWarpDistance
|
||||
|
||||
def __generateCache(self, fit):
|
||||
modStates = {}
|
||||
for mod in fit.modules:
|
||||
if mod.item is not None and mod.item.group.name in ('Propulsion Module', 'Mass Entanglers', 'Cloaking Device') and mod.state >= FittingModuleState.ACTIVE:
|
||||
modStates[mod] = mod.state
|
||||
mod.state = FittingModuleState.ONLINE
|
||||
projFitStates = {}
|
||||
for projFit in fit.projectedFits:
|
||||
projectionInfo = projFit.getProjectionInfo(fit.ID)
|
||||
if projectionInfo is not None and projectionInfo.active:
|
||||
projFitStates[projectionInfo] = projectionInfo.active
|
||||
projectionInfo.active = False
|
||||
projModStates = {}
|
||||
for mod in fit.projectedModules:
|
||||
if not mod.isExclusiveSystemEffect and mod.state >= FittingModuleState.ACTIVE:
|
||||
projModStates[mod] = mod.state
|
||||
mod.state = FittingModuleState.ONLINE
|
||||
projDroneStates = {}
|
||||
for drone in fit.projectedDrones:
|
||||
if drone.amountActive > 0:
|
||||
projDroneStates[drone] = drone.amountActive
|
||||
drone.amountActive = 0
|
||||
projFighterStates = {}
|
||||
for fighter in fit.projectedFighters:
|
||||
if fighter.active:
|
||||
projFighterStates[fighter] = fighter.active
|
||||
fighter.active = False
|
||||
fit.calculateModifiedAttributes()
|
||||
self._cache[fit.ID] = {'cleanSubwarpSpeed': fit.ship.getModifiedItemAttr('maxVelocity')}
|
||||
for projInfo, state in projFitStates.items():
|
||||
projInfo.active = state
|
||||
for mod, state in modStates.items():
|
||||
mod.state = state
|
||||
for mod, state in projModStates.items():
|
||||
mod.state = state
|
||||
for drone, amountActive in projDroneStates.items():
|
||||
drone.amountActive = amountActive
|
||||
for fighter, state in projFighterStates.items():
|
||||
fighter.active = state
|
||||
fit.calculateModifiedAttributes()
|
||||
|
||||
|
||||
# Taken from https://wiki.eveuniversity.org/Warp_time_calculation#Implementation
|
||||
# with minor modifications
|
||||
# Warp speed in AU/s, subwarp speed in m/s, distance in m
|
||||
def calculate_time_in_warp(max_warp_speed, max_subwarp_speed, warp_dist):
|
||||
|
||||
k_accel = max_warp_speed
|
||||
k_decel = min(max_warp_speed / 3, 2)
|
||||
|
||||
warp_dropout_speed = max_subwarp_speed / 2
|
||||
max_ms_warp_speed = max_warp_speed * AU_METERS
|
||||
|
||||
accel_dist = AU_METERS
|
||||
decel_dist = max_ms_warp_speed / k_decel
|
||||
|
||||
minimum_dist = accel_dist + decel_dist
|
||||
|
||||
cruise_time = 0
|
||||
|
||||
if minimum_dist > warp_dist:
|
||||
max_ms_warp_speed = warp_dist * k_accel * k_decel / (k_accel + k_decel)
|
||||
else:
|
||||
cruise_time = (warp_dist - minimum_dist) / max_ms_warp_speed
|
||||
|
||||
accel_time = math.log(max_ms_warp_speed / k_accel) / k_accel
|
||||
decel_time = math.log(max_ms_warp_speed / warp_dropout_speed) / k_decel
|
||||
|
||||
total_time = cruise_time + accel_time + decel_time
|
||||
return total_time
|
||||
@@ -202,6 +202,13 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
explosive=volleyValue.explosive * (1 - getattr(targetResists, "explosiveAmount", 0)))
|
||||
return adjustedVolley
|
||||
|
||||
def getVolleyPerEffect(self, targetResists=None):
|
||||
volleyParams = self.getVolleyParametersPerEffect(targetResists=targetResists)
|
||||
volleyMap = {}
|
||||
for effectID, volleyData in volleyParams.items():
|
||||
volleyMap[effectID] = volleyData[0]
|
||||
return volleyMap
|
||||
|
||||
def getVolley(self, targetResists=None):
|
||||
volleyParams = self.getVolleyParametersPerEffect(targetResists=targetResists)
|
||||
em = 0
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
from utils.repr import makeReprStr
|
||||
|
||||
|
||||
class DmgTypes:
|
||||
"""Container for damage data stats."""
|
||||
|
||||
@@ -68,3 +71,41 @@ class DmgTypes:
|
||||
self.explosive += other.explosive
|
||||
self._calcTotal()
|
||||
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)
|
||||
|
||||
def __imul__(self, mul):
|
||||
if mul == 1:
|
||||
return
|
||||
self.em *= mul
|
||||
self.thermal *= mul
|
||||
self.kinetic *= mul
|
||||
self.explosive *= mul
|
||||
self._calcTotal()
|
||||
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)
|
||||
|
||||
def __itruediv__(self, div):
|
||||
if div == 1:
|
||||
return
|
||||
self.em /= div
|
||||
self.thermal /= div
|
||||
self.kinetic /= div
|
||||
self.explosive /= div
|
||||
self._calcTotal()
|
||||
return self
|
||||
|
||||
def __repr__(self):
|
||||
spec = ['em', 'thermal', 'kinetic', 'explosive', 'total']
|
||||
return makeReprStr(self, spec)
|
||||
|
||||
Reference in New Issue
Block a user