From 3bc93899fe0f08b9c80697a16101c43525ac41e9 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 13 Aug 2019 08:28:59 +0300 Subject: [PATCH] Add web strength vs range graph --- graphs/calc.py | 62 +++++++++++++++ graphs/data/__init__.py | 1 + .../data/fitDamageStats/calc/application.py | 23 ++---- graphs/data/fitDamageStats/calc/projected.py | 26 +++---- graphs/data/fitDamageStats/graph.py | 3 +- graphs/data/fitEwarStats/__init__.py | 24 ++++++ graphs/data/fitEwarStats/getter.py | 76 +++++++++++++++++++ graphs/data/fitEwarStats/graph.py | 41 ++++++++++ graphs/wrapper.py | 31 +------- 9 files changed, 227 insertions(+), 60 deletions(-) create mode 100644 graphs/calc.py create mode 100644 graphs/data/fitEwarStats/__init__.py create mode 100644 graphs/data/fitEwarStats/getter.py create mode 100644 graphs/data/fitEwarStats/graph.py diff --git a/graphs/calc.py b/graphs/calc.py new file mode 100644 index 000000000..d476699fb --- /dev/null +++ b/graphs/calc.py @@ -0,0 +1,62 @@ +# ============================================================================= +# 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 calculateRangeFactor(srcOptimalRange, srcFalloffRange, distance): + """Range strength/chance factor, applicable to guns, ewar, RRs, etc.""" + if distance is None: + return 1 + if srcFalloffRange > 0: + return 0.5 ** ((max(0, distance - srcOptimalRange) / srcFalloffRange) ** 2) + elif distance <= srcOptimalRange: + return 1 + else: + return 0 + + +# Just copy-paste penalization chain calculation code (with some modifications, +# as multipliers arrive in different form) in here to not make actual attribute +# calculations slower than they already are due to extra function calls +def calculateMultiplier(multipliers): + """ + multipliers: dictionary in format: + {stacking group name: [(mult, resist attr ID), (mult, resist attr ID)]} + """ + val = 1 + for penalizedMultipliers in multipliers.values(): + # A quick explanation of how this works: + # 1: Bonuses and penalties are calculated seperately, so we'll have to filter each of them + l1 = [v[0] for v in penalizedMultipliers if v[0] > 1] + l2 = [v[0] for v in penalizedMultipliers if v[0] < 1] + # 2: The most significant bonuses take the smallest penalty, + # This means we'll have to sort + abssort = lambda _val: -abs(_val - 1) + l1.sort(key=abssort) + l2.sort(key=abssort) + # 3: The first module doesn't get penalized at all + # Any module after the first takes penalties according to: + # 1 + (multiplier - 1) * math.exp(- math.pow(i, 2) / 7.1289) + for l in (l1, l2): + for i in range(len(l)): + bonus = l[i] + val *= 1 + (bonus - 1) * math.exp(- i ** 2 / 7.1289) + return val diff --git a/graphs/data/__init__.py b/graphs/data/__init__.py index bd58e57e3..d02a48e9b 100644 --- a/graphs/data/__init__.py +++ b/graphs/data/__init__.py @@ -19,6 +19,7 @@ from . import fitDamageStats +from . import fitEwarStats from . import fitShieldRegen from . import fitCapRegen from . import fitMobility diff --git a/graphs/data/fitDamageStats/calc/application.py b/graphs/data/fitDamageStats/calc/application.py index afcf612be..a104b2dfe 100644 --- a/graphs/data/fitDamageStats/calc/application.py +++ b/graphs/data/fitDamageStats/calc/application.py @@ -23,6 +23,7 @@ from functools import lru_cache from eos.const import FittingHardpoint from eos.utils.float import floatUnerr +from graphs.calc import calculateRangeFactor from service.const import GraphDpsDroneMode from service.settings import GraphSettings @@ -256,9 +257,9 @@ def getFighterAbilityMult(fighter, ability, src, distance, tgtSpeed, tgtSigRadiu rangeFactorDistance = None else: rangeFactorDistance = distance + src.getRadius() - fighter.getModifiedItemAttr('radius') - rangeFactor = _calcRangeFactor( - atkOptimalRange=fighter.getModifiedItemAttr('{}RangeOptimal'.format(attrPrefix)) or fighter.getModifiedItemAttr('{}Range'.format(attrPrefix)), - atkFalloffRange=fighter.getModifiedItemAttr('{}RangeFalloff'.format(attrPrefix)), + rangeFactor = calculateRangeFactor( + srcOptimalRange=fighter.getModifiedItemAttr('{}RangeOptimal'.format(attrPrefix)) or fighter.getModifiedItemAttr('{}Range'.format(attrPrefix)), + srcFalloffRange=fighter.getModifiedItemAttr('{}RangeFalloff'.format(attrPrefix)), distance=rangeFactorDistance) drf = fighter.getModifiedItemAttr('{}ReductionFactor'.format(attrPrefix), None) if drf is None: @@ -301,7 +302,7 @@ def _calcTurretChanceToHit( """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) + rangeFactor = calculateRangeFactor(atkOptimalRange, atkFalloffRange, distance) trackingFactor = _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius) cth = rangeFactor * trackingFactor return cth @@ -350,19 +351,7 @@ def _calcAggregatedDrf(reductionFactor, reductionSensitivity): return math.log(reductionFactor) / math.log(reductionSensitivity) -# Generic math -def _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance): - """Range strength/chance factor, applicable to guns, ewar, RRs, etc.""" - if distance is None: - return 1 - if atkFalloffRange > 0: - return 0.5 ** ((max(0, distance - atkOptimalRange) / atkFalloffRange) ** 2) - elif distance <= atkOptimalRange: - return 1 - else: - return 0 - - +# Misc math def _calcBombFactor(atkEr, tgtSigRadius): if atkEr == 0: return 1 diff --git a/graphs/data/fitDamageStats/calc/projected.py b/graphs/data/fitDamageStats/calc/projected.py index 501498412..48b5ec5cf 100644 --- a/graphs/data/fitDamageStats/calc/projected.py +++ b/graphs/data/fitDamageStats/calc/projected.py @@ -21,9 +21,9 @@ import math from eos.utils.float import floatUnerr +from graphs.calc import calculateRangeFactor from service.const import GraphDpsDroneMode from service.settings import GraphSettings -from .application import _calcRangeFactor def getWebbedSpeed(src, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighters, distance): @@ -39,9 +39,9 @@ def getWebbedSpeed(src, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighte appliedMultipliers = {} # Modules first, they are applied always the same way for wData in webMods: - appliedBoost = wData.boost * _calcRangeFactor( - atkOptimalRange=wData.optimal, - atkFalloffRange=wData.falloff, + appliedBoost = wData.boost * calculateRangeFactor( + srcOptimalRange=wData.optimal, + srcFalloffRange=wData.falloff, distance=distance) if appliedBoost: appliedMultipliers.setdefault(wData.stackingGroup, []).append((1 + appliedBoost / 100, wData.resAttrID)) @@ -79,9 +79,9 @@ def getWebbedSpeed(src, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighte rangeFactorDistance = None else: rangeFactorDistance = distance + atkRadius - mwData.radius - appliedMwBoost = mwData.boost * _calcRangeFactor( - atkOptimalRange=mwData.optimal, - atkFalloffRange=mwData.falloff, + appliedMwBoost = mwData.boost * calculateRangeFactor( + srcOptimalRange=mwData.optimal, + srcFalloffRange=mwData.falloff, distance=rangeFactorDistance) appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + appliedMwBoost / 100, mwData.resAttrID)) mobileWebs.remove(mwData) @@ -99,9 +99,9 @@ def getTpMult(src, tgt, tgtSpeed, tpMods, tpDrones, tpFighters, distance): # Modules appliedMultipliers = {} for tpData in tpMods: - appliedBoost = tpData.boost * _calcRangeFactor( - atkOptimalRange=tpData.optimal, - atkFalloffRange=tpData.falloff, + appliedBoost = tpData.boost * calculateRangeFactor( + srcOptimalRange=tpData.optimal, + srcFalloffRange=tpData.falloff, distance=distance) if appliedBoost: appliedMultipliers.setdefault(tpData.stackingGroup, []).append((1 + appliedBoost / 100, tpData.resAttrID)) @@ -123,9 +123,9 @@ def getTpMult(src, tgt, tgtSpeed, tpMods, tpDrones, tpFighters, distance): rangeFactorDistance = None else: rangeFactorDistance = distance + atkRadius - mtpData.radius - appliedMtpBoost = mtpData.boost * _calcRangeFactor( - atkOptimalRange=mtpData.optimal, - atkFalloffRange=mtpData.falloff, + appliedMtpBoost = mtpData.boost * calculateRangeFactor( + srcOptimalRange=mtpData.optimal, + srcFalloffRange=mtpData.falloff, distance=rangeFactorDistance) appliedMultipliers.setdefault(mtpData.stackingGroup, []).append((1 + appliedMtpBoost / 100, mtpData.resAttrID)) tpedSig = tgt.getSigRadius(extraMultipliers=appliedMultipliers) diff --git a/graphs/data/fitDamageStats/graph.py b/graphs/data/fitDamageStats/graph.py index a6a26c09a..a12372808 100644 --- a/graphs/data/fitDamageStats/graph.py +++ b/graphs/data/fitDamageStats/graph.py @@ -91,8 +91,7 @@ class FitDamageStatsGraph(FitGraph): ('atkSpeed', '%'): lambda v, src, tgt: v / 100 * src.getMaxVelocity(), ('tgtSpeed', '%'): lambda v, src, tgt: v / 100 * tgt.getMaxVelocity(), ('tgtSigRad', '%'): lambda v, src, tgt: v / 100 * tgt.getSigRadius()} - _limiters = { - 'time': lambda src, tgt: (0, 2500)} + _limiters = {'time': lambda src, tgt: (0, 2500)} _getters = { ('distance', 'dps'): Distance2DpsGetter, ('distance', 'volley'): Distance2VolleyGetter, diff --git a/graphs/data/fitEwarStats/__init__.py b/graphs/data/fitEwarStats/__init__.py new file mode 100644 index 000000000..e58bfd3ba --- /dev/null +++ b/graphs/data/fitEwarStats/__init__.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 . +# ============================================================================= + + +from .graph import FitEwarStatsGraph + + +FitEwarStatsGraph.register() diff --git a/graphs/data/fitEwarStats/getter.py b/graphs/data/fitEwarStats/getter.py new file mode 100644 index 000000000..89ab118c6 --- /dev/null +++ b/graphs/data/fitEwarStats/getter.py @@ -0,0 +1,76 @@ +# ============================================================================= +# 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 + +from eos.const import FittingModuleState +from graphs.calc import calculateMultiplier, calculateRangeFactor +from graphs.data.base import SmoothPointGetter + + +class Distance2WebbingStrengthGetter(SmoothPointGetter): + + _baseResolution = 50 + _extraDepth = 2 + + def _getCommonData(self, miscParams, src, tgt): + resist = dict(miscParams)['resist'] or 0 + webs = [] + for mod in src.item.modules: + if mod.state <= FittingModuleState.ONLINE: + continue + for webEffectName in ('remoteWebifierFalloff', 'structureModuleEffectStasisWebifier'): + if webEffectName in mod.item.effects: + webs.append(( + mod.getModifiedItemAttr('speedFactor') * (1 - resist), + mod.maxRange or 0, mod.falloff or 0, 'default')) + if 'doomsdayAOEWeb' in mod.item.effects: + webs.append(( + mod.getModifiedItemAttr('speedFactor') * (1 - resist), + max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()), + mod.falloff or 0, 'default')) + for drone in src.item.drones: + if drone.amountActive <= 0: + continue + if 'remoteWebifierEntity' in drone.item.effects: + webs.extend(drone.amountActive * (( + drone.getModifiedItemAttr('speedFactor') * (1 - resist), + src.item.extraAttributes['droneControlRange'], 0, 'default'),)) + for fighter in src.item.fighters: + if not fighter.active: + continue + for ability in fighter.abilities: + if not ability.active: + continue + if ability.effect.name == 'fighterAbilityStasisWebifier': + webs.append(( + fighter.getModifiedItemAttr('fighterAbilityStasisWebifierSpeedPenalty') * fighter.amountActive * (1 - resist), + math.inf, 0, 'default')) + return {'webs': webs} + + def _calculatePoint(self, x, miscParams, src, tgt, commonData): + distance = x + strMults = {} + for strength, optimal, falloff, stackingGroup in commonData['webs']: + strength *= calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance) + strMults.setdefault(stackingGroup, []).append((1 + strength / 100, None)) + strMult = calculateMultiplier(strMults) + strength = (1 - strMult) * 100 + return strength diff --git a/graphs/data/fitEwarStats/graph.py b/graphs/data/fitEwarStats/graph.py new file mode 100644 index 000000000..5b7df53d3 --- /dev/null +++ b/graphs/data/fitEwarStats/graph.py @@ -0,0 +1,41 @@ +# ============================================================================= +# 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 graphs.data.base import FitGraph, Input, XDef, YDef +from .getter import Distance2WebbingStrengthGetter + + +class FitEwarStatsGraph(FitGraph): + + # UI stuff + internalName = 'ewarStatsGraph' + name = 'Electronic Warfare Stats' + xDefs = [XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km'))] + yDefs = [YDef(handle='webStr', unit='%', label='Webbing strength')] + inputs = [ + Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=None, defaultRange=(0, 100)), + Input(handle='resist', unit='%', label='Target resistance', iconID=1393, defaultValue=0, defaultRange=(0, 100))] + + # Calculation stuff + _normalizers = { + ('distance', 'km'): lambda v, src, tgt: None if v is None else v * 1000, + ('resist', '%'): lambda v, src, tgt: None if v is None else v / 100} + _limiters = {'resist': lambda src, tgt: (0, 1)} + _getters = {('distance', 'webStr'): Distance2WebbingStrengthGetter} diff --git a/graphs/wrapper.py b/graphs/wrapper.py index 48b7b9e6a..370e57801 100644 --- a/graphs/wrapper.py +++ b/graphs/wrapper.py @@ -18,12 +18,11 @@ # ============================================================================= -import math - from eos.saveddata.damagePattern import DamagePattern from eos.saveddata.fit import Fit from eos.saveddata.targetProfile import TargetProfile from service.const import TargetResistMode +from .calc import calculateMultiplier class BaseWrapper: @@ -64,7 +63,7 @@ class BaseWrapper: elif self.isProfile: maxVelocity = self.item.maxVelocity if extraMultipliers: - maxVelocity *= _calculateMultiplier(extraMultipliers) + maxVelocity *= calculateMultiplier(extraMultipliers) else: maxVelocity = None return maxVelocity @@ -78,7 +77,7 @@ class BaseWrapper: elif self.isProfile: sigRadius = self.item.signatureRadius if extraMultipliers: - sigRadius *= _calculateMultiplier(extraMultipliers) + sigRadius *= calculateMultiplier(extraMultipliers) else: sigRadius = None return sigRadius @@ -141,30 +140,6 @@ class TargetWrapper(BaseWrapper): return em, therm, kin, explo -# Just copy-paste penalization chain calculation code (with some modifications, -# as multipliers arrive in different form) in here to not make actual attribute -# calculations slower than they already are due to extra function calls -def _calculateMultiplier(multipliers): - val = 1 - for penalizedMultipliers in multipliers.values(): - # A quick explanation of how this works: - # 1: Bonuses and penalties are calculated seperately, so we'll have to filter each of them - l1 = [v[0] for v in penalizedMultipliers if v[0] > 1] - l2 = [v[0] for v in penalizedMultipliers if v[0] < 1] - # 2: The most significant bonuses take the smallest penalty, - # This means we'll have to sort - abssort = lambda _val: -abs(_val - 1) - l1.sort(key=abssort) - l2.sort(key=abssort) - # 3: The first module doesn't get penalized at all - # Any module after the first takes penalties according to: - # 1 + (multiplier - 1) * math.exp(- math.pow(i, 2) / 7.1289) - for l in (l1, l2): - for i in range(len(l)): - bonus = l[i] - val *= 1 + (bonus - 1) * math.exp(- i ** 2 / 7.1289) - return val - def _getShieldResists(ship): em = 1 - ship.getModifiedItemAttr('shieldEmDamageResonance')