Store wrappers in graph lists

This commit is contained in:
DarkPhoenix
2019-08-03 23:56:44 +03:00
parent 1b2bff8a77
commit e821b2d09c
26 changed files with 604 additions and 567 deletions

View File

@@ -28,11 +28,11 @@ class PointGetter(metaclass=ABCMeta):
self.graph = graph
@abstractmethod
def getRange(self, xRange, miscParams, fit, tgt):
def getRange(self, xRange, miscParams, src, tgt):
raise NotImplementedError
@abstractmethod
def getPoint(self, x, miscParams, fit, tgt):
def getPoint(self, x, miscParams, src, tgt):
raise NotImplementedError
@@ -41,16 +41,16 @@ class SmoothPointGetter(PointGetter, metaclass=ABCMeta):
_baseResolution = 200
_extraDepth = 0
def getRange(self, xRange, miscParams, fit, tgt):
def getRange(self, xRange, miscParams, src, tgt):
xs = []
ys = []
commonData = self._getCommonData(miscParams=miscParams, fit=fit, tgt=tgt)
commonData = self._getCommonData(miscParams=miscParams, src=src, 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)
newY = self._calculatePoint(x=newX, miscParams=miscParams, src=src, tgt=tgt, commonData=commonData)
addExtraPoints(x1=prevX, y1=prevY, x2=newX, y2=newY, depth=depth - 1)
xs.append(newX)
ys.append(newY)
@@ -60,7 +60,7 @@ class SmoothPointGetter(PointGetter, metaclass=ABCMeta):
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)
y = self._calculatePoint(x=x, miscParams=miscParams, src=src, 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
@@ -71,9 +71,9 @@ class SmoothPointGetter(PointGetter, metaclass=ABCMeta):
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 getPoint(self, x, miscParams, src, tgt):
commonData = self._getCommonData(miscParams=miscParams, src=src, tgt=tgt)
return self._calculatePoint(x=x, miscParams=miscParams, src=src, tgt=tgt, commonData=commonData)
def _xIterLinear(self, xRange):
xLow = min(xRange)
@@ -87,9 +87,9 @@ class SmoothPointGetter(PointGetter, metaclass=ABCMeta):
for i in range(self._baseResolution + 1):
yield xLow + step * i
def _getCommonData(self, miscParams, fit, tgt):
def _getCommonData(self, miscParams, src, tgt):
return {}
@abstractmethod
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
raise NotImplementedError

View File

@@ -22,8 +22,6 @@ 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
@@ -91,18 +89,20 @@ class FitGraph(metaclass=ABCMeta):
tgtVectorDef = None
hasTargets = False
def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt=None):
if isinstance(tgt, Fit):
def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, src, tgt=None):
if tgt is not None and tgt.isFit:
tgtType = 'fit'
elif isinstance(tgt, TargetProfile):
elif tgt is not None and tgt.isProfile:
tgtType = 'profile'
else:
tgtType = None
cacheKey = (fit.ID, tgtType, getattr(tgt, 'ID', None))
cacheKey = (src.itemID, tgtType, getattr(tgt, 'itemID', None))
try:
plotData = self._plotCache[cacheKey][(ySpec, xSpec)]
except KeyError:
plotData = self._calcPlotPoints(mainInput, miscInputs, xSpec, ySpec, fit, tgt)
plotData = self._calcPlotPoints(
mainInput=mainInput, miscInputs=miscInputs,
xSpec=xSpec, ySpec=ySpec, src=src, tgt=tgt)
self._plotCache.setdefault(cacheKey, {})[(ySpec, xSpec)] = plotData
return plotData
@@ -136,17 +136,23 @@ class FitGraph(metaclass=ABCMeta):
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)
def _calcPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, src, tgt):
mainParamRange, miscParams = self._normalizeInputs(
mainInput=mainInput, miscInputs=miscInputs,
src=src, tgt=tgt)
mainParamRange, miscParams = self._limitParams(
mainParamRange=mainParamRange, miscParams=miscParams,
src=src, tgt=tgt)
xs, ys = self._getPoints(
xRange=mainParamRange[1], miscParams=miscParams,
xSpec=xSpec, ySpec=ySpec, src=src, tgt=tgt)
ys = self._denormalizeValues(values=ys, axisSpec=ySpec, src=src, tgt=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
# proper graph which shows the same value over whole specified relative parameter
# range
try:
xs = self._denormalizeValues(xs, xSpec, fit, tgt)
xs = self._denormalizeValues(values=xs, axisSpec=xSpec, src=src, tgt=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)]
@@ -163,11 +169,11 @@ class FitGraph(metaclass=ABCMeta):
_normalizers = {}
def _normalizeInputs(self, mainInput, miscInputs, fit, tgt):
def _normalizeInputs(self, mainInput, miscInputs, src, 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))
mainParamRange = (mainInput.handle, tuple(normalizer(v, src, tgt) for v in mainInput.value))
else:
mainParamRange = (mainInput.handle, mainInput.value)
miscParams = []
@@ -175,7 +181,7 @@ class FitGraph(metaclass=ABCMeta):
key = (miscInput.handle, miscInput.unit)
if key in self._normalizers:
normalizer = self._normalizers[key]
miscParam = (miscInput.handle, normalizer(miscInput.value, fit, tgt))
miscParam = (miscInput.handle, normalizer(miscInput.value, src, tgt))
else:
miscParam = (miscInput.handle, miscInput.value)
miscParams.append(miscParam)
@@ -183,7 +189,7 @@ class FitGraph(metaclass=ABCMeta):
_limiters = {}
def _limitParams(self, mainParamRange, miscParams, fit, tgt):
def _limitParams(self, mainParamRange, miscParams, src, tgt):
def limitToRange(val, limitRange):
if val is None:
@@ -195,7 +201,7 @@ class FitGraph(metaclass=ABCMeta):
mainHandle, mainValue = mainParamRange
if mainHandle in self._limiters:
limiter = self._limiters[mainHandle]
newMainParamRange = (mainHandle, tuple(limitToRange(v, limiter(fit, tgt)) for v in mainValue))
newMainParamRange = (mainHandle, tuple(limitToRange(v, limiter(src, tgt)) for v in mainValue))
else:
newMainParamRange = mainParamRange
newMiscParams = []
@@ -203,7 +209,7 @@ class FitGraph(metaclass=ABCMeta):
miscHandle, miscValue = miscParam
if miscHandle in self._limiters:
limiter = self._limiters[miscHandle]
newMiscParam = (miscHandle, limitToRange(miscValue, limiter(fit, tgt)))
newMiscParam = (miscHandle, limitToRange(miscValue, limiter(src, tgt)))
newMiscParams.append(newMiscParam)
else:
newMiscParams.append(miscParam)
@@ -211,20 +217,20 @@ class FitGraph(metaclass=ABCMeta):
_getters = {}
def _getPoints(self, xRange, miscParams, xSpec, ySpec, fit, tgt):
def _getPoints(self, xRange, miscParams, xSpec, ySpec, src, 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)
return getter.getRange(xRange=xRange, miscParams=miscParams, src=src, tgt=tgt)
_denormalizers = {}
def _denormalizeValues(self, values, axisSpec, fit, tgt):
def _denormalizeValues(self, values, axisSpec, src, tgt):
key = (axisSpec.handle, axisSpec.unit)
if key in self._denormalizers:
denormalizer = self._denormalizers[key]
values = [denormalizer(v, fit, tgt) for v in values]
values = [denormalizer(v, src, tgt) for v in values]
return values

View File

