diff --git a/gui/builtinGraphs/fitDamageStats/__init__.py b/gui/builtinGraphs/fitDamageStats/__init__.py new file mode 100644 index 000000000..b01f971ca --- /dev/null +++ b/gui/builtinGraphs/fitDamageStats/__init__.py @@ -0,0 +1 @@ +import gui.builtinGraphs.fitDamageStats.graph # noqa: E402,F401 diff --git a/gui/builtinGraphs/fitDamageStats/cacheTime.py b/gui/builtinGraphs/fitDamageStats/cacheTime.py new file mode 100644 index 000000000..b711ecadb --- /dev/null +++ b/gui/builtinGraphs/fitDamageStats/cacheTime.py @@ -0,0 +1,24 @@ +# ============================================================================= +# 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 . +# ============================================================================= + + +class TimeCache: + + def __init__(self): + self.data = {} diff --git a/gui/builtinGraphs/fitDamageStats/calc.py b/gui/builtinGraphs/fitDamageStats/calc.py new file mode 100644 index 000000000..fe7dc1124 --- /dev/null +++ b/gui/builtinGraphs/fitDamageStats/calc.py @@ -0,0 +1,207 @@ +# ============================================================================= +# 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 math + + +def getTurretMult(mod, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): + cth = _calcTurretChanceToHit( + atkSpeed=atkSpeed, + atkAngle=atkAngle, + atkRadius=fit.ship.getModifiedItemAttr('radius'), + atkOptimalRange=mod.maxRange, + atkFalloffRange=mod.falloff, + atkTracking=mod.getModifiedItemAttr('trackingSpeed'), + atkOptimalSigRadius=mod.getModifiedItemAttr('optimalSigRadius'), + distance=distance, + tgtSpeed=tgtSpeed, + tgtAngle=tgtAngle, + tgtRadius=tgt.ship.getModifiedItemAttr('radius'), + tgtSigRadius=tgtSigRadius) + mult = _calcTurretMult(cth) + return mult + + +def getLauncherMult(mod, fit, distance, tgtSpeed, tgtSigRadius): + modRange = mod.maxRange + if modRange is None: + return 0 + mult = _calcMissileMult( + atkRadius=fit.ship.getModifiedItemAttr('radius'), + atkRange=modRange, + atkEr=mod.getModifiedChargeAttr('aoeCloudSize'), + atkEv=mod.getModifiedChargeAttr('aoeVelocity'), + atkDrf=mod.getModifiedChargeAttr('aoeDamageReductionFactor'), + distance=distance, + tgtSpeed=tgtSpeed, + tgtSigRadius=tgtSigRadius) + return mult + + +def getDroneMult(drone, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): + if distance > fit.extraAttributes['droneControlRange']: + return 0 + droneSpeed = drone.getModifiedItemAttr('maxVelocity') + # Hard to simulate drone behavior, so assume chance to hit is 1 + # when drone is not sentry and is faster than its target + if droneSpeed > 1 and droneSpeed >= tgtSpeed: + cth = 1 + # Otherwise put the drone into center of the ship, move it at its max speed or ship's speed + # (whichever is lower) towards direction of attacking ship and see how well it projects + else: + droneRadius = drone.getModifiedItemAttr('radius') + cth = _calcTurretChanceToHit( + atkSpeed=min(atkSpeed, droneSpeed), + atkAngle=atkAngle, + atkRadius=droneRadius, + atkOptimalRange=drone.maxRange, + atkFalloffRange=drone.falloff, + atkTracking=drone.getModifiedItemAttr('trackingSpeed'), + atkOptimalSigRadius=drone.getModifiedItemAttr('optimalSigRadius'), + # As distance is ship surface to ship surface, we adjust it according + # to attacker fit's radiuses to have drone surface to ship surface distance + distance=distance + fit.ship.getModifiedItemAttr('radius') - droneRadius, + tgtSpeed=tgtSpeed, + tgtAngle=tgtAngle, + tgtRadius=tgt.ship.getModifiedItemAttr('radius'), + tgtSigRadius=tgtSigRadius) + mult = _calcTurretMult(cth) + return mult + + +def getFighterAbilityMult(fighter, ability, fit, distance, tgtSpeed, tgtSigRadius): + fighterSpeed = fighter.getModifiedItemAttr('maxVelocity') + attrPrefix = ability.attrPrefix + if fighterSpeed >= tgtSpeed: + rangeFactor = 1 + # Same as with drones, if fighters are slower - put them to center of + # the ship and see how they apply + else: + rangeFactor = _calcRangeFactor( + atkOptimalRange=fighter.getModifiedItemAttr('{}RangeOptimal'.format(attrPrefix)), + atkFalloffRange=fighter.getModifiedItemAttr('{}RangeFalloff'.format(attrPrefix)), + distance=distance + fit.ship.getModifiedItemAttr('radius') - fighter.getModifiedItemAttr('radius')) + drf = fighter.getModifiedItemAttr('{}ReductionFactor'.format(attrPrefix), None) + if drf is None: + drf = fighter.getModifiedItemAttr('{}DamageReductionFactor'.format(attrPrefix)) + drs = fighter.getModifiedItemAttr('{}ReductionSensitivity'.format(attrPrefix), None) + if drs is None: + drs = fighter.getModifiedItemAttr('{}DamageReductionSensitivity'.format(attrPrefix)) + missileFactor = _calcMissileFactor( + atkEr=fighter.getModifiedItemAttr('{}ExplosionRadius'.format(attrPrefix)), + atkEv=fighter.getModifiedItemAttr('{}ExplosionVelocity'.format(attrPrefix)), + atkDrf=_calcAggregatedDrf(reductionFactor=drf, reductionSensitivity=drs), + tgtSpeed=tgtSpeed, + tgtSigRadius=tgtSigRadius) + mult = rangeFactor * missileFactor + return mult + + +# Turret-specific +def _calcTurretMult(chanceToHit): + """Calculate damage multiplier for turret-based weapons.""" + # https://wiki.eveuniversity.org/Turret_mechanics#Damage + wreckingChance = min(chanceToHit, 0.01) + wreckingPart = wreckingChance * 3 + normalChance = chanceToHit - wreckingChance + if normalChance > 0: + avgDamageMult = (0.01 + chanceToHit) / 2 + 0.49 + normalPart = normalChance * avgDamageMult + else: + normalPart = 0 + totalMult = normalPart + wreckingPart + return totalMult + + +def _calcTurretChanceToHit( + atkSpeed, atkAngle, atkRadius, atkOptimalRange, atkFalloffRange, atkTracking, atkOptimalSigRadius, + distance, tgtSpeed, tgtAngle, tgtRadius, tgtSigRadius +): + """Calculate chance to hit for turret-based weapons.""" + # https://wiki.eveuniversity.org/Turret_mechanics#Hit_Math + angularSpeed = _calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius) + rangeFactor = _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) + trackingFactor = _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius) + cth = rangeFactor * trackingFactor + return cth + + +def _calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius): + """Calculate angular speed based on mobility parameters of two ships.""" + atkAngle = atkAngle * math.pi / 180 + tgtAngle = tgtAngle * math.pi / 180 + ctcDistance = atkRadius + distance + tgtRadius + # Target is to the right of the attacker, so transversal is projection onto Y axis + transSpeed = abs(atkSpeed * math.sin(atkAngle) - tgtSpeed * math.sin(tgtAngle)) + if ctcDistance == 0: + angularSpeed = 0 if transSpeed == 0 else math.inf + else: + angularSpeed = transSpeed / ctcDistance + return angularSpeed + + +def _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius): + """Calculate tracking chance to hit component.""" + return 0.5 ** (((angularSpeed * atkOptimalSigRadius) / (atkTracking * tgtSigRadius)) ** 2) + + +# Missile-specific +def _calcMissileMult(atkRadius, atkRange, atkEr, atkEv, atkDrf, distance, tgtSpeed, tgtSigRadius): + """Calculate damage multiplier for missile launcher.""" + # Missiles spawn in the center of the attacking ship + if distance + atkRadius > atkRange: + mult = 0 + else: + mult = _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius) + return mult + + +def _calcFighterMult(atkOptimalRange, atkFalloffRange, atkEr, atkEv, atkDrf, distance, tgtSpeed, tgtSigRadius): + """Calculate damage multiplier for separate fighter ability,""" + rangeFactor = _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) + missileFactor = _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius) + mult = rangeFactor * missileFactor + return mult + + +def _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius): + """Missile application.""" + factors = [1] + # "Slow" part + factors.append(tgtSigRadius / atkEr) + # "Fast" part + if tgtSpeed > 0: + factors.append(((atkEv * tgtSigRadius) / (atkEr * tgtSpeed)) ** atkDrf) + totalMult = min(factors) + return totalMult + + +def _calcAggregatedDrf(reductionFactor, reductionSensitivity): + """ + Sometimes DRF is specified as 2 separate numbers, + here we combine them into generic form. + """ + return math.log(reductionFactor) / math.log(reductionSensitivity) + + +# Generic +def _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance): + """Range strength/chance factor, applicable to guns, ewar, RRs, etc.""" + return 0.5 ** ((max(0, distance - atkOptimalRange) / atkFalloffRange) ** 2) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats/graph.py similarity index 72% rename from gui/builtinGraphs/fitDamageStats.py rename to gui/builtinGraphs/fitDamageStats/graph.py index 470d0496b..1ef75fd1c 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats/graph.py @@ -18,7 +18,6 @@ # ============================================================================= -import math from copy import copy from itertools import chain @@ -27,7 +26,8 @@ from eos.const import FittingHardpoint, FittingModuleState from eos.utils.float import floatUnerr from eos.utils.spoolSupport import SpoolType, SpoolOptions from eos.utils.stats import DmgTypes -from .base import FitGraph, XDef, YDef, Input, VectorDef +from gui.builtinGraphs.base import FitGraph, XDef, YDef, Input, VectorDef +from .calc import getTurretMult, getLauncherMult, getDroneMult, getFighterAbilityMult class FitDamageStatsGraph(FitGraph): @@ -96,7 +96,7 @@ class FitDamageStatsGraph(FitGraph): totalDps += modDps * getLauncherMult( mod=mod, fit=fit, - distance=miscInputMap['distance'], + distance=distance, tgtSpeed=miscInputMap['tgtSpeed'], tgtSigRadius=tgtSigRad) xs.append(distance) @@ -459,186 +459,4 @@ class FitDamageStatsGraph(FitGraph): return xs, ys -def getTurretMult(mod, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): - cth = _calcTurretChanceToHit( - atkSpeed=atkSpeed, - atkAngle=atkAngle, - atkRadius=fit.ship.getModifiedItemAttr('radius'), - atkOptimalRange=mod.maxRange, - atkFalloffRange=mod.falloff, - atkTracking=mod.getModifiedItemAttr('trackingSpeed'), - atkOptimalSigRadius=mod.getModifiedItemAttr('optimalSigRadius'), - distance=distance, - tgtSpeed=tgtSpeed, - tgtAngle=tgtAngle, - tgtRadius=tgt.ship.getModifiedItemAttr('radius'), - tgtSigRadius=tgtSigRadius) - mult = _calcTurretMult(cth) - return mult - - -def getLauncherMult(mod, fit, distance, tgtSpeed, tgtSigRadius): - modRange = mod.maxRange - if modRange is None: - return 0 - mult = _calcMissileMult( - atkRadius=fit.ship.getModifiedItemAttr('radius'), - atkRange=modRange, - atkEr=mod.getModifiedChargeAttr('aoeCloudSize'), - atkEv=mod.getModifiedChargeAttr('aoeVelocity'), - atkDrf=mod.getModifiedChargeAttr('aoeDamageReductionFactor'), - distance=distance, - tgtSpeed=tgtSpeed, - tgtSigRadius=tgtSigRadius) - return mult - - -def getDroneMult(drone, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): - if distance > fit.extraAttributes['droneControlRange']: - return 0 - droneSpeed = drone.getModifiedItemAttr('maxVelocity') - # Hard to simulate drone behavior, so assume chance to hit is 1 - # when drone is not sentry and is faster than its target - if droneSpeed > 1 and droneSpeed >= tgtSpeed: - cth = 1 - # Otherwise put the drone into center of the ship, move it at its max speed or ship's speed - # (whichever is lower) towards direction of attacking ship and see how well it projects - else: - droneRadius = drone.getModifiedItemAttr('radius') - cth = _calcTurretChanceToHit( - atkSpeed=min(atkSpeed, droneSpeed), - atkAngle=atkAngle, - atkRadius=droneRadius, - atkOptimalRange=drone.maxRange, - atkFalloffRange=drone.falloff, - atkTracking=drone.getModifiedItemAttr('trackingSpeed'), - atkOptimalSigRadius=drone.getModifiedItemAttr('optimalSigRadius'), - # As distance is ship surface to ship surface, we adjust it according - # to attacker fit's radiuses to have drone surface to ship surface distance - distance=distance + fit.ship.getModifiedItemAttr('radius') - droneRadius, - tgtSpeed=tgtSpeed, - tgtAngle=tgtAngle, - tgtRadius=tgt.ship.getModifiedItemAttr('radius'), - tgtSigRadius=tgtSigRadius) - mult = _calcTurretMult(cth) - return mult - - -def getFighterAbilityMult(fighter, ability, fit, distance, tgtSpeed, tgtSigRadius): - fighterSpeed = fighter.getModifiedItemAttr('maxVelocity') - attrPrefix = ability.attrPrefix - if fighterSpeed >= tgtSpeed: - rangeFactor = 1 - # Same as with drones, if fighters are slower - put them to center of - # the ship and see how they apply - else: - rangeFactor = _calcRangeFactor( - atkOptimalRange=fighter.getModifiedItemAttr('{}RangeOptimal'.format(attrPrefix)), - atkFalloffRange=fighter.getModifiedItemAttr('{}RangeFalloff'.format(attrPrefix)), - distance=distance + fit.ship.getModifiedItemAttr('radius') - fighter.getModifiedItemAttr('radius')) - drf = fighter.getModifiedItemAttr('{}ReductionFactor'.format(attrPrefix), None) - if drf is None: - drf = fighter.getModifiedItemAttr('{}DamageReductionFactor'.format(attrPrefix)) - drs = fighter.getModifiedItemAttr('{}ReductionSensitivity'.format(attrPrefix), None) - if drs is None: - drs = fighter.getModifiedItemAttr('{}DamageReductionSensitivity'.format(attrPrefix)) - missileFactor = _calcMissileFactor( - atkEr=fighter.getModifiedItemAttr('{}ExplosionRadius'.format(attrPrefix)), - atkEv=fighter.getModifiedItemAttr('{}ExplosionVelocity'.format(attrPrefix)), - atkDrf=_calcAggregatedDrf(reductionFactor=drf, reductionSensitivity=drs), - tgtSpeed=tgtSpeed, - tgtSigRadius=tgtSigRadius) - mult = rangeFactor * missileFactor - return mult - - -# Turret-specific -def _calcTurretMult(chanceToHit): - """Calculate damage multiplier for turret-based weapons.""" - # https://wiki.eveuniversity.org/Turret_mechanics#Damage - wreckingChance = min(chanceToHit, 0.01) - wreckingPart = wreckingChance * 3 - normalChance = chanceToHit - wreckingChance - if normalChance > 0: - avgDamageMult = (0.01 + chanceToHit) / 2 + 0.49 - normalPart = normalChance * avgDamageMult - else: - normalPart = 0 - totalMult = normalPart + wreckingPart - return totalMult - - -def _calcTurretChanceToHit( - atkSpeed, atkAngle, atkRadius, atkOptimalRange, atkFalloffRange, atkTracking, atkOptimalSigRadius, - distance, tgtSpeed, tgtAngle, tgtRadius, tgtSigRadius -): - """Calculate chance to hit for turret-based weapons.""" - # https://wiki.eveuniversity.org/Turret_mechanics#Hit_Math - angularSpeed = _calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius) - rangeFactor = _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) - trackingFactor = _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius) - cth = rangeFactor * trackingFactor - return cth - - -def _calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius): - """Calculate angular speed based on mobility parameters of two ships.""" - atkAngle = atkAngle * math.pi / 180 - tgtAngle = tgtAngle * math.pi / 180 - ctcDistance = atkRadius + distance + tgtRadius - # Target is to the right of the attacker, so transversal is projection onto Y axis - transSpeed = abs(atkSpeed * math.sin(atkAngle) - tgtSpeed * math.sin(tgtAngle)) - if ctcDistance == 0: - angularSpeed = 0 if transSpeed == 0 else math.inf - else: - angularSpeed = transSpeed / ctcDistance - return angularSpeed - - -def _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius): - """Calculate tracking chance to hit component.""" - return 0.5 ** (((angularSpeed * atkOptimalSigRadius) / (atkTracking * tgtSigRadius)) ** 2) - - -# Missile-specific -def _calcMissileMult(atkRadius, atkRange, atkEr, atkEv, atkDrf, distance, tgtSpeed, tgtSigRadius): - """Calculate damage multiplier for missile launcher.""" - # Missiles spawn in the center of the attacking ship - if distance + atkRadius > atkRange: - mult = 0 - else: - mult = _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius) - return mult - - -def _calcFighterMult(atkOptimalRange, atkFalloffRange, atkEr, atkEv, atkDrf, distance, tgtSpeed, tgtSigRadius): - """Calculate damage multiplier for separate fighter ability,""" - rangeFactor = _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) - missileFactor = _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius) - mult = rangeFactor * missileFactor - return mult - - -def _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius): - """Missile application.""" - slowPart = tgtSigRadius / atkEr - fastPart = ((atkEv * tgtSigRadius) / (atkEr * tgtSpeed)) ** atkDrf - totalMult = min(1, slowPart, fastPart) - return totalMult - - -def _calcAggregatedDrf(reductionFactor, reductionSensitivity): - """ - Sometimes DRF is specified as 2 separate numbers, - here we combine them into generic form. - """ - return math.log(reductionFactor) / math.log(reductionSensitivity) - - -# Generic -def _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance): - """Range strength/chance factor, applicable to guns, ewar, RRs, etc.""" - return 0.5 ** ((max(0, distance - atkOptimalRange) / atkFalloffRange) ** 2) - - FitDamageStatsGraph.register()