diff --git a/eos/graph/fitDpsRange.py b/eos/graph/fitDpsVsRange.py
similarity index 55%
rename from eos/graph/fitDpsRange.py
rename to eos/graph/fitDpsVsRange.py
index 5470074c4..8957a6d3a 100644
--- a/eos/graph/fitDpsRange.py
+++ b/eos/graph/fitDpsVsRange.py
@@ -22,75 +22,56 @@ from math import exp, log, radians, sin, inf
from logbook import Logger
from eos.const import FittingHardpoint, FittingModuleState
-from eos.graph import Graph
+from eos.graph import SmoothGraph
pyfalog = Logger(__name__)
-class FitDpsRangeGraph(Graph):
+class FitDpsVsRangeGraph(SmoothGraph):
- defaults = {
- "angle" : 0,
- "distance" : 0,
- "signatureRadius": None,
- "velocity" : 0
- }
-
- def __init__(self, fit, data=None):
- Graph.__init__(self, fit, self.calcDps, data if data is not None else self.defaults)
- self.fit = fit
-
- def calcDps(self, data):
- ew = {'signatureRadius': [], 'velocity': []}
- fit = self.fit
+ 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 = data["distance"] * 1000
- abssort = lambda _val: -abs(_val - 1)
+ 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:
- ew['signatureRadius'].append(
- 1 + (mod.getModifiedItemAttr("signatureRadiusBonus") / 100) * self.calculateModuleMultiplier(
- mod, data))
+ 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"):
- ew['velocity'].append(1 + (mod.getModifiedItemAttr("speedFactor") / 100))
+ tgtSpeedMods.append(1 + (mod.getModifiedItemAttr("speedFactor") / 100))
elif mod.getModifiedItemAttr("falloffEffectiveness") > 0:
# I am affected by falloff
- ew['velocity'].append(
- 1 + (mod.getModifiedItemAttr("speedFactor") / 100) * self.calculateModuleMultiplier(mod,
- data))
+ tgtSpeedMods.append(
+ 1 + (mod.getModifiedItemAttr("speedFactor") / 100) *
+ self.calculateModuleMultiplier(mod, distance))
- ew['signatureRadius'].sort(key=abssort)
- ew['velocity'].sort(key=abssort)
-
- for attr, values in ew.items():
- val = data[attr]
- try:
- for i in range(len(values)):
- bonus = values[i]
- val *= 1 + (bonus - 1) * exp(- i ** 2 / 7.1289)
- data[attr] = val
- except Exception as e:
- pyfalog.critical("Caught exception in calcDPS.")
- pyfalog.critical(e)
+ tgtSpeed = self.penalizeModChain(tgtSpeed, tgtSpeedMods)
+ tgtSigRad = self.penalizeModChain(tgtSigRad, tgtSigRadMods)
+ attRad = fit.ship.getModifiedItemAttr('radius', 0)
for mod in fit.modules:
dps = mod.getDps(targetResists=fit.targetResists).total
if mod.hardpoint == FittingHardpoint.TURRET:
if mod.state >= FittingModuleState.ACTIVE:
- total += dps * self.calculateTurretMultiplier(mod, data)
+ 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 >= distance:
- total += dps * self.calculateMissileMultiplier(mod, data)
+ 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"]:
+ if distance <= fit.extraAttributes['droneControlRange']:
for drone in fit.drones:
- multiplier = 1 if drone.getModifiedItemAttr("maxVelocity") > 1 else self.calculateTurretMultiplier(
- drone, data)
+ 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
@@ -103,88 +84,79 @@ class FitDpsRangeGraph(Graph):
if ability.dealsDamage and ability.active:
if ability.effectID not in fighterDpsMap:
continue
- multiplier = self.calculateFighterMissileMultiplier(ability, data)
+ multiplier = self.calculateFighterMissileMultiplier(tgtSpeed, tgtSigRad, ability)
dps = fighterDpsMap[ability.effectID].total
total += dps * multiplier
return total
@staticmethod
- def calculateMissileMultiplier(mod, data):
- targetSigRad = data["signatureRadius"]
- targetVelocity = data["velocity"]
- explosionRadius = mod.getModifiedChargeAttr("aoeCloudSize")
- targetSigRad = explosionRadius if targetSigRad is None else targetSigRad
- explosionVelocity = mod.getModifiedChargeAttr("aoeVelocity")
- damageReductionFactor = mod.getModifiedChargeAttr("aoeDamageReductionFactor")
+ def calculateMissileMultiplier(mod, tgtSpeed, tgtSigRad):
+ explosionRadius = mod.getModifiedChargeAttr('aoeCloudSize')
+ explosionVelocity = mod.getModifiedChargeAttr('aoeVelocity')
+ damageReductionFactor = mod.getModifiedChargeAttr('aoeDamageReductionFactor')
- sigRadiusFactor = targetSigRad / explosionRadius
- if targetVelocity:
- velocityFactor = (explosionVelocity / explosionRadius * targetSigRad / targetVelocity) ** damageReductionFactor
+ sigRadiusFactor = tgtSigRad / explosionRadius
+ if tgtSpeed:
+ velocityFactor = (explosionVelocity / explosionRadius * tgtSigRad / tgtSpeed) ** damageReductionFactor
else:
velocityFactor = 1
return min(sigRadiusFactor, velocityFactor, 1)
- def calculateTurretMultiplier(self, mod, data):
+ @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 = self.calculateTurretChanceToHit(mod, data)
+ 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")
+ dmgScaling = mod.getModifiedItemAttr('turretDamageScalingRadius')
if dmgScaling:
- targetSigRad = data["signatureRadius"]
- multiplier = min(1, (float(targetSigRad) / dmgScaling) ** 2)
+ multiplier = min(1, (float(tgtSigRad) / dmgScaling) ** 2)
return multiplier
@staticmethod
- def calculateFighterMissileMultiplier(ability, data):
+ def calculateFighterMissileMultiplier(tgtSpeed, tgtSigRad, ability):
prefix = ability.attrPrefix
- targetSigRad = data["signatureRadius"]
- targetVelocity = data["velocity"]
- explosionRadius = ability.fighter.getModifiedItemAttr("{}ExplosionRadius".format(prefix))
- explosionVelocity = ability.fighter.getModifiedItemAttr("{}ExplosionVelocity".format(prefix))
- damageReductionFactor = ability.fighter.getModifiedItemAttr("{}ReductionFactor".format(prefix), None)
+ 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))
+ damageReductionFactor = ability.fighter.getModifiedItemAttr('{}DamageReductionFactor'.format(prefix))
- damageReductionSensitivity = ability.fighter.getModifiedItemAttr("{}ReductionSensitivity".format(prefix), None)
+ damageReductionSensitivity = ability.fighter.getModifiedItemAttr('{}ReductionSensitivity'.format(prefix), None)
if damageReductionSensitivity is None:
- damageReductionSensitivity = ability.fighter.getModifiedItemAttr(
- "{}DamageReductionSensitivity".format(prefix))
+ damageReductionSensitivity = ability.fighter.getModifiedItemAttr('{}DamageReductionSensitivity'.format(prefix))
- targetSigRad = explosionRadius if targetSigRad is None else targetSigRad
- sigRadiusFactor = targetSigRad / explosionRadius
+ sigRadiusFactor = tgtSigRad / explosionRadius
- if targetVelocity:
- velocityFactor = (explosionVelocity / explosionRadius * targetSigRad / targetVelocity) ** (
+ if tgtSpeed:
+ velocityFactor = (explosionVelocity / explosionRadius * tgtSigRad / tgtSpeed) ** (
log(damageReductionFactor) / log(damageReductionSensitivity))
else:
velocityFactor = 1
return min(sigRadiusFactor, velocityFactor, 1)
- def calculateTurretChanceToHit(self, mod, data):
- distance = data["distance"] * 1000
- tracking = mod.getModifiedItemAttr("trackingSpeed")
+ @staticmethod
+ def calculateTurretChanceToHit(fit, mod, distance, angle, tgtSpeed, tgtSigRad):
+ tracking = mod.getModifiedItemAttr('trackingSpeed')
turretOptimal = mod.maxRange
turretFalloff = mod.falloff
- turretSigRes = mod.getModifiedItemAttr("optimalSigRadius")
- targetSigRad = data["signatureRadius"]
- targetSigRad = turretSigRes if targetSigRad is None else targetSigRad
- transversal = sin(radians(data["angle"])) * data["velocity"]
+ 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 + self.fit.ship.getModifiedItemAttr('radius', 0)
+ angDistance = distance + fit.ship.getModifiedItemAttr('radius', 0)
if angDistance == 0 and transversal == 0:
angularVelocity = 0
elif angDistance == 0 and transversal != 0:
@@ -192,18 +164,30 @@ class FitDpsRangeGraph(Graph):
else:
angularVelocity = transversal / angDistance
trackingEq = (((angularVelocity / tracking) *
- (turretSigRes / targetSigRad)) ** 2)
+ (turretSigRes / tgtSigRad)) ** 2)
rangeEq = ((max(0, distance - turretOptimal)) / turretFalloff) ** 2
return 0.5 ** (trackingEq + rangeEq)
@staticmethod
- def calculateModuleMultiplier(mod, data):
+ def calculateModuleMultiplier(mod, distance):
# Simplified formula, we make some assumptions about the module
# This is basically the calculateTurretChanceToHit without tracking values
- distance = data["distance"] * 1000
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
diff --git a/gui/builtinGraphs/__init__.py b/gui/builtinGraphs/__init__.py
index e5cdd82c0..d943dcf04 100644
--- a/gui/builtinGraphs/__init__.py
+++ b/gui/builtinGraphs/__init__.py
@@ -1,6 +1,6 @@
# noinspection PyUnresolvedReferences
from gui.builtinGraphs import ( # noqa: E402,F401
- # fitDpsRange,
+ fitDpsVsRange,
fitDmgVsTime,
fitShieldRegenVsShieldPerc,
fitShieldAmountVsTime,
diff --git a/gui/builtinGraphs/fitDpsRange.py b/gui/builtinGraphs/fitDpsRange.py
deleted file mode 100644
index 97c75c18b..000000000
--- a/gui/builtinGraphs/fitDpsRange.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# =============================================================================
-# Copyright (C) 2010 Diego Duclos
-#
-# This file is part of pyfa.
-#
-# pyfa is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# pyfa 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 General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with pyfa. If not, see .
-# =============================================================================
-
-import gui.mainFrame
-from eos.graph import Data
-from eos.graph.fitDpsRange import FitDpsRangeGraph as EosFitDpsRangeGraph
-from gui.bitmap_loader import BitmapLoader
-from gui.graph import Graph
-from service.attribute import Attribute
-
-
-class FitDpsRangeGraph(Graph):
-
- propertyAttributeMap = {"angle": "maxVelocity",
- "distance": "maxRange",
- "signatureRadius": "signatureRadius",
- "velocity": "maxVelocity"}
-
- propertyLabelMap = {"angle": "Target Angle (degrees)",
- "distance": "Distance to Target (km)",
- "signatureRadius": "Target Signature Radius (m)",
- "velocity": "Target Velocity (m/s)"}
-
- defaults = EosFitDpsRangeGraph.defaults.copy()
-
- def __init__(self):
- Graph.__init__(self)
- self.defaults["distance"] = "0-100"
- self.name = "DPS vs Range"
- self.eosGraph = None
- self.mainFrame = gui.mainFrame.MainFrame.getInstance()
-
- def getFields(self):
- return self.defaults
-
- def getLabels(self):
- return self.propertyLabelMap
-
- def getIcons(self):
- icons = {}
- sAttr = Attribute.getInstance()
- for key, attrName in self.propertyAttributeMap.items():
- iconFile = sAttr.getAttributeInfo(attrName).iconID
- bitmap = BitmapLoader.getBitmap(iconFile, "icons")
- if bitmap:
- icons[key] = bitmap
-
- return icons
-
- def getPoints(self, fit, fields):
- eosGraph = getattr(self, "eosGraph", None)
- if eosGraph is None or eosGraph.fit != fit:
- eosGraph = self.eosGraph = EosFitDpsRangeGraph(fit)
-
- eosGraph.clearData()
- variable = None
- for fieldName, value in fields.items():
- d = Data(fieldName, value)
- if not d.isConstant():
- if variable is None:
- variable = fieldName
- else:
- # We can't handle more then one variable atm, OOPS FUCK OUT
- return False, "Can only handle 1 variable"
-
- eosGraph.setData(d)
-
- if variable is None:
- return False, "No variable"
-
- x = []
- y = []
- for point, val in eosGraph.getIterator():
- x.append(point[variable])
- y.append(val)
-
- return x, y
-
-
-FitDpsRangeGraph.register()
diff --git a/gui/builtinGraphs/fitDpsVsRange.py b/gui/builtinGraphs/fitDpsVsRange.py
new file mode 100644
index 000000000..eccb6991c
--- /dev/null
+++ b/gui/builtinGraphs/fitDpsVsRange.py
@@ -0,0 +1,50 @@
+# =============================================================================
+# Copyright (C) 2010 Diego Duclos
+#
+# This file is part of pyfa.
+#
+# pyfa is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# pyfa 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with pyfa. If not, see .
+# =============================================================================
+
+
+from collections import OrderedDict
+
+from eos.graph.fitDpsVsRange import FitDpsVsRangeGraph as EosGraph
+from gui.graph import Graph, XDef, YDef, ExtraInput
+
+
+class FitDpsVsRangeGraph(Graph):
+
+ name = 'DPS vs Range'
+
+ def __init__(self):
+ self.eosGraph = EosGraph()
+
+ @property
+ def xDef(self):
+ return XDef(inputDefault='0-100', inputLabel='Distance to target (km)', inputIconID=1391, axisLabel='Distance to target, km')
+
+ @property
+ def yDefs(self):
+ return OrderedDict([('dps', YDef(switchLabel='DPS', axisLabel='DPS', eosGraph='eosGraph'))])
+
+ @property
+ def extraInputs(self):
+ return OrderedDict([
+ ('speed', ExtraInput(inputDefault=0, inputLabel='Target speed (m/s)', inputIconID=1389)),
+ ('angle', ExtraInput(inputDefault=0, inputLabel='Target angle (degrees)', inputIconID=1389)),
+ ('signatureRadius', ExtraInput(inputDefault=None, inputLabel='Target signature radius (m)', inputIconID=1390))])
+
+
+FitDpsVsRangeGraph.register()
diff --git a/gui/graph.py b/gui/graph.py
index 8e555002c..ffbd92578 100644
--- a/gui/graph.py
+++ b/gui/graph.py
@@ -57,6 +57,7 @@ class Graph(metaclass=ABCMeta):
def getPlotPoints(self, fit, extraData, xRange, xAmount, yType):
xRange = self.parseRange(xRange)
+ extraData = {k: float(v) if v else None for k, v in extraData.items()}
graph = getattr(self, self.yDefs[yType].eosGraph, None)
return graph.getPlotPoints(fit, extraData, xRange, xAmount)
@@ -78,7 +79,7 @@ class Graph(metaclass=ABCMeta):
XDef = namedtuple('XDef', ('inputDefault', 'inputLabel', 'inputIconID', 'axisLabel'))
YDef = namedtuple('YDef', ('switchLabel', 'axisLabel', 'eosGraph'))
-ExtraInput = namedtuple('ExtraInput', ('handle', 'inputDefault', 'inputLabel', 'inputIconID'))
+ExtraInput = namedtuple('ExtraInput', ('inputDefault', 'inputLabel', 'inputIconID'))
# noinspection PyUnresolvedReferences