Reorganize graph folder structure
This commit is contained in:
25
graphs/data/__init__.py
Normal file
25
graphs/data/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from . import fitDamageStats
|
||||
from . import fitShieldRegen
|
||||
from . import fitCapRegen
|
||||
from . import fitMobility
|
||||
from . import fitWarpTime
|
||||
23
graphs/data/base/__init__.py
Normal file
23
graphs/data/base/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
from .cache import FitDataCache
|
||||
from .defs import XDef, YDef, VectorDef, Input
|
||||
from .getter import PointGetter, SmoothPointGetter
|
||||
from .graph import FitGraph
|
||||
31
graphs/data/base/cache.py
Normal file
31
graphs/data/base/cache.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class FitDataCache:
|
||||
|
||||
def __init__(self):
|
||||
self._data = {}
|
||||
|
||||
def clearForFit(self, fitID):
|
||||
if fitID in self._data:
|
||||
del self._data[fitID]
|
||||
|
||||
def clearAll(self):
|
||||
self._data.clear()
|
||||
40
graphs/data/base/defs.py
Normal file
40
graphs/data/base/defs.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
YDef = namedtuple('YDef', ('handle', 'unit', 'label'))
|
||||
XDef = namedtuple('XDef', ('handle', 'unit', 'label', 'mainInput'))
|
||||
VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit', 'label'))
|
||||
|
||||
|
||||
class Input:
|
||||
|
||||
def __init__(self, handle, unit, label, iconID, defaultValue, defaultRange, mainOnly=False, mainTooltip=None, secondaryTooltip=None):
|
||||
self.handle = handle
|
||||
self.unit = unit
|
||||
self.label = label
|
||||
self.iconID = iconID
|
||||
self.defaultValue = defaultValue
|
||||
self.defaultRange = defaultRange
|
||||
self.mainOnly = mainOnly
|
||||
self.mainTooltip = mainTooltip
|
||||
self.secondaryTooltip = secondaryTooltip
|
||||
97
graphs/data/base/getter.py
Normal file
97
graphs/data/base/getter.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
||||
class PointGetter(metaclass=ABCMeta):
|
||||
|
||||
def __init__(self, graph):
|
||||
self.graph = graph
|
||||
|
||||
@abstractmethod
|
||||
def getRange(self, xRange, miscParams, fit, tgt):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def getPoint(self, x, miscParams, fit, tgt):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SmoothPointGetter(PointGetter, metaclass=ABCMeta):
|
||||
|
||||
def __init__(self, graph, baseResolution=50, extraDepth=2):
|
||||
super().__init__(graph)
|
||||
self._baseResolution = baseResolution
|
||||
self._extraDepth = extraDepth
|
||||
|
||||
def getRange(self, xRange, miscParams, fit, tgt):
|
||||
xs = []
|
||||
ys = []
|
||||
commonData = self._getCommonData(miscParams=miscParams, fit=fit, tgt=tgt)
|
||||
|
||||
def addExtraPoints(x1, y1, x2, y2, depth):
|
||||
if depth <= 0 or y1 == y2:
|
||||
return
|
||||
newX = (x1 + x2) / 2
|
||||
newY = self._calculatePoint(x=newX, miscParams=miscParams, fit=fit, tgt=tgt, commonData=commonData)
|
||||
addExtraPoints(x1=prevX, y1=prevY, x2=newX, y2=newY, depth=depth - 1)
|
||||
xs.append(newX)
|
||||
ys.append(newY)
|
||||
addExtraPoints(x1=newX, y1=newY, x2=x2, y2=y2, depth=depth - 1)
|
||||
|
||||
prevX = None
|
||||
prevY = None
|
||||
# Go through X points defined by our resolution setting
|
||||
for x in self._xIterLinear(xRange):
|
||||
y = self._calculatePoint(x=x, miscParams=miscParams, fit=fit, tgt=tgt, commonData=commonData)
|
||||
if prevX is not None and prevY is not None:
|
||||
# And if Y values of adjacent data points are not equal, add extra points
|
||||
# depending on extra depth setting
|
||||
addExtraPoints(x1=prevX, y1=prevY, x2=x, y2=y, depth=self._extraDepth)
|
||||
prevX = x
|
||||
prevY = y
|
||||
xs.append(x)
|
||||
ys.append(y)
|
||||
return xs, ys
|
||||
|
||||
def getPoint(self, x, miscParams, fit, tgt):
|
||||
commonData = self._getCommonData(miscParams=miscParams, fit=fit, tgt=tgt)
|
||||
return self._calculatePoint(x=x, miscParams=miscParams, fit=fit, tgt=tgt, commonData=commonData)
|
||||
|
||||
def _xIterLinear(self, xRange):
|
||||
xLow = min(xRange)
|
||||
xHigh = max(xRange)
|
||||
# Resolution defines amount of ranges between points here,
|
||||
# not amount of points
|
||||
step = (xHigh - xLow) / self._baseResolution
|
||||
if step == 0 or math.isnan(step):
|
||||
yield xLow
|
||||
else:
|
||||
for i in range(self._baseResolution + 1):
|
||||
yield xLow + step * i
|
||||
|
||||
def _getCommonData(self, miscParams, fit, tgt):
|
||||
return {}
|
||||
|
||||
@abstractmethod
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
raise NotImplementedError
|
||||
230
graphs/data/base/graph.py
Normal file
230
graphs/data/base/graph.py
Normal file
@@ -0,0 +1,230 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections import OrderedDict
|
||||
|
||||
from eos.saveddata.fit import Fit
|
||||
from eos.saveddata.targetProfile import TargetProfile
|
||||
from eos.utils.float import floatUnerr
|
||||
from service.const import GraphCacheCleanupReason
|
||||
|
||||
|
||||
class FitGraph(metaclass=ABCMeta):
|
||||
|
||||
# UI stuff
|
||||
views = []
|
||||
viewMap = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls):
|
||||
FitGraph.views.append(cls)
|
||||
FitGraph.viewMap[cls.internalName] = cls
|
||||
|
||||
def __init__(self):
|
||||
# Format: {(fit ID, target type, target ID): data}
|
||||
self._plotCache = {}
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def internalName(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def yDefs(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def yDefMap(self):
|
||||
return OrderedDict(((y.handle, y.unit), y) for y in self.yDefs)
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def xDefs(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def xDefMap(self):
|
||||
return OrderedDict(((x.handle, x.unit), x) for x in self.xDefs)
|
||||
|
||||
@property
|
||||
def inputs(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def inputMap(self):
|
||||
return OrderedDict(((i.handle, i.unit), i) for i in self.inputs)
|
||||
|
||||
@property
|
||||
def srcExtraCols(self):
|
||||
return ()
|
||||
|
||||
@property
|
||||
def tgtExtraCols(self):
|
||||
return ()
|
||||
|
||||
srcVectorDef = None
|
||||
tgtVectorDef = None
|
||||
hasTargets = False
|
||||
|
||||
def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt=None):
|
||||
if isinstance(tgt, Fit):
|
||||
tgtType = 'fit'
|
||||
elif isinstance(tgt, TargetProfile):
|
||||
tgtType = 'profile'
|
||||
else:
|
||||
tgtType = None
|
||||
cacheKey = (fit.ID, tgtType, getattr(tgt, 'ID', None))
|
||||
try:
|
||||
plotData = self._plotCache[cacheKey][(ySpec, xSpec)]
|
||||
except KeyError:
|
||||
plotData = self._calcPlotPoints(mainInput, miscInputs, xSpec, ySpec, fit, tgt)
|
||||
self._plotCache.setdefault(cacheKey, {})[(ySpec, xSpec)] = plotData
|
||||
return plotData
|
||||
|
||||
def clearCache(self, reason, extraData=None):
|
||||
plotKeysToClear = set()
|
||||
# If fit changed - clear plots which concern this fit
|
||||
if reason in (GraphCacheCleanupReason.fitChanged, GraphCacheCleanupReason.fitRemoved):
|
||||
for cacheKey in self._plotCache:
|
||||
cacheFitID, cacheTgtType, cacheTgtID = cacheKey
|
||||
if extraData == cacheFitID:
|
||||
plotKeysToClear.add(cacheKey)
|
||||
elif cacheTgtType == 'fit' and extraData == cacheTgtID:
|
||||
plotKeysToClear.add(cacheKey)
|
||||
# Same for profile
|
||||
elif reason in (GraphCacheCleanupReason.profileChanged, GraphCacheCleanupReason.profileRemoved):
|
||||
for cacheKey in self._plotCache:
|
||||
cacheFitID, cacheTgtType, cacheTgtID = cacheKey
|
||||
if cacheTgtType == 'profile' and extraData == cacheTgtID:
|
||||
plotKeysToClear.add(cacheKey)
|
||||
# Wipe out whole plot cache otherwise
|
||||
else:
|
||||
for cacheKey in self._plotCache:
|
||||
plotKeysToClear.add(cacheKey)
|
||||
# Do actual cleanup
|
||||
for cacheKey in plotKeysToClear:
|
||||
del self._plotCache[cacheKey]
|
||||
# Process any internal caches graphs might have
|
||||
self._clearInternalCache(reason, extraData)
|
||||
|
||||
def _clearInternalCache(self, reason, extraData):
|
||||
return
|
||||
|
||||
# Calculation stuff
|
||||
def _calcPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt):
|
||||
mainParamRange, miscParams = self._normalizeInputs(mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt)
|
||||
mainParamRange, miscParams = self._limitParams(mainParamRange=mainParamRange, miscParams=miscParams, fit=fit, tgt=tgt)
|
||||
xs, ys = self._getPoints(xRange=mainParamRange[1], miscParams=miscParams, xSpec=xSpec, ySpec=ySpec, fit=fit, tgt=tgt)
|
||||
ys = self._denormalizeValues(ys, ySpec, fit, tgt)
|
||||
# Sometimes x denormalizer may fail (e.g. during conversion of 0 ship speed to %).
|
||||
# If both inputs and outputs are in %, do some extra processing to at least have
|
||||
# proper graph which shows that fit has the same value over whole specified
|
||||
# relative parameter range
|
||||
try:
|
||||
xs = self._denormalizeValues(xs, xSpec, fit, tgt)
|
||||
except ZeroDivisionError:
|
||||
if mainInput.unit == xSpec.unit == '%' and len(set(floatUnerr(y) for y in ys)) == 1:
|
||||
xs = [min(mainInput.value), max(mainInput.value)]
|
||||
ys = [ys[0], ys[0]]
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
# Same for NaN which means we tried to denormalize infinity values, which might be the
|
||||
# case for the ideal target profile with infinite signature radius
|
||||
if mainInput.unit == xSpec.unit == '%' and all(math.isnan(x) for x in xs):
|
||||
xs = [min(mainInput.value), max(mainInput.value)]
|
||||
ys = [ys[0], ys[0]]
|
||||
return xs, ys
|
||||
|
||||
_normalizers = {}
|
||||
|
||||
def _normalizeInputs(self, mainInput, miscInputs, fit, tgt):
|
||||
key = (mainInput.handle, mainInput.unit)
|
||||
if key in self._normalizers:
|
||||
normalizer = self._normalizers[key]
|
||||
mainParamRange = (mainInput.handle, tuple(normalizer(v, fit, tgt) for v in mainInput.value))
|
||||
else:
|
||||
mainParamRange = (mainInput.handle, mainInput.value)
|
||||
miscParams = []
|
||||
for miscInput in miscInputs:
|
||||
key = (miscInput.handle, miscInput.unit)
|
||||
if key in self._normalizers:
|
||||
normalizer = self._normalizers[key]
|
||||
miscParam = (miscInput.handle, normalizer(miscInput.value, fit, tgt))
|
||||
else:
|
||||
miscParam = (miscInput.handle, miscInput.value)
|
||||
miscParams.append(miscParam)
|
||||
return mainParamRange, miscParams
|
||||
|
||||
_limiters = {}
|
||||
|
||||
def _limitParams(self, mainParamRange, miscParams, fit, tgt):
|
||||
|
||||
def limitToRange(val, limitRange):
|
||||
if val is None:
|
||||
return None
|
||||
val = max(val, min(limitRange))
|
||||
val = min(val, max(limitRange))
|
||||
return val
|
||||
|
||||
mainHandle, mainValue = mainParamRange
|
||||
if mainHandle in self._limiters:
|
||||
limiter = self._limiters[mainHandle]
|
||||
newMainParamRange = (mainHandle, tuple(limitToRange(v, limiter(fit, tgt)) for v in mainValue))
|
||||
else:
|
||||
newMainParamRange = mainParamRange
|
||||
newMiscParams = []
|
||||
for miscParam in miscParams:
|
||||
miscHandle, miscValue = miscParam
|
||||
if miscHandle in self._limiters:
|
||||
limiter = self._limiters[miscHandle]
|
||||
newMiscParam = (miscHandle, limitToRange(miscValue, limiter(fit, tgt)))
|
||||
newMiscParams.append(newMiscParam)
|
||||
else:
|
||||
newMiscParams.append(miscParam)
|
||||
return newMainParamRange, newMiscParams
|
||||
|
||||
_getters = {}
|
||||
|
||||
def _getPoints(self, xRange, miscParams, xSpec, ySpec, fit, tgt):
|
||||
try:
|
||||
getterClass = self._getters[(xSpec.handle, ySpec.handle)]
|
||||
except KeyError:
|
||||
return [], []
|
||||
else:
|
||||
getter = getterClass(graph=self)
|
||||
return getter.getRange(xRange=xRange, miscParams=miscParams, fit=fit, tgt=tgt)
|
||||
|
||||
_denormalizers = {}
|
||||
|
||||
def _denormalizeValues(self, values, axisSpec, fit, tgt):
|
||||
key = (axisSpec.handle, axisSpec.unit)
|
||||
if key in self._denormalizers:
|
||||
denormalizer = self._denormalizers[key]
|
||||
values = [denormalizer(v, fit, tgt) for v in values]
|
||||
return values
|
||||
24
graphs/data/fitCapRegen/__init__.py
Normal file
24
graphs/data/fitCapRegen/__init__.py
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from .graph import FitCapRegenGraph
|
||||
|
||||
|
||||
FitCapRegenGraph.register()
|
||||
93
graphs/data/fitCapRegen/getter.py
Normal file
93
graphs/data/fitCapRegen/getter.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
|
||||
from graphs.data.base import SmoothPointGetter
|
||||
|
||||
|
||||
class Time2CapAmountGetter(SmoothPointGetter):
|
||||
|
||||
def _getCommonData(self, miscParams, fit, tgt):
|
||||
return {
|
||||
'maxCapAmount': fit.ship.getModifiedItemAttr('capacitorCapacity'),
|
||||
'capRegenTime': fit.ship.getModifiedItemAttr('rechargeRate') / 1000}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
time = x
|
||||
capAmount = calculateCapAmount(
|
||||
maxCapAmount=commonData['maxCapAmount'],
|
||||
capRegenTime=commonData['capRegenTime'],
|
||||
time=time)
|
||||
return capAmount
|
||||
|
||||
|
||||
class Time2CapRegenGetter(SmoothPointGetter):
|
||||
|
||||
def _getCommonData(self, miscParams, fit, tgt):
|
||||
return {
|
||||
'maxCapAmount': fit.ship.getModifiedItemAttr('capacitorCapacity'),
|
||||
'capRegenTime': fit.ship.getModifiedItemAttr('rechargeRate') / 1000}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
time = x
|
||||
capAmount = calculateCapAmount(
|
||||
maxCapAmount=commonData['maxCapAmount'],
|
||||
capRegenTime=commonData['capRegenTime'],
|
||||
time=time)
|
||||
capRegen = calculateCapRegen(
|
||||
maxCapAmount=commonData['maxCapAmount'],
|
||||
capRegenTime=commonData['capRegenTime'],
|
||||
currentCapAmount=capAmount)
|
||||
return capRegen
|
||||
|
||||
|
||||
# Useless, but valid combination of x and y
|
||||
class CapAmount2CapAmountGetter(SmoothPointGetter):
|
||||
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
capAmount = x
|
||||
return capAmount
|
||||
|
||||
|
||||
class CapAmount2CapRegenGetter(SmoothPointGetter):
|
||||
|
||||
def _getCommonData(self, miscParams, fit, tgt):
|
||||
return {
|
||||
'maxCapAmount': fit.ship.getModifiedItemAttr('capacitorCapacity'),
|
||||
'capRegenTime': fit.ship.getModifiedItemAttr('rechargeRate') / 1000}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
capAmount = x
|
||||
capRegen = calculateCapRegen(
|
||||
maxCapAmount=commonData['maxCapAmount'],
|
||||
capRegenTime=commonData['capRegenTime'],
|
||||
currentCapAmount=capAmount)
|
||||
return capRegen
|
||||
|
||||
|
||||
def calculateCapAmount(maxCapAmount, capRegenTime, time):
|
||||
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
|
||||
return maxCapAmount * (1 + math.exp(5 * -time / capRegenTime) * -1) ** 2
|
||||
|
||||
|
||||
def calculateCapRegen(maxCapAmount, capRegenTime, currentCapAmount):
|
||||
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
|
||||
return 10 * maxCapAmount / capRegenTime * (math.sqrt(currentCapAmount / maxCapAmount) - currentCapAmount / maxCapAmount)
|
||||
53
graphs/data/fitCapRegen/graph.py
Normal file
53
graphs/data/fitCapRegen/graph.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from graphs.data.base import FitGraph, Input, XDef, YDef
|
||||
from .getter import CapAmount2CapAmountGetter, CapAmount2CapRegenGetter, Time2CapAmountGetter, Time2CapRegenGetter
|
||||
|
||||
|
||||
class FitCapRegenGraph(FitGraph):
|
||||
|
||||
# UI stuff
|
||||
internalName = 'capRegenGraph'
|
||||
name = 'Capacitor Regeneration'
|
||||
xDefs = [
|
||||
XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')),
|
||||
XDef(handle='capAmount', unit='GJ', label='Cap amount', mainInput=('capAmount', '%')),
|
||||
XDef(handle='capAmount', unit='%', label='Cap amount', mainInput=('capAmount', '%'))]
|
||||
yDefs = [
|
||||
YDef(handle='capAmount', unit='GJ', label='Cap amount'),
|
||||
YDef(handle='capRegen', unit='GJ/s', label='Cap regen')]
|
||||
inputs = [
|
||||
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), mainOnly=True),
|
||||
Input(handle='capAmount', unit='%', label='Cap amount', iconID=1668, defaultValue=25, defaultRange=(0, 100), mainOnly=True)]
|
||||
srcExtraCols = ('CapAmount', 'CapTime')
|
||||
|
||||
# Calculation stuff
|
||||
_normalizers = {
|
||||
('capAmount', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('capacitorCapacity')}
|
||||
_limiters = {
|
||||
'capAmount': lambda fit, tgt: (0, fit.ship.getModifiedItemAttr('capacitorCapacity'))}
|
||||
_getters = {
|
||||
('time', 'capAmount'): Time2CapAmountGetter,
|
||||
('time', 'capRegen'): Time2CapRegenGetter,
|
||||
('capAmount', 'capAmount'): CapAmount2CapAmountGetter,
|
||||
('capAmount', 'capRegen'): CapAmount2CapRegenGetter}
|
||||
_denormalizers = {
|
||||
('capAmount', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('capacitorCapacity')}
|
||||
24
graphs/data/fitDamageStats/__init__.py
Normal file
24
graphs/data/fitDamageStats/__init__.py
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from .graph import FitDamageStatsGraph
|
||||
|
||||
|
||||
FitDamageStatsGraph.register()
|
||||
22
graphs/data/fitDamageStats/cache/__init__.py
vendored
Normal file
22
graphs/data/fitDamageStats/cache/__init__.py
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from .projected import ProjectedDataCache
|
||||
from .time import TimeCache
|
||||
131
graphs/data/fitDamageStats/cache/projected.py
vendored
Normal file
131
graphs/data/fitDamageStats/cache/projected.py
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from eos.const import FittingModuleState
|
||||
from eos.modifiedAttributeDict import getResistanceAttrID
|
||||
from graphs.data.base import FitDataCache
|
||||
|
||||
|
||||
ModProjData = namedtuple('ModProjData', ('boost', 'optimal', 'falloff', 'stackingGroup', 'resAttrID'))
|
||||
MobileProjData = namedtuple('MobileProjData', ('boost', 'optimal', 'falloff', 'stackingGroup', 'resAttrID', 'speed', 'radius'))
|
||||
|
||||
|
||||
class ProjectedDataCache(FitDataCache):
|
||||
|
||||
def getProjModData(self, fit):
|
||||
try:
|
||||
projectedData = self._data[fit.ID]['modules']
|
||||
except KeyError:
|
||||
# Format of items for both: (boost strength, optimal, falloff, stacking group, resistance attr ID)
|
||||
webMods = []
|
||||
tpMods = []
|
||||
projectedData = self._data.setdefault(fit.ID, {})['modules'] = (webMods, tpMods)
|
||||
for mod in fit.modules:
|
||||
if mod.state <= FittingModuleState.ONLINE:
|
||||
continue
|
||||
for webEffectName in ('remoteWebifierFalloff', 'structureModuleEffectStasisWebifier'):
|
||||
if webEffectName in mod.item.effects:
|
||||
webMods.append(ModProjData(
|
||||
mod.getModifiedItemAttr('speedFactor'),
|
||||
mod.maxRange or 0,
|
||||
mod.falloff or 0,
|
||||
'default',
|
||||
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects[webEffectName])))
|
||||
if 'doomsdayAOEWeb' in mod.item.effects:
|
||||
webMods.append(ModProjData(
|
||||
mod.getModifiedItemAttr('speedFactor'),
|
||||
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - fit.ship.getModifiedItemAttr('radius')),
|
||||
mod.falloff or 0,
|
||||
'default',
|
||||
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects['doomsdayAOEWeb'])))
|
||||
for tpEffectName in ('remoteTargetPaintFalloff', 'structureModuleEffectTargetPainter'):
|
||||
if tpEffectName in mod.item.effects:
|
||||
tpMods.append(ModProjData(
|
||||
mod.getModifiedItemAttr('signatureRadiusBonus'),
|
||||
mod.maxRange or 0,
|
||||
mod.falloff or 0,
|
||||
'default',
|
||||
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects[tpEffectName])))
|
||||
if 'doomsdayAOEPaint' in mod.item.effects:
|
||||
tpMods.append(ModProjData(
|
||||
mod.getModifiedItemAttr('signatureRadiusBonus'),
|
||||
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - fit.ship.getModifiedItemAttr('radius')),
|
||||
mod.falloff or 0,
|
||||
'default',
|
||||
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects['doomsdayAOEPaint'])))
|
||||
return projectedData
|
||||
|
||||
def getProjDroneData(self, fit):
|
||||
try:
|
||||
projectedData = self._data[fit.ID]['drones']
|
||||
except KeyError:
|
||||
# Format of items for both: (boost strength, optimal, falloff, stacking group, resistance attr ID, drone speed, drone radius)
|
||||
webDrones = []
|
||||
tpDrones = []
|
||||
projectedData = self._data.setdefault(fit.ID, {})['drones'] = (webDrones, tpDrones)
|
||||
for drone in fit.drones:
|
||||
if drone.amountActive <= 0:
|
||||
continue
|
||||
if 'remoteWebifierEntity' in drone.item.effects:
|
||||
webDrones.extend(drone.amountActive * (MobileProjData(
|
||||
drone.getModifiedItemAttr('speedFactor'),
|
||||
drone.maxRange or 0,
|
||||
drone.falloff or 0,
|
||||
'default',
|
||||
getResistanceAttrID(modifyingItem=drone, effect=drone.item.effects['remoteWebifierEntity']),
|
||||
drone.getModifiedItemAttr('maxVelocity'),
|
||||
drone.getModifiedItemAttr('radius')),))
|
||||
if 'remoteTargetPaintEntity' in drone.item.effects:
|
||||
tpDrones.extend(drone.amountActive * (MobileProjData(
|
||||
drone.getModifiedItemAttr('signatureRadiusBonus'),
|
||||
drone.maxRange or 0,
|
||||
drone.falloff or 0,
|
||||
'default',
|
||||
getResistanceAttrID(modifyingItem=drone, effect=drone.item.effects['remoteTargetPaintEntity']),
|
||||
drone.getModifiedItemAttr('maxVelocity'),
|
||||
drone.getModifiedItemAttr('radius')),))
|
||||
return projectedData
|
||||
|
||||
def getProjFighterData(self, fit):
|
||||
try:
|
||||
projectedData = self._data[fit.ID]['fighters']
|
||||
except KeyError:
|
||||
# Format of items for both: (boost strength, optimal, falloff, stacking group, resistance attr ID, fighter speed, fighter radius)
|
||||
webFighters = []
|
||||
tpFighters = []
|
||||
projectedData = self._data.setdefault(fit.ID, {})['fighters'] = (webFighters, tpFighters)
|
||||
for fighter in fit.fighters:
|
||||
if not fighter.active:
|
||||
continue
|
||||
for ability in fighter.abilities:
|
||||
if not ability.active:
|
||||
continue
|
||||
if ability.effect.name == 'fighterAbilityStasisWebifier':
|
||||
webFighters.append(MobileProjData(
|
||||
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierSpeedPenalty') * fighter.amountActive,
|
||||
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierOptimalRange'),
|
||||
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierFalloffRange'),
|
||||
'default',
|
||||
getResistanceAttrID(modifyingItem=fighter, effect=fighter.item.effects['fighterAbilityStasisWebifier']),
|
||||
fighter.getModifiedItemAttr('maxVelocity'),
|
||||
fighter.getModifiedItemAttr('radius')))
|
||||
return projectedData
|
||||
254
graphs/data/fitDamageStats/cache/time.py
vendored
Normal file
254
graphs/data/fitDamageStats/cache/time.py
vendored
Normal file
@@ -0,0 +1,254 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from copy import copy
|
||||
|
||||
from eos.utils.float import floatUnerr
|
||||
from eos.utils.spoolSupport import SpoolOptions, SpoolType
|
||||
from eos.utils.stats import DmgTypes
|
||||
from graphs.data.base import FitDataCache
|
||||
|
||||
|
||||
class TimeCache(FitDataCache):
|
||||
|
||||
# Whole data getters
|
||||
def getDpsData(self, fit):
|
||||
"""Return DPS data in {time: {key: dps}} format."""
|
||||
return self._data[fit.ID]['finalDps']
|
||||
|
||||
def getVolleyData(self, fit):
|
||||
"""Return volley data in {time: {key: volley}} format."""
|
||||
return self._data[fit.ID]['finalVolley']
|
||||
|
||||
def getDmgData(self, fit):
|
||||
"""Return inflicted damage data in {time: {key: damage}} format."""
|
||||
return self._data[fit.ID]['finalDmg']
|
||||
|
||||
# Specific data point getters
|
||||
def getDpsDataPoint(self, fit, time):
|
||||
"""Get DPS data by specified time in {key: dps} format."""
|
||||
return self._getDataPoint(fit, time, self.getDpsData)
|
||||
|
||||
def getVolleyDataPoint(self, fit, time):
|
||||
"""Get volley data by specified time in {key: volley} format."""
|
||||
return self._getDataPoint(fit, time, self.getVolleyData)
|
||||
|
||||
def getDmgDataPoint(self, fit, time):
|
||||
"""Get inflicted damage data by specified time in {key: dmg} format."""
|
||||
return self._getDataPoint(fit, time, self.getDmgData)
|
||||
|
||||
# Preparation functions
|
||||
def prepareDpsData(self, fit, maxTime):
|
||||
self._prepareDpsVolleyData(fit, maxTime)
|
||||
|
||||
def prepareVolleyData(self, fit, maxTime):
|
||||
self._prepareDpsVolleyData(fit, maxTime)
|
||||
|
||||
def prepareDmgData(self, fit, maxTime):
|
||||
# Time is none means that time parameter has to be ignored,
|
||||
# we do not need cache for that
|
||||
if maxTime is None:
|
||||
return
|
||||
self._generateInternalForm(fit, maxTime)
|
||||
fitCache = self._data[fit.ID]
|
||||
# Final cache has been generated already, don't do anything
|
||||
if 'finalDmg' in fitCache:
|
||||
return
|
||||
intCache = fitCache['internalDmg']
|
||||
changesByTime = {}
|
||||
for key, dmgMap in intCache.items():
|
||||
for time in dmgMap:
|
||||
changesByTime.setdefault(time, []).append(key)
|
||||
# Here we convert cache to following format:
|
||||
# {time: {key: damage done by key at this time}}
|
||||
finalCache = fitCache['finalDmg'] = {}
|
||||
timeDmgData = {}
|
||||
for time in sorted(changesByTime):
|
||||
timeDmgData = copy(timeDmgData)
|
||||
for key in changesByTime[time]:
|
||||
keyDmg = intCache[key][time]
|
||||
if key in timeDmgData:
|
||||
timeDmgData[key] = timeDmgData[key] + keyDmg
|
||||
else:
|
||||
timeDmgData[key] = keyDmg
|
||||
finalCache[time] = timeDmgData
|
||||
# We do not need internal cache once we have final
|
||||
del fitCache['internalDmg']
|
||||
|
||||
# Private stuff
|
||||
def _prepareDpsVolleyData(self, fit, maxTime):
|
||||
# Time is none means that time parameter has to be ignored,
|
||||
# we do not need cache for that
|
||||
if maxTime is None:
|
||||
return True
|
||||
self._generateInternalForm(fit, maxTime)
|
||||
fitCache = self._data[fit.ID]
|
||||
# Final cache has been generated already, don't do anything
|
||||
if 'finalDps' in fitCache and 'finalVolley' in fitCache:
|
||||
return
|
||||
# Convert cache from segments with assigned values into points
|
||||
# which are located at times when dps/volley values change
|
||||
pointCache = {}
|
||||
for key, dmgList in fitCache['internalDpsVolley'].items():
|
||||
pointData = pointCache[key] = {}
|
||||
prevDps = None
|
||||
prevVolley = None
|
||||
prevTimeEnd = None
|
||||
for timeStart, timeEnd, dps, volley in dmgList:
|
||||
# First item
|
||||
if not pointData:
|
||||
pointData[timeStart] = (dps, volley)
|
||||
# Gap between items
|
||||
elif floatUnerr(prevTimeEnd) < floatUnerr(timeStart):
|
||||
pointData[prevTimeEnd] = (DmgTypes(0, 0, 0, 0), DmgTypes(0, 0, 0, 0))
|
||||
pointData[timeStart] = (dps, volley)
|
||||
# Changed value
|
||||
elif dps != prevDps or volley != prevVolley:
|
||||
pointData[timeStart] = (dps, volley)
|
||||
prevDps = dps
|
||||
prevVolley = volley
|
||||
prevTimeEnd = timeEnd
|
||||
# We have data in another form, do not need old one any longer
|
||||
del fitCache['internalDpsVolley']
|
||||
changesByTime = {}
|
||||
for key, dmgMap in pointCache.items():
|
||||
for time in dmgMap:
|
||||
changesByTime.setdefault(time, []).append(key)
|
||||
# Here we convert cache to following format:
|
||||
# {time: {key: (dps, volley}}
|
||||
finalDpsCache = fitCache['finalDps'] = {}
|
||||
finalVolleyCache = fitCache['finalVolley'] = {}
|
||||
timeDpsData = {}
|
||||
timeVolleyData = {}
|
||||
for time in sorted(changesByTime):
|
||||
timeDpsData = copy(timeDpsData)
|
||||
timeVolleyData = copy(timeVolleyData)
|
||||
for key in changesByTime[time]:
|
||||
dps, volley = pointCache[key][time]
|
||||
timeDpsData[key] = dps
|
||||
timeVolleyData[key] = volley
|
||||
finalDpsCache[time] = timeDpsData
|
||||
finalVolleyCache[time] = timeVolleyData
|
||||
|
||||
def _generateInternalForm(self, fit, maxTime):
|
||||
if self._isTimeCacheValid(fit, maxTime):
|
||||
return
|
||||
fitCache = self._data[fit.ID] = {'maxTime': maxTime}
|
||||
intCacheDpsVolley = fitCache['internalDpsVolley'] = {}
|
||||
intCacheDmg = fitCache['internalDmg'] = {}
|
||||
|
||||
def addDpsVolley(ddKey, addedTimeStart, addedTimeFinish, addedVolleys):
|
||||
if not addedVolleys:
|
||||
return
|
||||
volleySum = sum(addedVolleys, DmgTypes(0, 0, 0, 0))
|
||||
if volleySum.total > 0:
|
||||
addedDps = volleySum / (addedTimeFinish - addedTimeStart)
|
||||
# We can take "just best" volley, no matter target resistances, because all
|
||||
# known items have the same damage type ratio throughout their cycle - and
|
||||
# applying resistances doesn't change final outcome
|
||||
bestVolley = max(addedVolleys, key=lambda v: v.total)
|
||||
ddCacheDps = intCacheDpsVolley.setdefault(ddKey, [])
|
||||
ddCacheDps.append((addedTimeStart, addedTimeFinish, addedDps, bestVolley))
|
||||
|
||||
def addDmg(ddKey, addedTime, addedDmg):
|
||||
if addedDmg.total == 0:
|
||||
return
|
||||
intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg
|
||||
|
||||
# Modules
|
||||
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 cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles():
|
||||
cycleVolleys = []
|
||||
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
|
||||
for volleyTimeMs, volley in volleyParams.items():
|
||||
cycleVolleys.append(volley)
|
||||
addDmg(mod, currentTime + volleyTimeMs / 1000, volley)
|
||||
addDpsVolley(mod, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
|
||||
if inactiveTimeMs > 0:
|
||||
nonstopCycles = 0
|
||||
else:
|
||||
nonstopCycles += 1
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
|
||||
# Drones
|
||||
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 cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles():
|
||||
cycleVolleys = []
|
||||
for volleyTimeMs, volley in volleyParams.items():
|
||||
cycleVolleys.append(volley)
|
||||
addDmg(drone, currentTime + volleyTimeMs / 1000, volley)
|
||||
addDpsVolley(drone, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
|
||||
# Fighters
|
||||
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 cycleTimeMs, inactiveTimeMs in abilityCycleParams.iterCycles():
|
||||
cycleVolleys = []
|
||||
for volleyTimeMs, volley in abilityVolleyParams.items():
|
||||
cycleVolleys.append(volley)
|
||||
addDmg((fighter, effectID), currentTime + volleyTimeMs / 1000, volley)
|
||||
addDpsVolley((fighter, effectID), currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
|
||||
|
||||
def _isTimeCacheValid(self, fit, maxTime):
|
||||
try:
|
||||
cacheMaxTime = self._data[fit.ID]['maxTime']
|
||||
except KeyError:
|
||||
return False
|
||||
return maxTime <= cacheMaxTime
|
||||
|
||||
def _getDataPoint(self, fit, time, dataFunc):
|
||||
data = dataFunc(fit)
|
||||
timesBefore = [t for t in data if floatUnerr(t) <= floatUnerr(time)]
|
||||
try:
|
||||
time = max(timesBefore)
|
||||
except ValueError:
|
||||
return {}
|
||||
else:
|
||||
return data[time]
|
||||
18
graphs/data/fitDamageStats/calc/__init__.py
Normal file
18
graphs/data/fitDamageStats/calc/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
372
graphs/data/fitDamageStats/calc/application.py
Normal file
372
graphs/data/fitDamageStats/calc/application.py
Normal file
@@ -0,0 +1,372 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
from functools import lru_cache
|
||||
|
||||
from eos.const import FittingHardpoint
|
||||
from eos.saveddata.fit import Fit
|
||||
from eos.utils.float import floatUnerr
|
||||
from graphs.data.fitDamageStats.helper import getTgtRadius
|
||||
from service.const import GraphDpsDroneMode
|
||||
from service.settings import GraphSettings
|
||||
|
||||
|
||||
def getApplicationPerKey(fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
|
||||
applicationMap = {}
|
||||
for mod in fit.modules:
|
||||
if not mod.isDealingDamage():
|
||||
continue
|
||||
if mod.hardpoint == FittingHardpoint.TURRET:
|
||||
applicationMap[mod] = getTurretMult(
|
||||
mod=mod,
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
atkSpeed=atkSpeed,
|
||||
atkAngle=atkAngle,
|
||||
distance=distance,
|
||||
tgtSpeed=tgtSpeed,
|
||||
tgtAngle=tgtAngle,
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
elif mod.hardpoint == FittingHardpoint.MISSILE:
|
||||
applicationMap[mod] = getLauncherMult(
|
||||
mod=mod,
|
||||
fit=fit,
|
||||
distance=distance,
|
||||
tgtSpeed=tgtSpeed,
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
elif mod.item.group.name in ('Smart Bomb', 'Structure Area Denial Module'):
|
||||
applicationMap[mod] = getSmartbombMult(
|
||||
mod=mod,
|
||||
distance=distance)
|
||||
elif mod.item.group.name == 'Missile Launcher Bomb':
|
||||
applicationMap[mod] = getBombMult(
|
||||
mod=mod,
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
distance=distance,
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
elif mod.item.group.name == 'Structure Guided Bomb Launcher':
|
||||
applicationMap[mod] = getGuidedBombMult(
|
||||
mod=mod,
|
||||
fit=fit,
|
||||
distance=distance,
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
elif mod.item.group.name in ('Super Weapon', 'Structure Doomsday Weapon'):
|
||||
applicationMap[mod] = getDoomsdayMult(
|
||||
mod=mod,
|
||||
tgt=tgt,
|
||||
distance=distance,
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
for drone in fit.drones:
|
||||
if not drone.isDealingDamage():
|
||||
continue
|
||||
applicationMap[drone] = getDroneMult(
|
||||
drone=drone,
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
atkSpeed=atkSpeed,
|
||||
atkAngle=atkAngle,
|
||||
distance=distance,
|
||||
tgtSpeed=tgtSpeed,
|
||||
tgtAngle=tgtAngle,
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
for fighter in fit.fighters:
|
||||
if not fighter.isDealingDamage():
|
||||
continue
|
||||
for ability in fighter.abilities:
|
||||
if not ability.dealsDamage or not ability.active:
|
||||
continue
|
||||
applicationMap[(fighter, ability.effectID)] = getFighterAbilityMult(
|
||||
fighter=fighter,
|
||||
ability=ability,
|
||||
fit=fit,
|
||||
distance=distance,
|
||||
tgtSpeed=tgtSpeed,
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
# Ensure consistent results - round off a little to avoid float errors
|
||||
for k, v in applicationMap.items():
|
||||
applicationMap[k] = floatUnerr(v)
|
||||
return applicationMap
|
||||
|
||||
|
||||
# Item application multiplier calculation
|
||||
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=getTgtRadius(tgt),
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
mult = _calcTurretMult(cth)
|
||||
return mult
|
||||
|
||||
|
||||
def getLauncherMult(mod, fit, distance, tgtSpeed, tgtSigRadius):
|
||||
modRange = mod.maxRange
|
||||
if modRange is None:
|
||||
return 0
|
||||
if distance is not None and distance + fit.ship.getModifiedItemAttr('radius') > modRange:
|
||||
return 0
|
||||
mult = _calcMissileFactor(
|
||||
atkEr=mod.getModifiedChargeAttr('aoeCloudSize'),
|
||||
atkEv=mod.getModifiedChargeAttr('aoeVelocity'),
|
||||
atkDrf=mod.getModifiedChargeAttr('aoeDamageReductionFactor'),
|
||||
tgtSpeed=tgtSpeed,
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
return mult
|
||||
|
||||
|
||||
def getSmartbombMult(mod, distance):
|
||||
modRange = mod.maxRange
|
||||
if modRange is None:
|
||||
return 0
|
||||
if distance is not None and distance > modRange:
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
def getDoomsdayMult(mod, tgt, distance, tgtSigRadius):
|
||||
modRange = mod.maxRange
|
||||
# Single-target DDs have no range limit
|
||||
if distance is not None and modRange and distance > modRange:
|
||||
return 0
|
||||
# Single-target titan DDs are vs capitals only
|
||||
if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar'}.intersection(mod.item.effects):
|
||||
# Disallow only against subcap fits, allow against cap fits and tgt profiles
|
||||
if isinstance(tgt, Fit) and not tgt.ship.item.requiresSkill('Capital Ships'):
|
||||
return 0
|
||||
damageSig = mod.getModifiedItemAttr('doomsdayDamageRadius') or mod.getModifiedItemAttr('signatureRadius')
|
||||
if not damageSig:
|
||||
return 1
|
||||
return min(1, tgtSigRadius / damageSig)
|
||||
|
||||
|
||||
def getBombMult(mod, fit, tgt, distance, tgtSigRadius):
|
||||
modRange = mod.maxRange
|
||||
if modRange is None:
|
||||
return 0
|
||||
blastRadius = mod.getModifiedChargeAttr('explosionRange')
|
||||
atkRadius = fit.ship.getModifiedItemAttr('radius')
|
||||
tgtRadius = getTgtRadius(tgt)
|
||||
# Bomb starts in the center of the ship
|
||||
# Also here we assume that it affects target as long as blast
|
||||
# touches its surface, not center - I did not check this
|
||||
if distance is not None and distance < max(0, modRange - atkRadius - tgtRadius - blastRadius):
|
||||
return 0
|
||||
if distance is not None and distance > max(0, modRange - atkRadius + tgtRadius + blastRadius):
|
||||
return 0
|
||||
return _calcBombFactor(
|
||||
atkEr=mod.getModifiedChargeAttr('aoeCloudSize'),
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
|
||||
|
||||
def getGuidedBombMult(mod, fit, distance, tgtSigRadius):
|
||||
modRange = mod.maxRange
|
||||
if modRange is None:
|
||||
return 0
|
||||
if distance is not None and distance > modRange - fit.ship.getModifiedItemAttr('radius'):
|
||||
return 0
|
||||
eR = mod.getModifiedChargeAttr('aoeCloudSize')
|
||||
if eR == 0:
|
||||
return 1
|
||||
else:
|
||||
return min(1, tgtSigRadius / eR)
|
||||
|
||||
|
||||
def getDroneMult(drone, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
|
||||
if distance is not None and distance > fit.extraAttributes['droneControlRange']:
|
||||
return 0
|
||||
droneSpeed = drone.getModifiedItemAttr('maxVelocity')
|
||||
# Hard to simulate drone behavior, so assume chance to hit is 1 for mobile drones
|
||||
# which catch up with target
|
||||
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
|
||||
if (
|
||||
droneSpeed > 1 and (
|
||||
(droneOpt == GraphDpsDroneMode.auto and droneSpeed >= tgtSpeed) or
|
||||
droneOpt == GraphDpsDroneMode.followTarget)
|
||||
):
|
||||
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')
|
||||
if distance is None:
|
||||
cthDistance = None
|
||||
else:
|
||||
# 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
|
||||
cthDistance = distance + fit.ship.getModifiedItemAttr('radius') - droneRadius
|
||||
cth = _calcTurretChanceToHit(
|
||||
atkSpeed=min(atkSpeed, droneSpeed),
|
||||
atkAngle=atkAngle,
|
||||
atkRadius=droneRadius,
|
||||
atkOptimalRange=drone.maxRange,
|
||||
atkFalloffRange=drone.falloff,
|
||||
atkTracking=drone.getModifiedItemAttr('trackingSpeed'),
|
||||
atkOptimalSigRadius=drone.getModifiedItemAttr('optimalSigRadius'),
|
||||
distance=cthDistance,
|
||||
tgtSpeed=tgtSpeed,
|
||||
tgtAngle=tgtAngle,
|
||||
tgtRadius=getTgtRadius(tgt),
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
mult = _calcTurretMult(cth)
|
||||
return mult
|
||||
|
||||
|
||||
def getFighterAbilityMult(fighter, ability, fit, distance, tgtSpeed, tgtSigRadius):
|
||||
fighterSpeed = fighter.getModifiedItemAttr('maxVelocity')
|
||||
attrPrefix = ability.attrPrefix
|
||||
# It's bomb attack
|
||||
if attrPrefix == 'fighterAbilityLaunchBomb':
|
||||
# Just assume we can land bomb anywhere
|
||||
return _calcBombFactor(
|
||||
atkEr=fighter.getModifiedChargeAttr('aoeCloudSize'),
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
|
||||
# It's regular missile-based attack
|
||||
if (droneOpt == GraphDpsDroneMode.auto and fighterSpeed >= tgtSpeed) or droneOpt == GraphDpsDroneMode.followTarget:
|
||||
rangeFactor = 1
|
||||
# Same as with drones, if fighters are slower - put them to center of
|
||||
# the ship and see how they apply
|
||||
else:
|
||||
if distance is None:
|
||||
rangeFactorDistance = None
|
||||
else:
|
||||
rangeFactorDistance = distance + fit.ship.getModifiedItemAttr('radius') - fighter.getModifiedItemAttr('radius')
|
||||
rangeFactor = _calcRangeFactor(
|
||||
atkOptimalRange=fighter.getModifiedItemAttr('{}RangeOptimal'.format(attrPrefix)) or fighter.getModifiedItemAttr('{}Range'.format(attrPrefix)),
|
||||
atkFalloffRange=fighter.getModifiedItemAttr('{}RangeFalloff'.format(attrPrefix)),
|
||||
distance=rangeFactorDistance)
|
||||
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 math
|
||||
@lru_cache(maxsize=50)
|
||||
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
|
||||
|
||||
|
||||
@lru_cache(maxsize=1000)
|
||||
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."""
|
||||
if distance is None:
|
||||
return 0
|
||||
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:
|
||||
return 0 if transSpeed == 0 else math.inf
|
||||
else:
|
||||
return transSpeed / ctcDistance
|
||||
|
||||
|
||||
def _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius):
|
||||
"""Calculate tracking chance to hit component."""
|
||||
return 0.5 ** (((angularSpeed * atkOptimalSigRadius) / (atkTracking * tgtSigRadius)) ** 2)
|
||||
|
||||
|
||||
# Missile-specific math
|
||||
@lru_cache(maxsize=200)
|
||||
def _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius):
|
||||
"""Missile application."""
|
||||
factors = [1]
|
||||
# "Slow" part
|
||||
if atkEr > 0:
|
||||
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 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
|
||||
|
||||
|
||||
def _calcBombFactor(atkEr, tgtSigRadius):
|
||||
if atkEr == 0:
|
||||
return 1
|
||||
else:
|
||||
return min(1, tgtSigRadius / atkEr)
|
||||
138
graphs/data/fitDamageStats/calc/projected.py
Normal file
138
graphs/data/fitDamageStats/calc/projected.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
|
||||
from eos.saveddata.fit import Fit
|
||||
from eos.utils.float import floatUnerr
|
||||
from graphs.data.fitDamageStats.helper import getTgtMaxVelocity, getTgtSigRadius
|
||||
from service.const import GraphDpsDroneMode
|
||||
from service.settings import GraphSettings
|
||||
from .application import _calcRangeFactor
|
||||
|
||||
|
||||
def getWebbedSpeed(fit, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighters, distance):
|
||||
# Can slow down non-immune fits and target profiles
|
||||
if isinstance(tgt, Fit) and tgt.ship.getModifiedItemAttr('disallowOffensiveModifiers'):
|
||||
return currentUnwebbedSpeed
|
||||
maxUnwebbedSpeed = getTgtMaxVelocity(tgt)
|
||||
try:
|
||||
speedRatio = currentUnwebbedSpeed / maxUnwebbedSpeed
|
||||
except ZeroDivisionError:
|
||||
currentWebbedSpeed = 0
|
||||
else:
|
||||
appliedMultipliers = {}
|
||||
# Modules first, they are applied always the same way
|
||||
for wData in webMods:
|
||||
appliedBoost = wData.boost * _calcRangeFactor(
|
||||
atkOptimalRange=wData.optimal,
|
||||
atkFalloffRange=wData.falloff,
|
||||
distance=distance)
|
||||
if appliedBoost:
|
||||
appliedMultipliers.setdefault(wData.stackingGroup, []).append((1 + appliedBoost / 100, wData.resAttrID))
|
||||
maxWebbedSpeed = getTgtMaxVelocity(tgt, extraMultipliers=appliedMultipliers)
|
||||
currentWebbedSpeed = maxWebbedSpeed * speedRatio
|
||||
# Drones and fighters
|
||||
mobileWebs = []
|
||||
mobileWebs.extend(webFighters)
|
||||
# Drones have range limit
|
||||
if distance is None or distance <= fit.extraAttributes['droneControlRange']:
|
||||
mobileWebs.extend(webDrones)
|
||||
atkRadius = fit.ship.getModifiedItemAttr('radius')
|
||||
# As mobile webs either follow the target or stick to the attacking ship,
|
||||
# if target is within mobile web optimal - it can be applied unconditionally
|
||||
longEnoughMws = [mw for mw in mobileWebs if distance is None or distance <= mw.optimal - atkRadius + mw.radius]
|
||||
if longEnoughMws:
|
||||
for mwData in longEnoughMws:
|
||||
appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + mwData.boost / 100, mwData.resAttrID))
|
||||
mobileWebs.remove(mwData)
|
||||
maxWebbedSpeed = getTgtMaxVelocity(tgt, extraMultipliers=appliedMultipliers)
|
||||
currentWebbedSpeed = maxWebbedSpeed * speedRatio
|
||||
# Apply remaining webs, from fastest to slowest
|
||||
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
|
||||
while mobileWebs:
|
||||
# Process in batches unified by speed to save up resources
|
||||
fastestMwSpeed = max(mobileWebs, key=lambda mw: mw.speed).speed
|
||||
fastestMws = [mw for mw in mobileWebs if mw.speed == fastestMwSpeed]
|
||||
for mwData in fastestMws:
|
||||
# Faster than target or set to follow it - apply full slowdown
|
||||
if (droneOpt == GraphDpsDroneMode.auto and mwData.speed >= currentWebbedSpeed) or droneOpt == GraphDpsDroneMode.followTarget:
|
||||
appliedMwBoost = mwData.boost
|
||||
# Otherwise project from the center of the ship
|
||||
else:
|
||||
if distance is None:
|
||||
rangeFactorDistance = None
|
||||
else:
|
||||
rangeFactorDistance = distance + atkRadius - mwData.radius
|
||||
appliedMwBoost = mwData.boost * _calcRangeFactor(
|
||||
atkOptimalRange=mwData.optimal,
|
||||
atkFalloffRange=mwData.falloff,
|
||||
distance=rangeFactorDistance)
|
||||
appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + appliedMwBoost / 100, mwData.resAttrID))
|
||||
mobileWebs.remove(mwData)
|
||||
maxWebbedSpeed = getTgtMaxVelocity(tgt, extraMultipliers=appliedMultipliers)
|
||||
currentWebbedSpeed = maxWebbedSpeed * speedRatio
|
||||
# Ensure consistent results - round off a little to avoid float errors
|
||||
return floatUnerr(currentWebbedSpeed)
|
||||
|
||||
|
||||
def getTpMult(fit, tgt, tgtSpeed, tpMods, tpDrones, tpFighters, distance):
|
||||
# Can blow non-immune fits and target profiles
|
||||
if isinstance(tgt, Fit) and tgt.ship.getModifiedItemAttr('disallowOffensiveModifiers'):
|
||||
return 1
|
||||
untpedSig = getTgtSigRadius(tgt)
|
||||
# Modules
|
||||
appliedMultipliers = {}
|
||||
for tpData in tpMods:
|
||||
appliedBoost = tpData.boost * _calcRangeFactor(
|
||||
atkOptimalRange=tpData.optimal,
|
||||
atkFalloffRange=tpData.falloff,
|
||||
distance=distance)
|
||||
if appliedBoost:
|
||||
appliedMultipliers.setdefault(tpData.stackingGroup, []).append((1 + appliedBoost / 100, tpData.resAttrID))
|
||||
# Drones and fighters
|
||||
mobileTps = []
|
||||
mobileTps.extend(tpFighters)
|
||||
# Drones have range limit
|
||||
if distance is None or distance <= fit.extraAttributes['droneControlRange']:
|
||||
mobileTps.extend(tpDrones)
|
||||
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
|
||||
atkRadius = fit.ship.getModifiedItemAttr('radius')
|
||||
for mtpData in mobileTps:
|
||||
# Faster than target or set to follow it - apply full TP
|
||||
if (droneOpt == GraphDpsDroneMode.auto and mtpData.speed >= tgtSpeed) or droneOpt == GraphDpsDroneMode.followTarget:
|
||||
appliedMtpBoost = mtpData.boost
|
||||
# Otherwise project from the center of the ship
|
||||
else:
|
||||
if distance is None:
|
||||
rangeFactorDistance = None
|
||||
else:
|
||||
rangeFactorDistance = distance + atkRadius - mtpData.radius
|
||||
appliedMtpBoost = mtpData.boost * _calcRangeFactor(
|
||||
atkOptimalRange=mtpData.optimal,
|
||||
atkFalloffRange=mtpData.falloff,
|
||||
distance=rangeFactorDistance)
|
||||
appliedMultipliers.setdefault(mtpData.stackingGroup, []).append((1 + appliedMtpBoost / 100, mtpData.resAttrID))
|
||||
tpedSig = getTgtSigRadius(tgt, extraMultipliers=appliedMultipliers)
|
||||
if tpedSig == math.inf and untpedSig == math.inf:
|
||||
return 1
|
||||
mult = tpedSig / untpedSig
|
||||
# Ensure consistent results - round off a little to avoid float errors
|
||||
return floatUnerr(mult)
|
||||
428
graphs/data/fitDamageStats/getter.py
Normal file
428
graphs/data/fitDamageStats/getter.py
Normal file
@@ -0,0 +1,428 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import eos.config
|
||||
from eos.utils.spoolSupport import SpoolOptions, SpoolType
|
||||
from eos.utils.stats import DmgTypes
|
||||
from graphs.data.base import PointGetter, SmoothPointGetter
|
||||
from service.settings import GraphSettings
|
||||
from .calc.application import getApplicationPerKey
|
||||
from .calc.projected import getTpMult, getWebbedSpeed
|
||||
from .helper import getTgtSigRadius
|
||||
|
||||
|
||||
def applyDamage(dmgMap, applicationMap):
|
||||
total = DmgTypes(0, 0, 0, 0)
|
||||
for key, dmg in dmgMap.items():
|
||||
total += dmg * applicationMap.get(key, 0)
|
||||
return total
|
||||
|
||||
|
||||
# Y mixins
|
||||
class YDpsMixin:
|
||||
|
||||
def _getDamagePerKey(self, fit, time):
|
||||
# Use data from time cache if time was not specified
|
||||
if time is not None:
|
||||
return self._getTimeCacheDataPoint(fit=fit, time=time)
|
||||
# Compose map ourselves using current fit settings if time is not specified
|
||||
dpsMap = {}
|
||||
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
|
||||
for mod in fit.modules:
|
||||
if not mod.isDealingDamage():
|
||||
continue
|
||||
dpsMap[mod] = mod.getDps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))
|
||||
for drone in fit.drones:
|
||||
if not drone.isDealingDamage():
|
||||
continue
|
||||
dpsMap[drone] = drone.getDps()
|
||||
for fighter in fit.fighters:
|
||||
if not fighter.isDealingDamage():
|
||||
continue
|
||||
for effectID, effectDps in fighter.getDpsPerEffect().items():
|
||||
dpsMap[(fighter, effectID)] = effectDps
|
||||
return dpsMap
|
||||
|
||||
def _prepareTimeCache(self, fit, maxTime):
|
||||
self.graph._timeCache.prepareDpsData(fit=fit, maxTime=maxTime)
|
||||
|
||||
def _getTimeCacheData(self, fit):
|
||||
return self.graph._timeCache.getDpsData(fit=fit)
|
||||
|
||||
def _getTimeCacheDataPoint(self, fit, time):
|
||||
return self.graph._timeCache.getDpsDataPoint(fit=fit, time=time)
|
||||
|
||||
|
||||
class YVolleyMixin:
|
||||
|
||||
def _getDamagePerKey(self, fit, time):
|
||||
# Use data from time cache if time was not specified
|
||||
if time is not None:
|
||||
return self._getTimeCacheDataPoint(fit=fit, time=time)
|
||||
# Compose map ourselves using current fit settings if time is not specified
|
||||
volleyMap = {}
|
||||
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
|
||||
for mod in fit.modules:
|
||||
if not mod.isDealingDamage():
|
||||
continue
|
||||
volleyMap[mod] = mod.getVolley(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))
|
||||
for drone in fit.drones:
|
||||
if not drone.isDealingDamage():
|
||||
continue
|
||||
volleyMap[drone] = drone.getVolley()
|
||||
for fighter in fit.fighters:
|
||||
if not fighter.isDealingDamage():
|
||||
continue
|
||||
for effectID, effectVolley in fighter.getVolleyPerEffect().items():
|
||||
volleyMap[(fighter, effectID)] = effectVolley
|
||||
return volleyMap
|
||||
|
||||
def _prepareTimeCache(self, fit, maxTime):
|
||||
self.graph._timeCache.prepareVolleyData(fit=fit, maxTime=maxTime)
|
||||
|
||||
def _getTimeCacheData(self, fit):
|
||||
return self.graph._timeCache.getVolleyData(fit=fit)
|
||||
|
||||
def _getTimeCacheDataPoint(self, fit, time):
|
||||
return self.graph._timeCache.getVolleyDataPoint(fit=fit, time=time)
|
||||
|
||||
|
||||
class YInflictedDamageMixin:
|
||||
|
||||
def _getDamagePerKey(self, fit, time):
|
||||
# Damage inflicted makes no sense without time specified
|
||||
if time is None:
|
||||
raise ValueError
|
||||
return self._getTimeCacheDataPoint(fit=fit, time=time)
|
||||
|
||||
def _prepareTimeCache(self, fit, maxTime):
|
||||
self.graph._timeCache.prepareDmgData(fit=fit, maxTime=maxTime)
|
||||
|
||||
def _getTimeCacheData(self, fit):
|
||||
return self.graph._timeCache.getDmgData(fit=fit)
|
||||
|
||||
def _getTimeCacheDataPoint(self, fit, time):
|
||||
return self.graph._timeCache.getDmgDataPoint(fit=fit, time=time)
|
||||
|
||||
|
||||
# X mixins
|
||||
class XDistanceMixin(SmoothPointGetter):
|
||||
|
||||
def _getCommonData(self, miscParams, fit, tgt):
|
||||
# Process params into more convenient form
|
||||
miscParamMap = dict(miscParams)
|
||||
# Prepare time cache here because we need to do it only once,
|
||||
# and this function is called once per point info fetch
|
||||
self._prepareTimeCache(fit=fit, maxTime=miscParamMap['time'])
|
||||
return {
|
||||
'applyProjected': GraphSettings.getInstance().get('applyProjected'),
|
||||
'miscParamMap': miscParamMap,
|
||||
'dmgMap': self._getDamagePerKey(fit=fit, time=miscParamMap['time'])}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
distance = x
|
||||
miscParamMap = commonData['miscParamMap']
|
||||
tgtSpeed = miscParamMap['tgtSpeed']
|
||||
tgtSigRadius = getTgtSigRadius(tgt)
|
||||
if commonData['applyProjected']:
|
||||
webMods, tpMods = self.graph._projectedCache.getProjModData(fit)
|
||||
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(fit)
|
||||
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(fit)
|
||||
tgtSpeed = getWebbedSpeed(
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
currentUnwebbedSpeed=tgtSpeed,
|
||||
webMods=webMods,
|
||||
webDrones=webDrones,
|
||||
webFighters=webFighters,
|
||||
distance=distance)
|
||||
tgtSigRadius = tgtSigRadius * getTpMult(
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
tgtSpeed=tgtSpeed,
|
||||
tpMods=tpMods,
|
||||
tpDrones=tpDrones,
|
||||
tpFighters=tpFighters,
|
||||
distance=distance)
|
||||
applicationMap = getApplicationPerKey(
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
atkSpeed=miscParamMap['atkSpeed'],
|
||||
atkAngle=miscParamMap['atkAngle'],
|
||||
distance=distance,
|
||||
tgtSpeed=tgtSpeed,
|
||||
tgtAngle=miscParamMap['tgtAngle'],
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
y = applyDamage(dmgMap=commonData['dmgMap'], applicationMap=applicationMap).total
|
||||
return y
|
||||
|
||||
|
||||
class XTimeMixin(PointGetter):
|
||||
|
||||
def _prepareApplicationMap(self, miscParams, fit, tgt):
|
||||
# Process params into more convenient form
|
||||
miscParamMap = dict(miscParams)
|
||||
tgtSpeed = miscParamMap['tgtSpeed']
|
||||
tgtSigRadius = getTgtSigRadius(tgt)
|
||||
if GraphSettings.getInstance().get('applyProjected'):
|
||||
webMods, tpMods = self.graph._projectedCache.getProjModData(fit)
|
||||
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(fit)
|
||||
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(fit)
|
||||
tgtSpeed = getWebbedSpeed(
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
currentUnwebbedSpeed=tgtSpeed,
|
||||
webMods=webMods,
|
||||
webDrones=webDrones,
|
||||
webFighters=webFighters,
|
||||
distance=miscParamMap['distance'])
|
||||
tgtSigRadius = tgtSigRadius * getTpMult(
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
tgtSpeed=tgtSpeed,
|
||||
tpMods=tpMods,
|
||||
tpDrones=tpDrones,
|
||||
tpFighters=tpFighters,
|
||||
distance=miscParamMap['distance'])
|
||||
# Get all data we need for all times into maps/caches
|
||||
applicationMap = getApplicationPerKey(
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
atkSpeed=miscParamMap['atkSpeed'],
|
||||
atkAngle=miscParamMap['atkAngle'],
|
||||
distance=miscParamMap['distance'],
|
||||
tgtSpeed=tgtSpeed,
|
||||
tgtAngle=miscParamMap['tgtAngle'],
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
return applicationMap
|
||||
|
||||
def getRange(self, xRange, miscParams, fit, tgt):
|
||||
xs = []
|
||||
ys = []
|
||||
minTime, maxTime = xRange
|
||||
# Prepare time cache and various shared data
|
||||
self._prepareTimeCache(fit=fit, maxTime=maxTime)
|
||||
timeCache = self._getTimeCacheData(fit=fit)
|
||||
applicationMap = self._prepareApplicationMap(miscParams=miscParams, fit=fit, tgt=tgt)
|
||||
# Custom iteration for time graph to show all data points
|
||||
currentDmg = None
|
||||
currentTime = None
|
||||
for currentTime in sorted(timeCache):
|
||||
prevDmg = currentDmg
|
||||
currentDmgData = timeCache[currentTime]
|
||||
currentDmg = applyDamage(dmgMap=currentDmgData, applicationMap=applicationMap).total
|
||||
if currentTime < minTime:
|
||||
continue
|
||||
# First set of data points
|
||||
if not xs:
|
||||
# Start at exactly requested time, at last known value
|
||||
initialDmg = prevDmg or 0
|
||||
xs.append(minTime)
|
||||
ys.append(initialDmg)
|
||||
# If current time is bigger then starting, extend plot to that time with old value
|
||||
if currentTime > minTime:
|
||||
xs.append(currentTime)
|
||||
ys.append(initialDmg)
|
||||
# If new value is different, extend it with new point to the new value
|
||||
if currentDmg != prevDmg:
|
||||
xs.append(currentTime)
|
||||
ys.append(currentDmg)
|
||||
continue
|
||||
# Last data point
|
||||
if currentTime >= maxTime:
|
||||
xs.append(maxTime)
|
||||
ys.append(prevDmg)
|
||||
break
|
||||
# Anything in-between
|
||||
if currentDmg != prevDmg:
|
||||
if prevDmg is not None:
|
||||
xs.append(currentTime)
|
||||
ys.append(prevDmg)
|
||||
xs.append(currentTime)
|
||||
ys.append(currentDmg)
|
||||
# Special case - there are no damage dealers
|
||||
if currentDmg is None and currentTime is None:
|
||||
xs.append(minTime)
|
||||
ys.append(0)
|
||||
# Make sure that last data point is always at max time
|
||||
if maxTime > (currentTime or 0):
|
||||
xs.append(maxTime)
|
||||
ys.append(currentDmg or 0)
|
||||
return xs, ys
|
||||
|
||||
def getPoint(self, x, miscParams, fit, tgt):
|
||||
time = x
|
||||
# Prepare time cache and various data
|
||||
self._prepareTimeCache(fit=fit, maxTime=time)
|
||||
dmgData = self._getTimeCacheDataPoint(fit=fit, time=time)
|
||||
applicationMap = self._prepareApplicationMap(miscParams=miscParams, fit=fit, tgt=tgt)
|
||||
y = applyDamage(dmgMap=dmgData, applicationMap=applicationMap).total
|
||||
return y
|
||||
|
||||
|
||||
class XTgtSpeedMixin(SmoothPointGetter):
|
||||
|
||||
def _getCommonData(self, miscParams, fit, tgt):
|
||||
# Process params into more convenient form
|
||||
miscParamMap = dict(miscParams)
|
||||
# Prepare time cache here because we need to do it only once,
|
||||
# and this function is called once per point info fetch
|
||||
self._prepareTimeCache(fit=fit, maxTime=miscParamMap['time'])
|
||||
return {
|
||||
'applyProjected': GraphSettings.getInstance().get('applyProjected'),
|
||||
'miscParamMap': miscParamMap,
|
||||
'dmgMap': self._getDamagePerKey(fit=fit, time=miscParamMap['time'])}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
tgtSpeed = x
|
||||
miscParamMap = commonData['miscParamMap']
|
||||
tgtSigRadius = getTgtSigRadius(tgt)
|
||||
if commonData['applyProjected']:
|
||||
webMods, tpMods = self.graph._projectedCache.getProjModData(fit)
|
||||
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(fit)
|
||||
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(fit)
|
||||
tgtSpeed = getWebbedSpeed(
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
currentUnwebbedSpeed=tgtSpeed,
|
||||
webMods=webMods,
|
||||
webDrones=webDrones,
|
||||
webFighters=webFighters,
|
||||
distance=miscParamMap['distance'])
|
||||
tgtSigRadius = tgtSigRadius * getTpMult(
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
tgtSpeed=tgtSpeed,
|
||||
tpMods=tpMods,
|
||||
tpDrones=tpDrones,
|
||||
tpFighters=tpFighters,
|
||||
distance=miscParamMap['distance'])
|
||||
applicationMap = getApplicationPerKey(
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
atkSpeed=miscParamMap['atkSpeed'],
|
||||
atkAngle=miscParamMap['atkAngle'],
|
||||
distance=miscParamMap['distance'],
|
||||
tgtSpeed=tgtSpeed,
|
||||
tgtAngle=miscParamMap['tgtAngle'],
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
y = applyDamage(dmgMap=commonData['dmgMap'], applicationMap=applicationMap).total
|
||||
return y
|
||||
|
||||
|
||||
class XTgtSigRadiusMixin(SmoothPointGetter):
|
||||
|
||||
def _getCommonData(self, miscParams, fit, tgt):
|
||||
# Process params into more convenient form
|
||||
miscParamMap = dict(miscParams)
|
||||
tgtSpeed = miscParamMap['tgtSpeed']
|
||||
tgtSigMult = 1
|
||||
if GraphSettings.getInstance().get('applyProjected'):
|
||||
webMods, tpMods = self.graph._projectedCache.getProjModData(fit)
|
||||
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(fit)
|
||||
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(fit)
|
||||
tgtSpeed = getWebbedSpeed(
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
currentUnwebbedSpeed=tgtSpeed,
|
||||
webMods=webMods,
|
||||
webDrones=webDrones,
|
||||
webFighters=webFighters,
|
||||
distance=miscParamMap['distance'])
|
||||
tgtSigMult = getTpMult(
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
tgtSpeed=tgtSpeed,
|
||||
tpMods=tpMods,
|
||||
tpDrones=tpDrones,
|
||||
tpFighters=tpFighters,
|
||||
distance=miscParamMap['distance'])
|
||||
# Prepare time cache here because we need to do it only once,
|
||||
# and this function is called once per point info fetch
|
||||
self._prepareTimeCache(fit=fit, maxTime=miscParamMap['time'])
|
||||
return {
|
||||
'miscParamMap': miscParamMap,
|
||||
'tgtSpeed': tgtSpeed,
|
||||
'tgtSigMult': tgtSigMult,
|
||||
'dmgMap': self._getDamagePerKey(fit=fit, time=miscParamMap['time'])}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
tgtSigRadius = x
|
||||
miscParamMap = commonData['miscParamMap']
|
||||
applicationMap = getApplicationPerKey(
|
||||
fit=fit,
|
||||
tgt=tgt,
|
||||
atkSpeed=miscParamMap['atkSpeed'],
|
||||
atkAngle=miscParamMap['atkAngle'],
|
||||
distance=miscParamMap['distance'],
|
||||
tgtSpeed=commonData['tgtSpeed'],
|
||||
tgtAngle=miscParamMap['tgtAngle'],
|
||||
tgtSigRadius=tgtSigRadius * commonData['tgtSigMult'])
|
||||
y = applyDamage(dmgMap=commonData['dmgMap'], applicationMap=applicationMap).total
|
||||
return y
|
||||
|
||||
|
||||
# Final getters
|
||||
class Distance2DpsGetter(XDistanceMixin, YDpsMixin):
|
||||
pass
|
||||
|
||||
|
||||
class Distance2VolleyGetter(XDistanceMixin, YVolleyMixin):
|
||||
pass
|
||||
|
||||
|
||||
class Distance2InflictedDamageGetter(XDistanceMixin, YInflictedDamageMixin):
|
||||
pass
|
||||
|
||||
|
||||
class Time2DpsGetter(XTimeMixin, YDpsMixin):
|
||||
pass
|
||||
|
||||
|
||||
class Time2VolleyGetter(XTimeMixin, YVolleyMixin):
|
||||
pass
|
||||
|
||||
|
||||
class Time2InflictedDamageGetter(XTimeMixin, YInflictedDamageMixin):
|
||||
pass
|
||||
|
||||
|
||||
class TgtSpeed2DpsGetter(XTgtSpeedMixin, YDpsMixin):
|
||||
pass
|
||||
|
||||
|
||||
class TgtSpeed2VolleyGetter(XTgtSpeedMixin, YVolleyMixin):
|
||||
pass
|
||||
|
||||
|
||||
class TgtSpeed2InflictedDamageGetter(XTgtSpeedMixin, YInflictedDamageMixin):
|
||||
pass
|
||||
|
||||
|
||||
class TgtSigRadius2DpsGetter(XTgtSigRadiusMixin, YDpsMixin):
|
||||
pass
|
||||
|
||||
|
||||
class TgtSigRadius2VolleyGetter(XTgtSigRadiusMixin, YVolleyMixin):
|
||||
pass
|
||||
|
||||
|
||||
class TgtSigRadius2InflictedDamageGetter(XTgtSigRadiusMixin, YInflictedDamageMixin):
|
||||
pass
|
||||
101
graphs/data/fitDamageStats/graph.py
Normal file
101
graphs/data/fitDamageStats/graph.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from graphs.data.base import FitGraph, XDef, YDef, Input, VectorDef
|
||||
from service.const import GraphCacheCleanupReason
|
||||
from .cache import ProjectedDataCache, TimeCache
|
||||
from .getter import (
|
||||
Distance2DpsGetter, Distance2VolleyGetter, Distance2InflictedDamageGetter,
|
||||
Time2DpsGetter, Time2VolleyGetter, Time2InflictedDamageGetter,
|
||||
TgtSpeed2DpsGetter, TgtSpeed2VolleyGetter, TgtSpeed2InflictedDamageGetter,
|
||||
TgtSigRadius2DpsGetter, TgtSigRadius2VolleyGetter, TgtSigRadius2InflictedDamageGetter)
|
||||
from .helper import getTgtMaxVelocity, getTgtSigRadius
|
||||
|
||||
|
||||
class FitDamageStatsGraph(FitGraph):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._timeCache = TimeCache()
|
||||
self._projectedCache = ProjectedDataCache()
|
||||
|
||||
def _clearInternalCache(self, reason, extraData):
|
||||
# Here, we care only about fit changes and graph changes.
|
||||
# - Input changes are irrelevant as time cache cares only about
|
||||
# time input, and it regenerates once time goes beyond cached value
|
||||
# - Option changes are irrelevant as cache contains "raw" damage
|
||||
# values which do not rely on any graph options
|
||||
if reason in (GraphCacheCleanupReason.fitChanged, GraphCacheCleanupReason.fitRemoved):
|
||||
self._timeCache.clearForFit(extraData)
|
||||
self._projectedCache.clearForFit(extraData)
|
||||
elif reason == GraphCacheCleanupReason.graphSwitched:
|
||||
self._timeCache.clearAll()
|
||||
self._projectedCache.clearAll()
|
||||
|
||||
# UI stuff
|
||||
internalName = 'dmgStatsGraph'
|
||||
name = 'Damage Stats'
|
||||
xDefs = [
|
||||
XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km')),
|
||||
XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')),
|
||||
XDef(handle='tgtSpeed', unit='m/s', label='Target speed', mainInput=('tgtSpeed', '%')),
|
||||
XDef(handle='tgtSpeed', unit='%', label='Target speed', mainInput=('tgtSpeed', '%')),
|
||||
XDef(handle='tgtSigRad', unit='m', label='Target signature radius', mainInput=('tgtSigRad', '%')),
|
||||
XDef(handle='tgtSigRad', unit='%', label='Target signature radius', mainInput=('tgtSigRad', '%'))]
|
||||
yDefs = [
|
||||
YDef(handle='dps', unit=None, label='DPS'),
|
||||
YDef(handle='volley', unit=None, label='Volley'),
|
||||
YDef(handle='damage', unit=None, label='Damage inflicted')]
|
||||
inputs = [
|
||||
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), secondaryTooltip='When set, uses exact attacker\'s damage stats of at a given time\nWhen not set, uses attacker\'s damage stats as shown in stats panel of main window'),
|
||||
Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=None, defaultRange=(0, 100), mainTooltip='Distance between the attacker and the target, as seen in overview (surface-to-surface)', secondaryTooltip='Distance between the attacker and the target, as seen in overview (surface-to-surface)\nWhen set, places the target that far away from the attacker\nWhen not set, attacker\'s weapons always hit the target'),
|
||||
Input(handle='tgtSpeed', unit='%', label='Target speed', iconID=1389, defaultValue=100, defaultRange=(0, 100)),
|
||||
Input(handle='tgtSigRad', unit='%', label='Target signature', iconID=1390, defaultValue=100, defaultRange=(100, 200), mainOnly=True)]
|
||||
srcVectorDef = VectorDef(lengthHandle='atkSpeed', lengthUnit='%', angleHandle='atkAngle', angleUnit='degrees', label='Attacker')
|
||||
tgtVectorDef = VectorDef(lengthHandle='tgtSpeed', lengthUnit='%', angleHandle='tgtAngle', angleUnit='degrees', label='Target')
|
||||
hasTargets = True
|
||||
srcExtraCols = ('Dps', 'Volley', 'Speed', 'Radius')
|
||||
tgtExtraCols = ('Speed', 'SigRadius', 'Radius')
|
||||
|
||||
# Calculation stuff
|
||||
_normalizers = {
|
||||
('distance', 'km'): lambda v, fit, tgt: None if v is None else v * 1000,
|
||||
('atkSpeed', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('maxVelocity'),
|
||||
('tgtSpeed', '%'): lambda v, fit, tgt: v / 100 * getTgtMaxVelocity(tgt),
|
||||
('tgtSigRad', '%'): lambda v, fit, tgt: v / 100 * getTgtSigRadius(tgt)}
|
||||
_limiters = {
|
||||
'time': lambda fit, tgt: (0, 2500)}
|
||||
_getters = {
|
||||
('distance', 'dps'): Distance2DpsGetter,
|
||||
('distance', 'volley'): Distance2VolleyGetter,
|
||||
('distance', 'damage'): Distance2InflictedDamageGetter,
|
||||
('time', 'dps'): Time2DpsGetter,
|
||||
('time', 'volley'): Time2VolleyGetter,
|
||||
('time', 'damage'): Time2InflictedDamageGetter,
|
||||
('tgtSpeed', 'dps'): TgtSpeed2DpsGetter,
|
||||
('tgtSpeed', 'volley'): TgtSpeed2VolleyGetter,
|
||||
('tgtSpeed', 'damage'): TgtSpeed2InflictedDamageGetter,
|
||||
('tgtSigRad', 'dps'): TgtSigRadius2DpsGetter,
|
||||
('tgtSigRad', 'volley'): TgtSigRadius2VolleyGetter,
|
||||
('tgtSigRad', 'damage'): TgtSigRadius2InflictedDamageGetter}
|
||||
_denormalizers = {
|
||||
('distance', 'km'): lambda v, fit, tgt: None if v is None else v / 1000,
|
||||
('tgtSpeed', '%'): lambda v, fit, tgt: v * 100 / getTgtMaxVelocity(tgt),
|
||||
('tgtSigRad', '%'): lambda v, fit, tgt: v * 100 / getTgtSigRadius(tgt)}
|
||||
89
graphs/data/fitDamageStats/helper.py
Normal file
89
graphs/data/fitDamageStats/helper.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
|
||||
from eos.saveddata.fit import Fit
|
||||
from eos.saveddata.targetProfile import TargetProfile
|
||||
|
||||
|
||||
def getTgtMaxVelocity(tgt, extraMultipliers=None):
|
||||
if isinstance(tgt, Fit):
|
||||
if extraMultipliers:
|
||||
maxVelocity = tgt.ship.getModifiedItemAttrWithExtraMods('maxVelocity', extraMultipliers=extraMultipliers)
|
||||
else:
|
||||
maxVelocity = tgt.ship.getModifiedItemAttr('maxVelocity')
|
||||
elif isinstance(tgt, TargetProfile):
|
||||
maxVelocity = tgt.maxVelocity
|
||||
if extraMultipliers:
|
||||
maxVelocity *= _calculateMultiplier(extraMultipliers)
|
||||
else:
|
||||
maxVelocity = None
|
||||
return maxVelocity
|
||||
|
||||
|
||||
def getTgtSigRadius(tgt, extraMultipliers=None):
|
||||
if isinstance(tgt, Fit):
|
||||
if extraMultipliers:
|
||||
sigRadius = tgt.ship.getModifiedItemAttrWithExtraMods('signatureRadius', extraMultipliers=extraMultipliers)
|
||||
else:
|
||||
sigRadius = tgt.ship.getModifiedItemAttr('signatureRadius')
|
||||
elif isinstance(tgt, TargetProfile):
|
||||
sigRadius = tgt.signatureRadius
|
||||
if extraMultipliers:
|
||||
sigRadius *= _calculateMultiplier(extraMultipliers)
|
||||
else:
|
||||
sigRadius = None
|
||||
return sigRadius
|
||||
|
||||
|
||||
def getTgtRadius(tgt):
|
||||
if isinstance(tgt, Fit):
|
||||
radius = tgt.ship.getModifiedItemAttr('radius')
|
||||
elif isinstance(tgt, TargetProfile):
|
||||
radius = tgt.radius
|
||||
else:
|
||||
radius = None
|
||||
return radius
|
||||
|
||||
|
||||
# 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
|
||||
24
graphs/data/fitMobility/__init__.py
Normal file
24
graphs/data/fitMobility/__init__.py
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from .graph import FitMobilityVsTimeGraph
|
||||
|
||||
|
||||
FitMobilityVsTimeGraph.register()
|
||||
62
graphs/data/fitMobility/getter.py
Normal file
62
graphs/data/fitMobility/getter.py
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
|
||||
from graphs.data.base import SmoothPointGetter
|
||||
|
||||
|
||||
class Time2SpeedGetter(SmoothPointGetter):
|
||||
|
||||
def _getCommonData(self, miscParams, fit, tgt):
|
||||
return {
|
||||
'maxSpeed': fit.ship.getModifiedItemAttr('maxVelocity'),
|
||||
'mass': fit.ship.getModifiedItemAttr('mass'),
|
||||
'agility': fit.ship.getModifiedItemAttr('agility')}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
time = x
|
||||
maxSpeed = commonData['maxSpeed']
|
||||
mass = commonData['mass']
|
||||
agility = commonData['agility']
|
||||
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
|
||||
speed = maxSpeed * (1 - math.exp((-time * 1000000) / (agility * mass)))
|
||||
return speed
|
||||
|
||||
|
||||
class Time2DistanceGetter(SmoothPointGetter):
|
||||
|
||||
def _getCommonData(self, miscParams, fit, tgt):
|
||||
return {
|
||||
'maxSpeed': fit.ship.getModifiedItemAttr('maxVelocity'),
|
||||
'mass': fit.ship.getModifiedItemAttr('mass'),
|
||||
'agility': fit.ship.getModifiedItemAttr('agility')}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
time = x
|
||||
maxSpeed = commonData['maxSpeed']
|
||||
mass = commonData['mass']
|
||||
agility = commonData['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
|
||||
44
graphs/data/fitMobility/graph.py
Normal file
44
graphs/data/fitMobility/graph.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from graphs.data.base import FitGraph, XDef, YDef, Input
|
||||
from .getter import Time2SpeedGetter, Time2DistanceGetter
|
||||
|
||||
|
||||
class FitMobilityVsTimeGraph(FitGraph):
|
||||
|
||||
# UI stuff
|
||||
internalName = 'mobilityGraph'
|
||||
name = 'Mobility'
|
||||
xDefs = [
|
||||
XDef(handle='time', unit='s', label='Time', mainInput=('time', 's'))]
|
||||
yDefs = [
|
||||
YDef(handle='speed', unit='m/s', label='Speed'),
|
||||
YDef(handle='distance', unit='km', label='Distance')]
|
||||
inputs = [
|
||||
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=10, defaultRange=(0, 30))]
|
||||
srcExtraCols = ('Speed', 'Agility')
|
||||
|
||||
# Calculation stuff
|
||||
_getters = {
|
||||
('time', 'speed'): Time2SpeedGetter,
|
||||
('time', 'distance'): Time2DistanceGetter}
|
||||
_denormalizers = {
|
||||
('distance', 'km'): lambda v, fit, tgt: v / 1000}
|
||||
24
graphs/data/fitShieldRegen/__init__.py
Normal file
24
graphs/data/fitShieldRegen/__init__.py
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from .graph import FitShieldRegenGraph
|
||||
|
||||
|
||||
FitShieldRegenGraph.register()
|
||||
95
graphs/data/fitShieldRegen/getter.py
Normal file
95
graphs/data/fitShieldRegen/getter.py
Normal file
@@ -0,0 +1,95 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
|
||||
from graphs.data.base import SmoothPointGetter
|
||||
|
||||
|
||||
class Time2ShieldAmountGetter(SmoothPointGetter):
|
||||
|
||||
def _getCommonData(self, miscParams, fit, tgt):
|
||||
return {
|
||||
'maxShieldAmount': fit.ship.getModifiedItemAttr('shieldCapacity'),
|
||||
'shieldRegenTime': fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
time = x
|
||||
shieldAmount = calculateShieldAmount(
|
||||
maxShieldAmount=commonData['maxShieldAmount'],
|
||||
shieldRegenTime=commonData['shieldRegenTime'],
|
||||
time=time)
|
||||
return shieldAmount
|
||||
|
||||
|
||||
class Time2ShieldRegenGetter(SmoothPointGetter):
|
||||
|
||||
def _getCommonData(self, miscParams, fit, tgt):
|
||||
return {
|
||||
'maxShieldAmount': fit.ship.getModifiedItemAttr('shieldCapacity'),
|
||||
'shieldRegenTime': fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
time = x
|
||||
shieldAmount = calculateShieldAmount(
|
||||
maxShieldAmount=commonData['maxShieldAmount'],
|
||||
shieldRegenTime=commonData['shieldRegenTime'],
|
||||
time=time)
|
||||
shieldRegen = calculateShieldRegen(
|
||||
maxShieldAmount=commonData['maxShieldAmount'],
|
||||
shieldRegenTime=commonData['shieldRegenTime'],
|
||||
currentShieldAmount=shieldAmount)
|
||||
return shieldRegen
|
||||
|
||||
|
||||
# Useless, but valid combination of x and y
|
||||
class ShieldAmount2ShieldAmountGetter(SmoothPointGetter):
|
||||
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
shieldAmount = x
|
||||
return shieldAmount
|
||||
|
||||
|
||||
class ShieldAmount2ShieldRegenGetter(SmoothPointGetter):
|
||||
|
||||
def _getCommonData(self, miscParams, fit, tgt):
|
||||
return {
|
||||
'maxShieldAmount': fit.ship.getModifiedItemAttr('shieldCapacity'),
|
||||
'shieldRegenTime': fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
shieldAmount = x
|
||||
shieldRegen = calculateShieldRegen(
|
||||
maxShieldAmount=commonData['maxShieldAmount'],
|
||||
shieldRegenTime=commonData['shieldRegenTime'],
|
||||
currentShieldAmount=shieldAmount)
|
||||
return shieldRegen
|
||||
|
||||
|
||||
def calculateShieldAmount(maxShieldAmount, shieldRegenTime, time):
|
||||
# The same formula as for cap
|
||||
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
|
||||
return maxShieldAmount * (1 + math.exp(5 * -time / shieldRegenTime) * -1) ** 2
|
||||
|
||||
|
||||
def calculateShieldRegen(maxShieldAmount, shieldRegenTime, currentShieldAmount):
|
||||
# The same formula as for cap
|
||||
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
|
||||
return 10 * maxShieldAmount / shieldRegenTime * (math.sqrt(currentShieldAmount / maxShieldAmount) - currentShieldAmount / maxShieldAmount)
|
||||
60
graphs/data/fitShieldRegen/graph.py
Normal file
60
graphs/data/fitShieldRegen/graph.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from graphs.data.base import FitGraph, XDef, YDef, Input
|
||||
from .getter import (
|
||||
Time2ShieldAmountGetter, Time2ShieldRegenGetter,
|
||||
ShieldAmount2ShieldAmountGetter, ShieldAmount2ShieldRegenGetter)
|
||||
|
||||
|
||||
class FitShieldRegenGraph(FitGraph):
|
||||
|
||||
# UI stuff
|
||||
internalName = 'shieldRegenGraph'
|
||||
name = 'Shield Regeneration'
|
||||
xDefs = [
|
||||
XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')),
|
||||
XDef(handle='shieldAmount', unit='EHP', label='Shield amount', mainInput=('shieldAmount', '%')),
|
||||
XDef(handle='shieldAmount', unit='HP', label='Shield amount', mainInput=('shieldAmount', '%')),
|
||||
XDef(handle='shieldAmount', unit='%', label='Shield amount', mainInput=('shieldAmount', '%'))]
|
||||
yDefs = [
|
||||
YDef(handle='shieldAmount', unit='EHP', label='Shield amount'),
|
||||
YDef(handle='shieldAmount', unit='HP', label='Shield amount'),
|
||||
YDef(handle='shieldRegen', unit='EHP/s', label='Shield regen'),
|
||||
YDef(handle='shieldRegen', unit='HP/s', label='Shield regen')]
|
||||
inputs = [
|
||||
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), mainOnly=True),
|
||||
Input(handle='shieldAmount', unit='%', label='Shield amount', iconID=1384, defaultValue=25, defaultRange=(0, 100), mainOnly=True)]
|
||||
srcExtraCols = ('ShieldAmount', 'ShieldTime')
|
||||
|
||||
# Calculation stuff
|
||||
_normalizers = {
|
||||
('shieldAmount', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('shieldCapacity')}
|
||||
_limiters = {
|
||||
'shieldAmount': lambda fit, tgt: (0, fit.ship.getModifiedItemAttr('shieldCapacity'))}
|
||||
_getters = {
|
||||
('time', 'shieldAmount'): Time2ShieldAmountGetter,
|
||||
('time', 'shieldRegen'): Time2ShieldRegenGetter,
|
||||
('shieldAmount', 'shieldAmount'): ShieldAmount2ShieldAmountGetter,
|
||||
('shieldAmount', 'shieldRegen'): ShieldAmount2ShieldRegenGetter}
|
||||
_denormalizers = {
|
||||
('shieldAmount', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('shieldCapacity'),
|
||||
('shieldAmount', 'EHP'): lambda v, fit, tgt: fit.damagePattern.effectivify(fit, v, 'shield'),
|
||||
('shieldRegen', 'EHP/s'): lambda v, fit, tgt: fit.damagePattern.effectivify(fit, v, 'shield')}
|
||||
24
graphs/data/fitWarpTime/__init__.py
Normal file
24
graphs/data/fitWarpTime/__init__.py
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from .graph import FitWarpTimeGraph
|
||||
|
||||
|
||||
FitWarpTimeGraph.register()
|
||||
82
graphs/data/fitWarpTime/cache.py
Normal file
82
graphs/data/fitWarpTime/cache.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from eos.const import FittingModuleState
|
||||
from graphs.data.base import FitDataCache
|
||||
|
||||
|
||||
class SubwarpSpeedCache(FitDataCache):
|
||||
|
||||
def getSubwarpSpeed(self, fit):
|
||||
try:
|
||||
subwarpSpeed = self._data[fit.ID]
|
||||
except KeyError:
|
||||
modStates = {}
|
||||
disallowedGroups = (
|
||||
# Active modules which affect ship speed and cannot be used in warp
|
||||
'Propulsion Module',
|
||||
'Mass Entanglers',
|
||||
'Cloaking Device',
|
||||
# Those reduce ship speed to 0
|
||||
'Siege Module',
|
||||
'Super Weapon',
|
||||
'Cynosural Field Generator',
|
||||
'Clone Vat Bay',
|
||||
'Jump Portal Generator')
|
||||
for mod in fit.modules:
|
||||
if mod.item is not None and mod.item.group.name in disallowedGroups 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()
|
||||
subwarpSpeed = fit.ship.getModifiedItemAttr('maxVelocity')
|
||||
self._data[fit.ID] = subwarpSpeed
|
||||
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()
|
||||
return subwarpSpeed
|
||||
78
graphs/data/fitWarpTime/getter.py
Normal file
78
graphs/data/fitWarpTime/getter.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
|
||||
from graphs.data.base import SmoothPointGetter
|
||||
|
||||
|
||||
AU_METERS = 149597870700
|
||||
|
||||
|
||||
class Distance2TimeGetter(SmoothPointGetter):
|
||||
|
||||
def __init__(self, graph):
|
||||
super().__init__(graph, baseResolution=500, extraDepth=0)
|
||||
|
||||
def _getCommonData(self, miscParams, fit, tgt):
|
||||
return {
|
||||
'subwarpSpeed': self.graph._subspeedCache.getSubwarpSpeed(fit),
|
||||
'warpSpeed': fit.warpSpeed}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
|
||||
distance = x
|
||||
time = calculate_time_in_warp(
|
||||
max_subwarp_speed=commonData['subwarpSpeed'],
|
||||
max_warp_speed=commonData['warpSpeed'],
|
||||
warp_dist=distance)
|
||||
return time
|
||||
|
||||
|
||||
# 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):
|
||||
|
||||
if warp_dist == 0:
|
||||
return 0
|
||||
|
||||
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
|
||||
62
graphs/data/fitWarpTime/graph.py
Normal file
62
graphs/data/fitWarpTime/graph.py
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from graphs.data.base import FitGraph, Input, XDef, YDef
|
||||
from service.const import GraphCacheCleanupReason
|
||||
from .cache import SubwarpSpeedCache
|
||||
from .getter import AU_METERS, Distance2TimeGetter
|
||||
|
||||
|
||||
class FitWarpTimeGraph(FitGraph):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._subspeedCache = SubwarpSpeedCache()
|
||||
|
||||
def _clearInternalCache(self, reason, extraData):
|
||||
if reason in (GraphCacheCleanupReason.fitChanged, GraphCacheCleanupReason.fitRemoved):
|
||||
self._subspeedCache.clearForFit(extraData)
|
||||
elif reason == GraphCacheCleanupReason.graphSwitched:
|
||||
self._subspeedCache.clearAll()
|
||||
|
||||
# UI stuff
|
||||
internalName = 'warpTimeGraph'
|
||||
name = 'Warp Time'
|
||||
xDefs = [
|
||||
XDef(handle='distance', unit='AU', label='Distance', mainInput=('distance', 'AU')),
|
||||
XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km'))]
|
||||
yDefs = [
|
||||
YDef(handle='time', unit='s', label='Warp time')]
|
||||
inputs = [
|
||||
Input(handle='distance', unit='AU', label='Distance', iconID=1391, defaultValue=20, defaultRange=(0, 50)),
|
||||
Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=1000, defaultRange=(150, 5000))]
|
||||
srcExtraCols = ('WarpSpeed', 'WarpDistance')
|
||||
|
||||
# Calculation stuff
|
||||
_normalizers = {
|
||||
('distance', 'AU'): lambda v, fit, tgt: v * AU_METERS,
|
||||
('distance', 'km'): lambda v, fit, tgt: v * 1000}
|
||||
_limiters = {
|
||||
'distance': lambda fit, tgt: (0, fit.maxWarpDistance * AU_METERS)}
|
||||
_getters = {
|
||||
('distance', 'time'): Distance2TimeGetter}
|
||||
_denormalizers = {
|
||||
('distance', 'AU'): lambda v, fit, tgt: v / AU_METERS,
|
||||
('distance', 'km'): lambda v, fit, tgt: v / 1000}
|
||||
Reference in New Issue
Block a user