@@ -25,12 +25,12 @@ from graphs.data.base import SmoothPointGetter
class Time2CapAmountGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxCapAmount': fit.ship.getModifiedItemAttr('capacitorCapacity'),
'capRegenTime': fit.ship.getModifiedItemAttr('rechargeRate') / 1000}
'maxCapAmount': src.item.ship.getModifiedItemAttr('capacitorCapacity'),
'capRegenTime': src.item.ship.getModifiedItemAttr('rechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
capAmount = calculateCapAmount(
maxCapAmount=commonData['maxCapAmount'],
@@ -41,12 +41,12 @@ class Time2CapAmountGetter(SmoothPointGetter):
class Time2CapRegenGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxCapAmount': fit.ship.getModifiedItemAttr('capacitorCapacity'),
'capRegenTime': fit.ship.getModifiedItemAttr('rechargeRate') / 1000}
'maxCapAmount': src.item.ship.getModifiedItemAttr('capacitorCapacity'),
'capRegenTime': src.item.ship.getModifiedItemAttr('rechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
capAmount = calculateCapAmount(
maxCapAmount=commonData['maxCapAmount'],
@@ -62,19 +62,19 @@ class Time2CapRegenGetter(SmoothPointGetter):
# Useless, but valid combination of x and y
class CapAmount2CapAmountGetter(SmoothPointGetter):
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
capAmount = x
return capAmount
class CapAmount2CapRegenGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxCapAmount': fit.ship.getModifiedItemAttr('capacitorCapacity'),
'capRegenTime': fit.ship.getModifiedItemAttr('rechargeRate') / 1000}
'maxCapAmount': src.item.ship.getModifiedItemAttr('capacitorCapacity'),
'capRegenTime': src.item.ship.getModifiedItemAttr('rechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
capAmount = x
capRegen = calculateCapRegen(
maxCapAmount=commonData['maxCapAmount'],

View File

@@ -41,13 +41,13 @@ class FitCapRegenGraph(FitGraph):
# Calculation stuff
_normalizers = {
('capAmount', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('capacitorCapacity')}
('capAmount', '%'): lambda v, src, tgt: v / 100 * src.item.ship.getModifiedItemAttr('capacitorCapacity')}
_limiters = {
'capAmount': lambda fit, tgt: (0, fit.ship.getModifiedItemAttr('capacitorCapacity'))}
'capAmount': lambda src, tgt: (0, src.item.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')}
('capAmount', '%'): lambda v, src, tgt: v * 100 / src.item.ship.getModifiedItemAttr('capacitorCapacity')}

View File

@@ -31,15 +31,15 @@ MobileProjData = namedtuple('MobileProjData', ('boost', 'optimal', 'falloff', 's
class ProjectedDataCache(FitDataCache):
def getProjModData(self, fit):
def getProjModData(self, src):
try:
projectedData = self._data[fit.ID]['modules']
projectedData = self._data[src.item.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:
projectedData = self._data.setdefault(src.item.ID, {})['modules'] = (webMods, tpMods)
for mod in src.item.modules:
if mod.state <= FittingModuleState.ONLINE:
continue
for webEffectName in ('remoteWebifierFalloff', 'structureModuleEffectStasisWebifier'):
@@ -53,7 +53,7 @@ class ProjectedDataCache(FitDataCache):
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')),
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0,
'default',
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects['doomsdayAOEWeb'])))
@@ -68,21 +68,21 @@ class ProjectedDataCache(FitDataCache):
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')),
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0,
'default',
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects['doomsdayAOEPaint'])))
return projectedData
def getProjDroneData(self, fit):
def getProjDroneData(self, src):
try:
projectedData = self._data[fit.ID]['drones']
projectedData = self._data[src.item.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:
projectedData = self._data.setdefault(src.item.ID, {})['drones'] = (webDrones, tpDrones)
for drone in src.item.drones:
if drone.amountActive <= 0:
continue
if 'remoteWebifierEntity' in drone.item.effects:
@@ -105,15 +105,15 @@ class ProjectedDataCache(FitDataCache):
drone.getModifiedItemAttr('radius')),))
return projectedData
def getProjFighterData(self, fit):
def getProjFighterData(self, src):
try:
projectedData = self._data[fit.ID]['fighters']
projectedData = self._data[src.item.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:
projectedData = self._data.setdefault(src.item.ID, {})['fighters'] = (webFighters, tpFighters)
for fighter in src.item.fighters:
if not fighter.active:
continue
for ability in fighter.abilities:

View File

@@ -29,45 +29,45 @@ from graphs.data.base import FitDataCache
class TimeCache(FitDataCache):
# Whole data getters
def getDpsData(self, fit):
def getDpsData(self, src):
"""Return DPS data in {time: {key: dps}} format."""
return self._data[fit.ID]['finalDps']
return self._data[src.item.ID]['finalDps']
def getVolleyData(self, fit):
def getVolleyData(self, src):
"""Return volley data in {time: {key: volley}} format."""
return self._data[fit.ID]['finalVolley']
return self._data[src.item.ID]['finalVolley']
def getDmgData(self, fit):
def getDmgData(self, src):
"""Return inflicted damage data in {time: {key: damage}} format."""
return self._data[fit.ID]['finalDmg']
return self._data[src.item.ID]['finalDmg']
# Specific data point getters
def getDpsDataPoint(self, fit, time):
def getDpsDataPoint(self, src, time):
"""Get DPS data by specified time in {key: dps} format."""
return self._getDataPoint(fit, time, self.getDpsData)
return self._getDataPoint(src=src, time=time, dataFunc=self.getDpsData)
def getVolleyDataPoint(self, fit, time):
def getVolleyDataPoint(self, src, time):
"""Get volley data by specified time in {key: volley} format."""
return self._getDataPoint(fit, time, self.getVolleyData)
return self._getDataPoint(src=src, time=time, dataFunc=self.getVolleyData)
def getDmgDataPoint(self, fit, time):
def getDmgDataPoint(self, src, time):
"""Get inflicted damage data by specified time in {key: dmg} format."""
return self._getDataPoint(fit, time, self.getDmgData)
return self._getDataPoint(src=src, time=time, dataFunc=self.getDmgData)
# Preparation functions
def prepareDpsData(self, fit, maxTime):
self._prepareDpsVolleyData(fit, maxTime)
def prepareDpsData(self, src, maxTime):
self._prepareDpsVolleyData(src=src, maxTime=maxTime)
def prepareVolleyData(self, fit, maxTime):
self._prepareDpsVolleyData(fit, maxTime)
def prepareVolleyData(self, src, maxTime):
self._prepareDpsVolleyData(src=src, maxTime=maxTime)
def prepareDmgData(self, fit, maxTime):
def prepareDmgData(self, src, 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]
self._generateInternalForm(src=src, maxTime=maxTime)
fitCache = self._data[src.item.ID]
# Final cache has been generated already, don't do anything
if 'finalDmg' in fitCache:
return
@@ -93,13 +93,13 @@ class TimeCache(FitDataCache):
del fitCache['internalDmg']
# Private stuff
def _prepareDpsVolleyData(self, fit, maxTime):
def _prepareDpsVolleyData(self, src, 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]
self._generateInternalForm(src=src, maxTime=maxTime)
fitCache = self._data[src.item.ID]
# Final cache has been generated already, don't do anything
if 'finalDps' in fitCache and 'finalVolley' in fitCache:
return
@@ -147,10 +147,10 @@ class TimeCache(FitDataCache):
finalDpsCache[time] = timeDpsData
finalVolleyCache[time] = timeVolleyData
def _generateInternalForm(self, fit, maxTime):
if self._isTimeCacheValid(fit, maxTime):
def _generateInternalForm(self, src, maxTime):
if self._isTimeCacheValid(src=src, maxTime=maxTime):
return
fitCache = self._data[fit.ID] = {'maxTime': maxTime}
fitCache = self._data[src.item.ID] = {'maxTime': maxTime}
intCacheDpsVolley = fitCache['internalDpsVolley'] = {}
intCacheDmg = fitCache['internalDmg'] = {}
@@ -173,7 +173,7 @@ class TimeCache(FitDataCache):
intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg
# Modules
for mod in fit.modules:
for mod in src.item.modules:
if not mod.isDealingDamage():
continue
cycleParams = mod.getCycleParameters(reloadOverride=True)
@@ -196,7 +196,7 @@ class TimeCache(FitDataCache):
break
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
# Drones
for drone in fit.drones:
for drone in src.item.drones:
if not drone.isDealingDamage():
continue
cycleParams = drone.getCycleParameters(reloadOverride=True)
@@ -214,7 +214,7 @@ class TimeCache(FitDataCache):
break
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
# Fighters
for fighter in fit.fighters:
for fighter in src.item.fighters:
if not fighter.isDealingDamage():
continue
cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True)
@@ -236,15 +236,15 @@ class TimeCache(FitDataCache):
break
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
def _isTimeCacheValid(self, fit, maxTime):
def _isTimeCacheValid(self, src, maxTime):
try:
cacheMaxTime = self._data[fit.ID]['maxTime']
cacheMaxTime = self._data[src.item.ID]['maxTime']
except KeyError:
return False
return maxTime <= cacheMaxTime
def _getDataPoint(self, fit, time, dataFunc):
data = dataFunc(fit)
def _getDataPoint(self, src, time, dataFunc):
data = dataFunc(src)
timesBefore = [t for t in data if floatUnerr(t) <= floatUnerr(time)]
try:
time = max(timesBefore)

View File

