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