@@ -22,22 +22,20 @@ 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):
def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
applicationMap = {}
for mod in fit.modules:
for mod in src.item.modules:
if not mod.isDealingDamage():
continue
if mod.hardpoint == FittingHardpoint.TURRET:
applicationMap[mod] = getTurretMult(
mod=mod,
fit=fit,
src=src,
tgt=tgt,
atkSpeed=atkSpeed,
atkAngle=atkAngle,
@@ -48,7 +46,7 @@ def getApplicationPerKey(fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
elif mod.hardpoint == FittingHardpoint.MISSILE:
applicationMap[mod] = getLauncherMult(
mod=mod,
fit=fit,
src=src,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
@@ -59,14 +57,14 @@ def getApplicationPerKey(fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
elif mod.item.group.name == 'Missile Launcher Bomb':
applicationMap[mod] = getBombMult(
mod=mod,
fit=fit,
src=src,
tgt=tgt,
distance=distance,
tgtSigRadius=tgtSigRadius)
elif mod.item.group.name == 'Structure Guided Bomb Launcher':
applicationMap[mod] = getGuidedBombMult(
mod=mod,
fit=fit,
src=src,
distance=distance,
tgtSigRadius=tgtSigRadius)
elif mod.item.group.name in ('Super Weapon', 'Structure Doomsday Weapon'):
@@ -75,12 +73,12 @@ def getApplicationPerKey(fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
tgt=tgt,
distance=distance,
tgtSigRadius=tgtSigRadius)
for drone in fit.drones:
for drone in src.item.drones:
if not drone.isDealingDamage():
continue
applicationMap[drone] = getDroneMult(
drone=drone,
fit=fit,
src=src,
tgt=tgt,
atkSpeed=atkSpeed,
atkAngle=atkAngle,
@@ -88,7 +86,7 @@ def getApplicationPerKey(fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius)
for fighter in fit.fighters:
for fighter in src.item.fighters:
if not fighter.isDealingDamage():
continue
for ability in fighter.abilities:
@@ -97,7 +95,7 @@ def getApplicationPerKey(fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
applicationMap[(fighter, ability.effectID)] = getFighterAbilityMult(
fighter=fighter,
ability=ability,
fit=fit,
src=src,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
@@ -108,11 +106,11 @@ def getApplicationPerKey(fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
# Item application multiplier calculation
def getTurretMult(mod, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
def getTurretMult(mod, src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
cth = _calcTurretChanceToHit(
atkSpeed=atkSpeed,
atkAngle=atkAngle,
atkRadius=fit.ship.getModifiedItemAttr('radius'),
atkRadius=src.getRadius(),
atkOptimalRange=mod.maxRange,
atkFalloffRange=mod.falloff,
atkTracking=mod.getModifiedItemAttr('trackingSpeed'),
@@ -120,17 +118,17 @@ def getTurretMult(mod, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngl
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtRadius=getTgtRadius(tgt),
tgtRadius=tgt.getRadius(),
tgtSigRadius=tgtSigRadius)
mult = _calcTurretMult(cth)
return mult
def getLauncherMult(mod, fit, distance, tgtSpeed, tgtSigRadius):
def getLauncherMult(mod, src, distance, tgtSpeed, tgtSigRadius):
modRange = mod.maxRange
if modRange is None:
return 0
if distance is not None and distance + fit.ship.getModifiedItemAttr('radius') > modRange:
if distance is not None and distance + src.getRadius() > modRange:
return 0
mult = _calcMissileFactor(
atkEr=mod.getModifiedChargeAttr('aoeCloudSize'),
@@ -157,8 +155,8 @@ def getDoomsdayMult(mod, tgt, distance, tgtSigRadius):
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'):
# Disallow only against subcaps, allow against caps and tgt profiles
if tgt.isFit and not tgt.item.ship.item.requiresSkill('Capital Ships'):
return 0
damageSig = mod.getModifiedItemAttr('doomsdayDamageRadius') or mod.getModifiedItemAttr('signatureRadius')
if not damageSig:
@@ -166,13 +164,13 @@ def getDoomsdayMult(mod, tgt, distance, tgtSigRadius):
return min(1, tgtSigRadius / damageSig)
def getBombMult(mod, fit, tgt, distance, tgtSigRadius):
def getBombMult(mod, src, tgt, distance, tgtSigRadius):
modRange = mod.maxRange
if modRange is None:
return 0
blastRadius = mod.getModifiedChargeAttr('explosionRange')
atkRadius = fit.ship.getModifiedItemAttr('radius')
tgtRadius = getTgtRadius(tgt)
atkRadius = src.getRadius()
tgtRadius = tgt.getRadius()
# 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
@@ -185,11 +183,11 @@ def getBombMult(mod, fit, tgt, distance, tgtSigRadius):
tgtSigRadius=tgtSigRadius)
def getGuidedBombMult(mod, fit, distance, tgtSigRadius):
def getGuidedBombMult(mod, src, distance, tgtSigRadius):
modRange = mod.maxRange
if modRange is None:
return 0
if distance is not None and distance > modRange - fit.ship.getModifiedItemAttr('radius'):
if distance is not None and distance > modRange - src.getRadius():
return 0
eR = mod.getModifiedChargeAttr('aoeCloudSize')
if eR == 0:
@@ -198,8 +196,8 @@ def getGuidedBombMult(mod, fit, distance, tgtSigRadius):
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']:
def getDroneMult(drone, src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
if distance is not None and distance > src.item.extraAttributes['droneControlRange']:
return 0
droneSpeed = drone.getModifiedItemAttr('maxVelocity')
# Hard to simulate drone behavior, so assume chance to hit is 1 for mobile drones
@@ -219,8 +217,8 @@ def getDroneMult(drone, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAng
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
# to attacker ship's radiuses to have drone surface to ship surface distance
cthDistance = distance + src.getRadius() - droneRadius
cth = _calcTurretChanceToHit(
atkSpeed=min(atkSpeed, droneSpeed),
atkAngle=atkAngle,
@@ -232,13 +230,13 @@ def getDroneMult(drone, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAng
distance=cthDistance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtRadius=getTgtRadius(tgt),
tgtRadius=tgt.getRadius(),
tgtSigRadius=tgtSigRadius)
mult = _calcTurretMult(cth)
return mult
def getFighterAbilityMult(fighter, ability, fit, distance, tgtSpeed, tgtSigRadius):
def getFighterAbilityMult(fighter, ability, src, distance, tgtSpeed, tgtSigRadius):
fighterSpeed = fighter.getModifiedItemAttr('maxVelocity')
attrPrefix = ability.attrPrefix
# It's bomb attack
@@ -257,7 +255,7 @@ def getFighterAbilityMult(fighter, ability, fit, distance, tgtSpeed, tgtSigRadiu
if distance is None:
rangeFactorDistance = None
else:
rangeFactorDistance = distance + fit.ship.getModifiedItemAttr('radius') - fighter.getModifiedItemAttr('radius')
rangeFactorDistance = distance + src.getRadius() - fighter.getModifiedItemAttr('radius')
rangeFactor = _calcRangeFactor(
atkOptimalRange=fighter.getModifiedItemAttr('{}RangeOptimal'.format(attrPrefix)) or fighter.getModifiedItemAttr('{}Range'.format(attrPrefix)),
atkFalloffRange=fighter.getModifiedItemAttr('{}RangeFalloff'.format(attrPrefix)),

View File

@@ -20,19 +20,17 @@
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'):
def getWebbedSpeed(src, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighters, distance):
# Can slow down non-immune ships and target profiles
if tgt.isFit and tgt.item.ship.getModifiedItemAttr('disallowOffensiveModifiers'):
return currentUnwebbedSpeed
maxUnwebbedSpeed = getTgtMaxVelocity(tgt)
maxUnwebbedSpeed = tgt.getMaxVelocity()
try:
speedRatio = currentUnwebbedSpeed / maxUnwebbedSpeed
except ZeroDivisionError:
@@ -47,15 +45,15 @@ def getWebbedSpeed(fit, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighte
distance=distance)
if appliedBoost:
appliedMultipliers.setdefault(wData.stackingGroup, []).append((1 + appliedBoost / 100, wData.resAttrID))
maxWebbedSpeed = getTgtMaxVelocity(tgt, extraMultipliers=appliedMultipliers)
maxWebbedSpeed = tgt.getMaxVelocity(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']:
if distance is None or distance <= src.extraAttributes['droneControlRange']:
mobileWebs.extend(webDrones)
atkRadius = fit.ship.getModifiedItemAttr('radius')
atkRadius = src.getRadius()
# 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]
@@ -63,7 +61,7 @@ def getWebbedSpeed(fit, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighte
for mwData in longEnoughMws:
appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + mwData.boost / 100, mwData.resAttrID))
mobileWebs.remove(mwData)
maxWebbedSpeed = getTgtMaxVelocity(tgt, extraMultipliers=appliedMultipliers)
maxWebbedSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers)
currentWebbedSpeed = maxWebbedSpeed * speedRatio
# Apply remaining webs, from fastest to slowest
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
@@ -87,17 +85,17 @@ def getWebbedSpeed(fit, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighte
distance=rangeFactorDistance)
appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + appliedMwBoost / 100, mwData.resAttrID))
mobileWebs.remove(mwData)
maxWebbedSpeed = getTgtMaxVelocity(tgt, extraMultipliers=appliedMultipliers)
maxWebbedSpeed = tgt.getMaxVelocity(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'):
def getTpMult(src, tgt, tgtSpeed, tpMods, tpDrones, tpFighters, distance):
# Can blow non-immune ships and target profiles
if tgt.isFit and tgt.item.ship.getModifiedItemAttr('disallowOffensiveModifiers'):
return 1
untpedSig = getTgtSigRadius(tgt)
untpedSig = tgt.getSigRadius()
# Modules
appliedMultipliers = {}
for tpData in tpMods:
@@ -111,10 +109,10 @@ def getTpMult(fit, tgt, tgtSpeed, tpMods, tpDrones, tpFighters, distance):
mobileTps = []
mobileTps.extend(tpFighters)
# Drones have range limit
if distance is None or distance <= fit.extraAttributes['droneControlRange']:
if distance is None or distance <= src.item.extraAttributes['droneControlRange']:
mobileTps.extend(tpDrones)
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
atkRadius = fit.ship.getModifiedItemAttr('radius')
atkRadius = src.getRadius()
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:
@@ -130,7 +128,7 @@ def getTpMult(fit, tgt, tgtSpeed, tpMods, tpDrones, tpFighters, distance):
atkFalloffRange=mtpData.falloff,
distance=rangeFactorDistance)
appliedMultipliers.setdefault(mtpData.stackingGroup, []).append((1 + appliedMtpBoost / 100, mtpData.resAttrID))
tpedSig = getTgtSigRadius(tgt, extraMultipliers=appliedMultipliers)
tpedSig = tgt.getSigRadius(extraMultipliers=appliedMultipliers)
if tpedSig == math.inf and untpedSig == math.inf:
return 1
mult = tpedSig / untpedSig

View File

@@ -25,7 +25,6 @@ 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):
@@ -38,88 +37,88 @@ def applyDamage(dmgMap, applicationMap):
# Y mixins
class YDpsMixin:
def _getDamagePerKey(self, fit, time):
def _getDamagePerKey(self, src, time):
# Use data from time cache if time was not specified
if time is not None:
return self._getTimeCacheDataPoint(fit=fit, time=time)
return self._getTimeCacheDataPoint(src=src, 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:
for mod in src.item.modules:
if not mod.isDealingDamage():
continue
dpsMap[mod] = mod.getDps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))
for drone in fit.drones:
for drone in src.item.drones:
if not drone.isDealingDamage():
continue
dpsMap[drone] = drone.getDps()
for fighter in fit.fighters:
for fighter in src.item.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 _prepareTimeCache(self, src, maxTime):
self.graph._timeCache.prepareDpsData(src=src, maxTime=maxTime)
def _getTimeCacheData(self, fit):
return self.graph._timeCache.getDpsData(fit=fit)
def _getTimeCacheData(self, src):
return self.graph._timeCache.getDpsData(src=src)
def _getTimeCacheDataPoint(self, fit, time):
return self.graph._timeCache.getDpsDataPoint(fit=fit, time=time)
def _getTimeCacheDataPoint(self, src, time):
return self.graph._timeCache.getDpsDataPoint(src=src, time=time)
class YVolleyMixin:
def _getDamagePerKey(self, fit, time):
def _getDamagePerKey(self, src, time):
# Use data from time cache if time was not specified
if time is not None:
return self._getTimeCacheDataPoint(fit=fit, time=time)
return self._getTimeCacheDataPoint(src=src, 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:
for mod in src.item.modules:
if not mod.isDealingDamage():
continue
volleyMap[mod] = mod.getVolley(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))
for drone in fit.drones:
for drone in src.item.drones:
if not drone.isDealingDamage():
continue
volleyMap[drone] = drone.getVolley()
for fighter in fit.fighters:
for fighter in src.item.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 _prepareTimeCache(self, src, maxTime):
self.graph._timeCache.prepareVolleyData(src=src, maxTime=maxTime)
def _getTimeCacheData(self, fit):
return self.graph._timeCache.getVolleyData(fit=fit)
def _getTimeCacheData(self, src):
return self.graph._timeCache.getVolleyData(src=src)
def _getTimeCacheDataPoint(self, fit, time):
return self.graph._timeCache.getVolleyDataPoint(fit=fit, time=time)
def _getTimeCacheDataPoint(self, src, time):
return self.graph._timeCache.getVolleyDataPoint(src=src, time=time)
class YInflictedDamageMixin:
def _getDamagePerKey(self, fit, time):
def _getDamagePerKey(self, src, time):
# Damage inflicted makes no sense without time specified
if time is None:
raise ValueError
return self._getTimeCacheDataPoint(fit=fit, time=time)
return self._getTimeCacheDataPoint(src=src, time=time)
def _prepareTimeCache(self, fit, maxTime):
self.graph._timeCache.prepareDmgData(fit=fit, maxTime=maxTime)
def _prepareTimeCache(self, src, maxTime):
self.graph._timeCache.prepareDmgData(src=src, maxTime=maxTime)
def _getTimeCacheData(self, fit):
return self.graph._timeCache.getDmgData(fit=fit)
def _getTimeCacheData(self, src):
return self.graph._timeCache.getDmgData(src=src)
def _getTimeCacheDataPoint(self, fit, time):
return self.graph._timeCache.getDmgDataPoint(fit=fit, time=time)
def _getTimeCacheDataPoint(self, src, time):
return self.graph._timeCache.getDmgDataPoint(src=src, time=time)
# X mixins
@@ -128,28 +127,28 @@ class XDistanceMixin(SmoothPointGetter):
_baseResolution = 50
_extraDepth = 2
def _getCommonData(self, miscParams, fit, tgt):
def _getCommonData(self, miscParams, src, 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'])
self._prepareTimeCache(src=src, maxTime=miscParamMap['time'])
return {
'applyProjected': GraphSettings.getInstance().get('applyProjected'),
'miscParamMap': miscParamMap,
'dmgMap': self._getDamagePerKey(fit=fit, time=miscParamMap['time'])}
'dmgMap': self._getDamagePerKey(src=src, time=miscParamMap['time'])}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
miscParamMap = commonData['miscParamMap']
tgtSpeed = miscParamMap['tgtSpeed']
tgtSigRadius = getTgtSigRadius(tgt)
tgtSigRadius = tgt.getSigRadius()
if commonData['applyProjected']:
webMods, tpMods = self.graph._projectedCache.getProjModData(fit)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(fit)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(fit)
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getWebbedSpeed(
fit=fit,
src=src,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
webMods=webMods,
@@ -157,7 +156,7 @@ class XDistanceMixin(SmoothPointGetter):
webFighters=webFighters,
distance=distance)
tgtSigRadius = tgtSigRadius * getTpMult(
fit=fit,
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
tpMods=tpMods,
@@ -165,7 +164,7 @@ class XDistanceMixin(SmoothPointGetter):
tpFighters=tpFighters,
distance=distance)
applicationMap = getApplicationPerKey(
fit=fit,
src=src,
tgt=tgt,
atkSpeed=miscParamMap['atkSpeed'],
atkAngle=miscParamMap['atkAngle'],
@@ -179,17 +178,17 @@ class XDistanceMixin(SmoothPointGetter):
class XTimeMixin(PointGetter):
def _prepareApplicationMap(self, miscParams, fit, tgt):
def _prepareApplicationMap(self, miscParams, src, tgt):
# Process params into more convenient form
miscParamMap = dict(miscParams)
tgtSpeed = miscParamMap['tgtSpeed']
tgtSigRadius = getTgtSigRadius(tgt)
tgtSigRadius = tgt.getSigRadius()
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)
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getWebbedSpeed(
fit=fit,
src=src,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
webMods=webMods,
@@ -197,7 +196,7 @@ class XTimeMixin(PointGetter):
webFighters=webFighters,
distance=miscParamMap['distance'])
tgtSigRadius = tgtSigRadius * getTpMult(
fit=fit,
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
tpMods=tpMods,
@@ -206,7 +205,7 @@ class XTimeMixin(PointGetter):
distance=miscParamMap['distance'])
# Get all data we need for all times into maps/caches
applicationMap = getApplicationPerKey(
fit=fit,
src=src,
tgt=tgt,
atkSpeed=miscParamMap['atkSpeed'],
atkAngle=miscParamMap['atkAngle'],
@@ -216,14 +215,14 @@ class XTimeMixin(PointGetter):
tgtSigRadius=tgtSigRadius)
return applicationMap
def getRange(self, xRange, miscParams, fit, tgt):
def getRange(self, xRange, miscParams, src, 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)
self._prepareTimeCache(src=src, maxTime=maxTime)
timeCache = self._getTimeCacheData(src=src)
applicationMap = self._prepareApplicationMap(miscParams=miscParams, src=src, tgt=tgt)
# Custom iteration for time graph to show all data points
currentDmg = None
currentTime = None
@@ -270,12 +269,12 @@ class XTimeMixin(PointGetter):
ys.append(currentDmg or 0)
return xs, ys
def getPoint(self, x, miscParams, fit, tgt):
def getPoint(self, x, miscParams, src, 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)
self._prepareTimeCache(src=src, maxTime=time)
dmgData = self._getTimeCacheDataPoint(src=src, time=time)
applicationMap = self._prepareApplicationMap(miscParams=miscParams, src=src, tgt=tgt)
y = applyDamage(dmgMap=dmgData, applicationMap=applicationMap).total
return y
@@ -285,27 +284,27 @@ class XTgtSpeedMixin(SmoothPointGetter):
_baseResolution = 50
_extraDepth = 2
def _getCommonData(self, miscParams, fit, tgt):
def _getCommonData(self, miscParams, src, 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'])
self._prepareTimeCache(src=src, maxTime=miscParamMap['time'])
return {
'applyProjected': GraphSettings.getInstance().get('applyProjected'),
'miscParamMap': miscParamMap,
'dmgMap': self._getDamagePerKey(fit=fit, time=miscParamMap['time'])}
'dmgMap': self._getDamagePerKey(src=src, time=miscParamMap['time'])}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
tgtSpeed = x
miscParamMap = commonData['miscParamMap']
tgtSigRadius = getTgtSigRadius(tgt)
tgtSigRadius = tgt.getSigRadius()
if commonData['applyProjected']:
webMods, tpMods = self.graph._projectedCache.getProjModData(fit)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(fit)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(fit)
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getWebbedSpeed(
fit=fit,
src=src,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
webMods=webMods,
@@ -313,7 +312,7 @@ class XTgtSpeedMixin(SmoothPointGetter):
webFighters=webFighters,
distance=miscParamMap['distance'])
tgtSigRadius = tgtSigRadius * getTpMult(
fit=fit,
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
tpMods=tpMods,
@@ -321,7 +320,7 @@ class XTgtSpeedMixin(SmoothPointGetter):
tpFighters=tpFighters,
distance=miscParamMap['distance'])
applicationMap = getApplicationPerKey(
fit=fit,
src=src,
tgt=tgt,
atkSpeed=miscParamMap['atkSpeed'],
atkAngle=miscParamMap['atkAngle'],
@@ -338,17 +337,17 @@ class XTgtSigRadiusMixin(SmoothPointGetter):
_baseResolution = 50
_extraDepth = 2
def _getCommonData(self, miscParams, fit, tgt):
def _getCommonData(self, miscParams, src, 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)
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getWebbedSpeed(
fit=fit,
src=src,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
webMods=webMods,
@@ -356,7 +355,7 @@ class XTgtSigRadiusMixin(SmoothPointGetter):
webFighters=webFighters,
distance=miscParamMap['distance'])
tgtSigMult = getTpMult(
fit=fit,
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
tpMods=tpMods,
@@ -365,18 +364,18 @@ class XTgtSigRadiusMixin(SmoothPointGetter):
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'])
self._prepareTimeCache(src=src, maxTime=miscParamMap['time'])
return {
'miscParamMap': miscParamMap,
'tgtSpeed': tgtSpeed,
'tgtSigMult': tgtSigMult,
'dmgMap': self._getDamagePerKey(fit=fit, time=miscParamMap['time'])}
'dmgMap': self._getDamagePerKey(src=src, time=miscParamMap['time'])}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
tgtSigRadius = x
miscParamMap = commonData['miscParamMap']
applicationMap = getApplicationPerKey(
fit=fit,
src=src,
tgt=tgt,
atkSpeed=miscParamMap['atkSpeed'],
atkAngle=miscParamMap['atkAngle'],

View File

@@ -26,7 +26,6 @@ from .getter import (
Time2DpsGetter, Time2VolleyGetter, Time2InflictedDamageGetter,
TgtSpeed2DpsGetter, TgtSpeed2VolleyGetter, TgtSpeed2InflictedDamageGetter,
TgtSigRadius2DpsGetter, TgtSigRadius2VolleyGetter, TgtSigRadius2InflictedDamageGetter)
from .helper import getTgtMaxVelocity, getTgtSigRadius
class FitDamageStatsGraph(FitGraph):
@@ -76,12 +75,12 @@ class FitDamageStatsGraph(FitGraph):
# 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)}
('distance', 'km'): lambda v, src, tgt: None if v is None else v * 1000,
('atkSpeed', '%'): lambda v, src, tgt: v / 100 * src.getMaxVelocity(),
('tgtSpeed', '%'): lambda v, src, tgt: v / 100 * tgt.getMaxVelocity(),
('tgtSigRad', '%'): lambda v, src, tgt: v / 100 * tgt.getSigRadius()}
_limiters = {
'time': lambda fit, tgt: (0, 2500)}
'time': lambda src, tgt: (0, 2500)}
_getters = {
('distance', 'dps'): Distance2DpsGetter,
('distance', 'volley'): Distance2VolleyGetter,
@@ -96,6 +95,6 @@ class FitDamageStatsGraph(FitGraph):
('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)}
('distance', 'km'): lambda v, src, tgt: None if v is None else v / 1000,
('tgtSpeed', '%'): lambda v, src, tgt: v * 100 / tgt.getMaxVelocity(),
('tgtSigRad', '%'): lambda v, src, tgt: v * 100 / tgt.getSigRadius()}

View File

@@ -1,89 +0,0 @@
# =============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <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

View File

@@ -25,13 +25,13 @@ from graphs.data.base import SmoothPointGetter
class Time2SpeedGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxSpeed': fit.ship.getModifiedItemAttr('maxVelocity'),
'mass': fit.ship.getModifiedItemAttr('mass'),
'agility': fit.ship.getModifiedItemAttr('agility')}
'maxSpeed': src.getMaxVelocity(),
'mass': src.item.ship.getModifiedItemAttr('mass'),
'agility': src.item.ship.getModifiedItemAttr('agility')}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
maxSpeed = commonData['maxSpeed']
mass = commonData['mass']
@@ -43,13 +43,13 @@ class Time2SpeedGetter(SmoothPointGetter):
class Time2DistanceGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxSpeed': fit.ship.getModifiedItemAttr('maxVelocity'),
'mass': fit.ship.getModifiedItemAttr('mass'),
'agility': fit.ship.getModifiedItemAttr('agility')}
'maxSpeed': src.getMaxVelocity(),
'mass': src.item.ship.getModifiedItemAttr('mass'),
'agility': src.item.ship.getModifiedItemAttr('agility')}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
maxSpeed = commonData['maxSpeed']
mass = commonData['mass']

View File

@@ -41,4 +41,4 @@ class FitMobilityVsTimeGraph(FitGraph):
('time', 'speed'): Time2SpeedGetter,
('time', 'distance'): Time2DistanceGetter}
_denormalizers = {
('distance', 'km'): lambda v, fit, tgt: v / 1000}
('distance', 'km'): lambda v, src, tgt: v / 1000}

View File

@@ -25,12 +25,12 @@ from graphs.data.base import SmoothPointGetter
class Time2ShieldAmountGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxShieldAmount': fit.ship.getModifiedItemAttr('shieldCapacity'),
'shieldRegenTime': fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
'maxShieldAmount': src.item.ship.getModifiedItemAttr('shieldCapacity'),
'shieldRegenTime': src.item.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
shieldAmount = calculateShieldAmount(
maxShieldAmount=commonData['maxShieldAmount'],
@@ -41,12 +41,12 @@ class Time2ShieldAmountGetter(SmoothPointGetter):
class Time2ShieldRegenGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxShieldAmount': fit.ship.getModifiedItemAttr('shieldCapacity'),
'shieldRegenTime': fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
'maxShieldAmount': src.item.ship.getModifiedItemAttr('shieldCapacity'),
'shieldRegenTime': src.item.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
shieldAmount = calculateShieldAmount(
maxShieldAmount=commonData['maxShieldAmount'],
@@ -62,19 +62,19 @@ class Time2ShieldRegenGetter(SmoothPointGetter):
# Useless, but valid combination of x and y
class ShieldAmount2ShieldAmountGetter(SmoothPointGetter):
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
shieldAmount = x
return shieldAmount
class ShieldAmount2ShieldRegenGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxShieldAmount': fit.ship.getModifiedItemAttr('shieldCapacity'),
'shieldRegenTime': fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
'maxShieldAmount': src.item.ship.getModifiedItemAttr('shieldCapacity'),
'shieldRegenTime': src.item.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
shieldAmount = x
shieldRegen = calculateShieldRegen(
maxShieldAmount=commonData['maxShieldAmount'],

View File

@@ -46,15 +46,15 @@ class FitShieldRegenGraph(FitGraph):
# Calculation stuff
_normalizers = {
('shieldAmount', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('shieldCapacity')}
('shieldAmount', '%'): lambda v, src, tgt: v / 100 * src.item.ship.getModifiedItemAttr('shieldCapacity')}
_limiters = {
'shieldAmount': lambda fit, tgt: (0, fit.ship.getModifiedItemAttr('shieldCapacity'))}
'shieldAmount': lambda src, tgt: (0, src.item.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')}
('shieldAmount', '%'): lambda v, src, tgt: v * 100 / src.item.ship.getModifiedItemAttr('shieldCapacity'),
('shieldAmount', 'EHP'): lambda v, src, tgt: src.item.damagePattern.effectivify(src.item, v, 'shield'),
('shieldRegen', 'EHP/s'): lambda v, src, tgt: src.item.damagePattern.effectivify(src.item, v, 'shield')}

View File

@@ -24,9 +24,9 @@ from graphs.data.base import FitDataCache
class SubwarpSpeedCache(FitDataCache):
def getSubwarpSpeed(self, fit):
def getSubwarpSpeed(self, src):
try:
subwarpSpeed = self._data[fit.ID]
subwarpSpeed = self._data[src.item.ID]
except KeyError:
modStates = {}
disallowedGroups = (
@@ -40,34 +40,34 @@ class SubwarpSpeedCache(FitDataCache):
'Cynosural Field Generator',
'Clone Vat Bay',
'Jump Portal Generator')
for mod in fit.modules:
for mod in src.item.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)
for projFit in src.item.projectedFits:
projectionInfo = projFit.getProjectionInfo(src.item.ID)
if projectionInfo is not None and projectionInfo.active:
projFitStates[projectionInfo] = projectionInfo.active
projectionInfo.active = False
projModStates = {}
for mod in fit.projectedModules:
for mod in src.item.projectedModules:
if not mod.isExclusiveSystemEffect and mod.state >= FittingModuleState.ACTIVE:
projModStates[mod] = mod.state
mod.state = FittingModuleState.ONLINE
projDroneStates = {}
for drone in fit.projectedDrones:
for drone in src.item.projectedDrones:
if drone.amountActive > 0:
projDroneStates[drone] = drone.amountActive
drone.amountActive = 0
projFighterStates = {}
for fighter in fit.projectedFighters:
for fighter in src.item.projectedFighters:
if fighter.active:
projFighterStates[fighter] = fighter.active
fighter.active = False
fit.calculateModifiedAttributes()
subwarpSpeed = fit.ship.getModifiedItemAttr('maxVelocity')
self._data[fit.ID] = subwarpSpeed
src.item.calculateModifiedAttributes()
subwarpSpeed = src.getMaxVelocity()
self._data[src.item.ID] = subwarpSpeed
for projInfo, state in projFitStates.items():
projInfo.active = state
for mod, state in modStates.items():
@@ -78,5 +78,5 @@ class SubwarpSpeedCache(FitDataCache):
drone.amountActive = amountActive
for fighter, state in projFighterStates.items():
fighter.active = state
fit.calculateModifiedAttributes()
src.item.calculateModifiedAttributes()
return subwarpSpeed

View File

@@ -30,12 +30,12 @@ class Distance2TimeGetter(SmoothPointGetter):
_baseResolution = 500
def _getCommonData(self, miscParams, fit, tgt):
def _getCommonData(self, miscParams, src, tgt):
return {
'subwarpSpeed': self.graph._subspeedCache.getSubwarpSpeed(fit),
'warpSpeed': fit.warpSpeed}
'subwarpSpeed': self.graph._subspeedCache.getSubwarpSpeed(src),
'warpSpeed': src.item.warpSpeed}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
time = calculate_time_in_warp(
max_subwarp_speed=commonData['subwarpSpeed'],

View File

@@ -51,12 +51,12 @@ class FitWarpTimeGraph(FitGraph):
# Calculation stuff
_normalizers = {
('distance', 'AU'): lambda v, fit, tgt: v * AU_METERS,
('distance', 'km'): lambda v, fit, tgt: v * 1000}
('distance', 'AU'): lambda v, src, tgt: v * AU_METERS,
('distance', 'km'): lambda v, src, tgt: v * 1000}
_limiters = {
'distance': lambda fit, tgt: (0, fit.maxWarpDistance * AU_METERS)}
'distance': lambda src, tgt: (0, src.item.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}
('distance', 'AU'): lambda v, src, tgt: v / AU_METERS,
('distance', 'km'): lambda v, src, tgt: v / 1000}

View File

@@ -247,15 +247,20 @@ class GraphFrame(wx.Frame):
mainInput, miscInputs = self.ctrlPanel.getValues()
view = self.getView()
fits = self.ctrlPanel.fits
sources = self.ctrlPanel.sources
if view.hasTargets:
targets = self.ctrlPanel.targets
iterList = tuple(itertools.product(fits, targets))
iterList = tuple(itertools.product(sources, self.ctrlPanel.targets))
else:
iterList = tuple((f, None) for f in fits)
for fit, target in iterList:
iterList = tuple((f, None) for f in sources)
for source, target in iterList:
try:
xs, ys = view.getPlotPoints(mainInput, miscInputs, chosenX, chosenY, fit, target)
xs, ys = view.getPlotPoints(
mainInput=mainInput,
miscInputs=miscInputs,
xSpec=chosenX,
ySpec=chosenY,
src=source,
tgt=target)
# Figure out min and max Y
min_y_this = min(ys, default=None)
@@ -275,11 +280,11 @@ class GraphFrame(wx.Frame):
self.subplot.plot(xs, ys)
if target is None:
legend.append(self.getObjName(fit))
legend.append(source.shortName)
else:
legend.append('{} vs {}'.format(self.getObjName(fit), self.getObjName(target)))
legend.append('{} vs {}'.format(source.shortName, target.shortName))
except Exception as ex:
pyfalog.warning('Invalid values in "{0}"', fit.name)
pyfalog.warning('Invalid values in "{0}"', source.name)
self.canvas.draw()
self.Refresh()
return
@@ -330,12 +335,3 @@ class GraphFrame(wx.Frame):
self.canvas.draw()
self.Refresh()
@staticmethod
def getObjName(thing):
if isinstance(thing, Fit):
return '{} ({})'.format(thing.name, thing.ship.item.getShortName())
elif isinstance(thing, TargetProfile):
return thing.name
return ''

View File

@@ -23,12 +23,13 @@ import wx
import gui.display
from eos.saveddata.targetProfile import TargetProfile
from graphs.wrapper import SourceWrapper, TargetWrapper
from gui.contextMenu import ContextMenu
from service.const import GraphCacheCleanupReason
from service.fit import Fit
class BaseList(gui.display.Display):
class BaseWrapperList(gui.display.Display):
DEFAULT_COLS = (
'Base Icon',
@@ -37,7 +38,7 @@ class BaseList(gui.display.Display):
def __init__(self, graphFrame, parent):
super().__init__(parent)
self.graphFrame = graphFrame
self.fits = []
self._wrappers = []
self.hoveredRow = None
self.hoveredColumn = None
@@ -47,6 +48,15 @@ class BaseList(gui.display.Display):
self.Bind(wx.EVT_MOTION, self.OnMouseMove)
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
@property
def wrappers(self):
return sorted(self._wrappers, key=lambda w: w.isFit)
# UI-related stuff
@property
def defaultTTText(self):
raise NotImplementedError
def refreshExtraColumns(self, extraColSpecs):
baseColNames = set()
for baseColName in self.DEFAULT_COLS:
@@ -63,31 +73,13 @@ class BaseList(gui.display.Display):
self.appendColumnBySpec(colSpec)
self.refreshView()
def handleDrag(self, type, fitID):
if type == 'fit':
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit not in self.fits:
self.fits.append(fit)
self.updateView()
self.graphFrame.draw()
def refreshView(self):
self.refresh(self.wrappers)
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL:
self.selectAll()
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE:
self.removeListItems(self.getSelectedListItems())
event.Skip()
def OnLeftDClick(self, event):
row, _ = self.HitTest(event.Position)
item = self.getListItem(row)
if item is None:
return
self.removeListItems([item])
def updateView(self):
self.update(self.wrappers)
# UI event handling
def OnMouseMove(self, event):
row, _, col = self.HitTestSubItem(event.Position)
if row != self.hoveredRow or col != self.hoveredColumn:
@@ -97,7 +89,7 @@ class BaseList(gui.display.Display):
self.hoveredRow = row
self.hoveredColumn = col
if row != -1 and col != -1 and col < self.ColumnCount:
item = self.getListItem(row)
item = self.getWrapper(row)
if item is None:
return
tooltip = self.activeColumns[col].getToolTip(item)
@@ -115,70 +107,138 @@ class BaseList(gui.display.Display):
self.hoveredColumn = None
event.Skip()
# Fit events
def handleDrag(self, type, fitID):
if type == 'fit' and not self.containsFitID(fitID):
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
self.appendItem(fit)
self.updateView()
self.graphFrame.draw()
def OnLeftDClick(self, event):
row, _ = self.HitTest(event.Position)
wrapper = self.getWrapper(row)
if wrapper is None:
return
self.removeWrappers([wrapper])
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL:
self.selectAll()
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE:
self.removeWrappers(self.getSelectedWrappers())
event.Skip()
# Wrapper-related methods
@property
def wrapperClass(self):
raise NotImplementedError
def getWrapper(self, row):
if row == -1:
return None
try:
return self._wrappers[row]
except IndexError:
return None
def removeWrappers(self, wrappers):
wrappers = set(wrappers).union(self._wrappers)
if not wrappers:
return
for wrapper in wrappers:
self._wrappers.remove(wrapper)
self.updateView()
for wrapper in wrappers:
if wrapper.isFit:
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.fitRemoved, extraData=wrapper.fitID)
elif wrapper.isProfile:
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.profileRemoved, extraData=wrapper.profileID)
self.graphFrame.draw()
def getSelectedWrappers(self):
wrappers = []
for row in self.getSelectedRows():
wrapper = self.getWrapper(row)
if wrapper is None:
continue
wrappers.append(wrapper)
return wrappers
def appendItem(self, item):
self._wrappers.append(self.wrapperClass(item))
def containsFitID(self, fitID):
for wrapper in self._wrappers:
if wrapper.isFit and wrapper.itemID == fitID:
return True
return False
def containsProfileID(self, profileID):
for wrapper in self._wrappers:
if wrapper.isProfile and wrapper.itemID == profileID:
return True
return False
# Wrapper-related events
def OnFitRenamed(self, event):
if event.fitID in [f.ID for f in self.fits]:
if self.containsFitID(event.fitID):
self.updateView()
def OnFitChanged(self, event):
if set(event.fitIDs).union(f.ID for f in self.fits):
if set(event.fitIDs).union(w.itemID for w in self._wrappers if w.isFit):
self.updateView()
def OnFitRemoved(self, event):
fit = next((f for f in self.fits if f.ID == event.fitID), None)
if fit is not None:
self.fits.remove(fit)
wrapper = next((w for w in self._wrappers if w.isFit and w.itemID == event.fitID), None)
if wrapper is not None:
self._wrappers.remove(wrapper)
self.updateView()
@property
def defaultTTText(self):
raise NotImplementedError
def OnProfileRenamed(self, event):
if self.containsProfileID(event.profileID):
self.updateView()
def refreshView(self):
raise NotImplementedError
def OnProfileChanged(self, event):
if self.containsProfileID(event.profileID):
self.updateView()
def updateView(self):
raise NotImplementedError
def getListItem(self, row):
raise NotImplementedError
def removeListItems(self, items):
raise NotImplementedError
def getSelectedListItems(self):
items = []
for row in self.getSelectedRows():
item = self.getListItem(row)
if item is None:
continue
items.append(item)
return items
def OnProfileRemoved(self, event):
wrapper = next((w for w in self._wrappers if w.isProfile and w.itemID == event.profileID), None)
if wrapper is not None:
self._wrappers.remove(wrapper)
self.updateView()
# Context menu handlers
def addFit(self, fit):
if fit is None:
return
if fit in self.fits:
if self.containsFitID(fit.ID):
return
self.fits.append(fit)
self.appendItem(fit)
self.updateView()
self.graphFrame.draw()
def getExistingFitIDs(self):
return [f.ID for f in self.fits]
return [w.itemID for w in self._wrappers if w.isFit]
def addFitsByIDs(self, fitIDs):
sFit = Fit.getInstance()
for fitID in fitIDs:
if self.containsFitID(fitID):
continue
fit = sFit.getFit(fitID)
if fit is not None:
self.fits.append(fit)
self.appendItem(fit)
self.updateView()
self.graphFrame.draw()
class FitList(BaseList):
class SourceWrapperList(BaseWrapperList):
wrapperClass = SourceWrapper
def __init__(self, graphFrame, parent):
super().__init__(graphFrame, parent)
@@ -187,19 +247,13 @@ class FitList(BaseList):
fit = Fit.getInstance().getFit(self.graphFrame.mainFrame.getActiveFit())
if fit is not None:
self.fits.append(fit)
self.appendItem(fit)
self.updateView()
def refreshView(self):
self.refresh(self.fits)
def updateView(self):
self.update(self.fits)
def spawnMenu(self, event):
selection = self.getSelectedListItems()
selection = self.getSelectedWrappers()
clickedPos = self.getRowByAbs(event.Position)
mainItem = self.getListItem(clickedPos)
mainItem = self.getWrapper(clickedPos)
sourceContext = 'graphFitList'
itemContext = None if mainItem is None else 'Fit'
@@ -207,51 +261,27 @@ class FitList(BaseList):
if menu:
self.PopupMenu(menu)
def getListItem(self, row):
if row == -1:
return None
try:
return self.fits[row]
except IndexError:
return None
def removeListItems(self, items):
toRemove = [i for i in items if i in self.fits]
if not toRemove:
return
for fit in toRemove:
self.fits.remove(fit)
self.updateView()
for fit in toRemove:
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.fitRemoved, extraData=fit.ID)
self.graphFrame.draw()
@property
def defaultTTText(self):
return 'Drag a fit into this list to graph it'
class TargetList(BaseList):
class TargetWrapperList(BaseWrapperList):
wrapperClass = TargetWrapper
def __init__(self, graphFrame, parent):
super().__init__(graphFrame, parent)
self.Bind(wx.EVT_CONTEXT_MENU, self.spawnMenu)
self.profiles = []
self.profiles.append(TargetProfile.getIdeal())
self.appendItem(TargetProfile.getIdeal())
self.updateView()
def refreshView(self):
self.refresh(self.targets)
def updateView(self):
self.update(self.targets)
def spawnMenu(self, event):
selection = self.getSelectedListItems()
selection = self.getSelectedWrappers()
clickedPos = self.getRowByAbs(event.Position)
mainItem = self.getListItem(clickedPos)
mainItem = self.getWrapper(clickedPos)
sourceContext = 'graphTgtList'
itemContext = None if mainItem is None else 'Target'
@@ -259,56 +289,6 @@ class TargetList(BaseList):
if menu:
self.PopupMenu(menu)
def getListItem(self, row):
if row == -1:
return None
numFits = len(self.fits)
numProfiles = len(self.profiles)
if (numFits + numProfiles) == 0:
return None
if row < numFits:
return self.fits[row]
else:
return self.profiles[row - numFits]
def removeListItems(self, items):
fitsToRemove = [i for i in items if i in self.fits]
profilesToRemove = [i for i in items if i in self.profiles]
if not fitsToRemove and not profilesToRemove:
return
for fit in fitsToRemove:
self.fits.remove(fit)
for profile in profilesToRemove:
self.profiles.remove(profile)
self.updateView()
for fit in fitsToRemove:
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.fitRemoved, extraData=fit.ID)
for profile in profilesToRemove:
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.profileRemoved, extraData=profile.ID)
self.graphFrame.draw()
# Target profile events
def OnProfileRenamed(self, event):
if event.profileID in [tp.ID for tp in self.profiles]:
self.updateView()
def OnProfileChanged(self, event):
if event.profileID in [tp.ID for tp in self.profiles]:
self.updateView()
def OnProfileRemoved(self, event):
profile = next((tp for tp in self.profiles if tp.ID == event.profileID), None)
if profile is not None:
self.profiles.remove(profile)
self.updateView()
@property
def targets(self):
return self.fits + self.profiles
@property
def defaultTTText(self):
return 'Drag a fit into this list to have your fits graphed against it'
@@ -317,8 +297,8 @@ class TargetList(BaseList):
def addProfile(self, profile):
if profile is None:
return
if profile in self.profiles:
if self.containsProfileID(profile.ID):
return
self.profiles.append(profile)
self.appendItem(profile)
self.updateView()
self.graphFrame.draw()

View File

@@ -28,7 +28,7 @@ from gui.contextMenu import ContextMenu
from gui.utils.inputs import FloatBox, FloatRangeBox
from service.const import GraphCacheCleanupReason
from service.fit import Fit
from .lists import FitList, TargetList
from .lists import SourceWrapperList, TargetWrapperList
from .vector import VectorPicker
@@ -114,10 +114,10 @@ class GraphControlPanel(wx.Panel):
mainSizer.Add(optsSizer, 0, wx.EXPAND | wx.ALL, 10)
srcTgtSizer = wx.BoxSizer(wx.HORIZONTAL)
self.fitList = FitList(graphFrame, self)
self.fitList.SetMinSize((270, -1))
srcTgtSizer.Add(self.fitList, 1, wx.EXPAND | wx.ALL, 0)
self.targetList = TargetList(graphFrame, self)
self.sourceList = SourceWrapperList(graphFrame, self)
self.sourceList.SetMinSize((270, -1))
srcTgtSizer.Add(self.sourceList, 1, wx.EXPAND | wx.ALL, 0)
self.targetList = TargetWrapperList(graphFrame, self)
self.targetList.SetMinSize((270, -1))
srcTgtSizer.Add(self.targetList, 1, wx.EXPAND | wx.LEFT, 10)
mainSizer.Add(srcTgtSizer, 1, wx.EXPAND | wx.LEFT | wx.BOTTOM | wx.RIGHT, 10)
@@ -162,7 +162,7 @@ class GraphControlPanel(wx.Panel):
self.tgtVectorLabel.Show(False)
# Source and target list
self.fitList.refreshExtraColumns(view.srcExtraCols)
self.sourceList.refreshExtraColumns(view.srcExtraCols)
self.targetList.refreshExtraColumns(view.tgtExtraCols)
self.targetList.Show(view.hasTargets)
@@ -333,34 +333,37 @@ class GraphControlPanel(wx.Panel):
return self.xSubSelection.GetClientData(self.xSubSelection.GetSelection())
@property
def fits(self):
return self.fitList.fits
def sources(self):
return self.sourceList.wrappers
@property
def targets(self):
return self.targetList.targets
return self.targetList.wrappers
# Fit events
def OnFitRenamed(self, event):
self.fitList.OnFitRenamed(event)
self.sourceList.OnFitRenamed(event)
self.targetList.OnFitRenamed(event)
def OnFitChanged(self, event):
self.fitList.OnFitChanged(event)
self.sourceList.OnFitChanged(event)
self.targetList.OnFitChanged(event)
def OnFitRemoved(self, event):
self.fitList.OnFitRemoved(event)
self.sourceList.OnFitRemoved(event)
self.targetList.OnFitRemoved(event)
# Target profile events
def OnProfileRenamed(self, event):
self.sourceList.OnProfileRenamed(event)
self.targetList.OnProfileRenamed(event)
def OnProfileChanged(self, event):
self.sourceList.OnProfileChanged(event)
self.targetList.OnProfileChanged(event)
def OnProfileRemoved(self, event):
self.sourceList.OnProfileRemoved(event)
self.targetList.OnProfileRemoved(event)
def formatLabel(self, axisDef):

131
graphs/wrapper.py Normal file
View 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/>.
# =============================================================================
import math
from eos.saveddata.fit import Fit
from eos.saveddata.targetProfile import TargetProfile
class BaseWrapper:
def __init__(self, item):
self.item = item
@property
def isFit(self):
return isinstance(self.item, Fit)
@property
def isProfile(self):
return isinstance(self.item, TargetProfile)
@property
def itemID(self):
return self.item.ID
@property
def name(self):
if self.isFit:
return '{} ({})'.format(self.item.name, self.item.ship.item.name)
elif self.isProfile:
return self.item.name
return ''
@property
def shortName(self):
if self.isFit:
return '{} ({})'.format(self.item.name, self.item.ship.item.getShortName())
elif self.isProfile:
return self.item.name
return ''
def getMaxVelocity(self, extraMultipliers=None):
if self.isFit:
if extraMultipliers:
maxVelocity = self.item.ship.getModifiedItemAttrWithExtraMods('maxVelocity', extraMultipliers=extraMultipliers)
else:
maxVelocity = self.item.ship.getModifiedItemAttr('maxVelocity')
elif self.isProfile:
maxVelocity = self.item.maxVelocity
if extraMultipliers:
maxVelocity *= _calculateMultiplier(extraMultipliers)
else:
maxVelocity = None
return maxVelocity
def getSigRadius(self, extraMultipliers=None):
if self.isFit:
if extraMultipliers:
sigRadius = self.item.ship.getModifiedItemAttrWithExtraMods('signatureRadius', extraMultipliers=extraMultipliers)
else:
sigRadius = self.item.ship.getModifiedItemAttr('signatureRadius')
elif self.isProfile:
sigRadius = self.item.signatureRadius
if extraMultipliers:
sigRadius *= _calculateMultiplier(extraMultipliers)
else:
sigRadius = None
return sigRadius
def getRadius(self):
if self.isFit:
radius = self.item.ship.getModifiedItemAttr('radius')
elif self.isProfile:
radius = self.item.radius
else:
radius = None
return radius
class SourceWrapper(BaseWrapper):
pass
class TargetWrapper(BaseWrapper):
def __init__(self, item):
super().__init__(item=item)
self.resistMode = None
# 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

View File

@@ -161,7 +161,7 @@ class RemoveItem(ContextMenuCombined):
fitID=fitID, commandFitIDs=commandFitIDs))
def __handleGraphItem(self, callingWindow, mainItem, selection):
callingWindow.removeListItems(selection)
callingWindow.removeWrappers(selection)
RemoveItem.register()

View File

@@ -27,9 +27,10 @@ import gui.mainFrame
from eos.saveddata.fit import Fit
from eos.saveddata.targetProfile import TargetProfile
from eos.utils.spoolSupport import SpoolOptions, SpoolType
from graphs.wrapper import BaseWrapper
from gui.bitmap_loader import BitmapLoader
from gui.viewColumn import ViewColumn
from gui.utils.numberFormatter import formatAmount
from gui.viewColumn import ViewColumn
class GraphColumn(ViewColumn, metaclass=ABCMeta):
@@ -47,6 +48,8 @@ class GraphColumn(ViewColumn, metaclass=ABCMeta):
raise NotImplementedError
def getText(self, stuff):
if isinstance(stuff, BaseWrapper):
stuff = stuff.item
if isinstance(stuff, (Fit, TargetProfile)):
val, unit = self._getValue(stuff)
if val is None:
@@ -59,6 +62,8 @@ class GraphColumn(ViewColumn, metaclass=ABCMeta):
raise NotImplementedError
def getToolTip(self, stuff):
if isinstance(stuff, BaseWrapper):
stuff = stuff.item
if isinstance(stuff, (Fit, TargetProfile)):
return self._getFitTooltip()
return ''

View File

@@ -1,11 +1,13 @@
# noinspection PyPackageRequirements
import wx
from eos.saveddata.implant import Implant
from eos.saveddata.drone import Drone
from eos.saveddata.module import Module, Rack
from eos.saveddata.fit import Fit
from eos.saveddata.targetProfile import TargetProfile
from eos.const import FittingSlot
from eos.saveddata.drone import Drone
from eos.saveddata.fit import Fit
from eos.saveddata.implant import Implant
from eos.saveddata.module import Module, Rack
from eos.saveddata.targetProfile import TargetProfile
from graphs.wrapper import BaseWrapper
from gui.viewColumn import ViewColumn
@@ -21,6 +23,9 @@ class BaseIcon(ViewColumn):
self.shipImage = fittingView.imageList.GetImageIndex("ship_small", "gui")
def getImageId(self, stuff):
if isinstance(stuff, BaseWrapper):
stuff = stuff.item
if isinstance(stuff, Drone):
return -1
elif isinstance(stuff, Fit):

View File

@@ -21,19 +21,22 @@
# noinspection PyPackageRequirements
import wx
from logbook import Logger
import gui.mainFrame
from eos.const import FittingSlot
from eos.saveddata.cargo import Cargo
from eos.saveddata.implant import Implant
from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter
from eos.saveddata.module import Module, Rack
from eos.saveddata.fit import Fit, FitLite
from eos.saveddata.implant import Implant
from eos.saveddata.module import Module, Rack
from eos.saveddata.targetProfile import TargetProfile
from eos.const import FittingSlot
from graphs.wrapper import BaseWrapper
from gui.builtinContextMenus.envEffectAdd import AddEnvironmentEffect
from gui.viewColumn import ViewColumn
from service.fit import Fit as FitSvc
from service.market import Market
from gui.viewColumn import ViewColumn
from gui.builtinContextMenus.envEffectAdd import AddEnvironmentEffect
import gui.mainFrame
pyfalog = Logger(__name__)
@@ -50,6 +53,9 @@ class BaseName(ViewColumn):
self.projectedView = isinstance(fittingView, gui.builtinAdditionPanes.projectedView.ProjectedView)
def getText(self, stuff):
if isinstance(stuff, BaseWrapper):
stuff = stuff.item
if isinstance(stuff, Drone):
return "%dx %s" % (stuff.amount, stuff.item.name)
elif isinstance(stuff, Fighter):