diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py
index 4679b1631..d32f15476 100644
--- a/eos/saveddata/fit.py
+++ b/eos/saveddata/fit.py
@@ -1575,12 +1575,16 @@ class Fit:
if drone.amountActive > 0:
yield drone
- def activeFighterAbilityIter(self):
+ def activeFightersIter(self):
for fighter in self.fighters:
if fighter.active:
- for ability in fighter.abilities:
- if ability.active:
- yield fighter, ability
+ yield fighter
+
+ def activeFighterAbilityIter(self):
+ for fighter in self.activeFightersIter():
+ for ability in fighter.abilities:
+ if ability.active:
+ yield fighter, ability
def __deepcopy__(self, memo=None):
fitCopy = Fit()
diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py
index 2baeb8e35..7e42bb9f5 100644
--- a/eos/saveddata/module.py
+++ b/eos/saveddata/module.py
@@ -549,8 +549,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
for rrAmount in repAmountParams.values():
rrDuringCycle += rrAmount
rrFactor = 1 / (avgCycleTime / 1000)
- rrDuringCycle *= rrFactor
- return rrDuringCycle
+ rps = rrDuringCycle * rrFactor
+ return rps
def getSpoolData(self, spoolOptions=None):
weaponMultMax = self.getModifiedItemAttr("damageMultiplierBonusMax", 0)
diff --git a/graphs/data/fitDamageStats/cache/time.py b/graphs/data/fitDamageStats/cache/time.py
index 21bcbb3d9..4b31aae7b 100644
--- a/graphs/data/fitDamageStats/cache/time.py
+++ b/graphs/data/fitDamageStats/cache/time.py
@@ -173,7 +173,7 @@ class TimeCache(FitDataCache):
intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg
# Modules
- for mod in src.item.modules:
+ for mod in src.item.activeModulesIter():
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 src.item.drones:
+ for drone in src.item.activeDronesIter():
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 src.item.fighters:
+ for fighter in src.item.activeFightersIter():
if not fighter.isDealingDamage():
continue
cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True)
diff --git a/graphs/data/fitDamageStats/calc/application.py b/graphs/data/fitDamageStats/calc/application.py
index a104b2dfe..c1f93de49 100644
--- a/graphs/data/fitDamageStats/calc/application.py
+++ b/graphs/data/fitDamageStats/calc/application.py
@@ -30,7 +30,7 @@ from service.settings import GraphSettings
def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
applicationMap = {}
- for mod in src.item.modules:
+ for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
if mod.hardpoint == FittingHardpoint.TURRET:
@@ -74,7 +74,7 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
tgt=tgt,
distance=distance,
tgtSigRadius=tgtSigRadius)
- for drone in src.item.drones:
+ for drone in src.item.activeDronesIter():
if not drone.isDealingDamage():
continue
applicationMap[drone] = getDroneMult(
@@ -87,7 +87,7 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius)
- for fighter in src.item.fighters:
+ for fighter in src.item.activeFightersIter():
if not fighter.isDealingDamage():
continue
for ability in fighter.abilities:
@@ -112,8 +112,8 @@ def getTurretMult(mod, src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngl
atkSpeed=atkSpeed,
atkAngle=atkAngle,
atkRadius=src.getRadius(),
- atkOptimalRange=mod.maxRange,
- atkFalloffRange=mod.falloff,
+ atkOptimalRange=mod.maxRange or 0,
+ atkFalloffRange=mod.falloff or 0,
atkTracking=mod.getModifiedItemAttr('trackingSpeed'),
atkOptimalSigRadius=mod.getModifiedItemAttr('optimalSigRadius'),
distance=distance,
@@ -224,8 +224,8 @@ def getDroneMult(drone, src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAng
atkSpeed=min(atkSpeed, droneSpeed),
atkAngle=atkAngle,
atkRadius=droneRadius,
- atkOptimalRange=drone.maxRange,
- atkFalloffRange=drone.falloff,
+ atkOptimalRange=drone.maxRange or 0,
+ atkFalloffRange=drone.falloff or 0,
atkTracking=drone.getModifiedItemAttr('trackingSpeed'),
atkOptimalSigRadius=drone.getModifiedItemAttr('optimalSigRadius'),
distance=cthDistance,
diff --git a/graphs/data/fitDamageStats/getter.py b/graphs/data/fitDamageStats/getter.py
index 280ac3d93..ec5916301 100644
--- a/graphs/data/fitDamageStats/getter.py
+++ b/graphs/data/fitDamageStats/getter.py
@@ -51,15 +51,15 @@ class YDpsMixin:
# Compose map ourselves using current fit settings if time is not specified
dpsMap = {}
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
- for mod in src.item.modules:
+ for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
dpsMap[mod] = mod.getDps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))
- for drone in src.item.drones:
+ for drone in src.item.activeDronesIter():
if not drone.isDealingDamage():
continue
dpsMap[drone] = drone.getDps()
- for fighter in src.item.fighters:
+ for fighter in src.item.activeFightersIter():
if not fighter.isDealingDamage():
continue
for effectID, effectDps in fighter.getDpsPerEffect().items():
@@ -85,15 +85,15 @@ class YVolleyMixin:
# Compose map ourselves using current fit settings if time is not specified
volleyMap = {}
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
- for mod in src.item.modules:
+ for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
volleyMap[mod] = mod.getVolley(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))
- for drone in src.item.drones:
+ for drone in src.item.activeDronesIter():
if not drone.isDealingDamage():
continue
volleyMap[drone] = drone.getVolley()
- for fighter in src.item.fighters:
+ for fighter in src.item.activeFightersIter():
if not fighter.isDealingDamage():
continue
for effectID, effectVolley in fighter.getVolleyPerEffect().items():
diff --git a/graphs/data/fitRemoteReps/cache.py b/graphs/data/fitRemoteReps/cache.py
new file mode 100644
index 000000000..3841340fe
--- /dev/null
+++ b/graphs/data/fitRemoteReps/cache.py
@@ -0,0 +1,205 @@
+# =============================================================================
+# Copyright (C) 2010 Diego Duclos
+#
+# This file is part of pyfa.
+#
+# pyfa is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# pyfa is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with pyfa. If not, see .
+# =============================================================================
+
+
+from copy import copy
+
+from eos.utils.float import floatUnerr
+from eos.utils.spoolSupport import SpoolOptions, SpoolType
+from eos.utils.stats import RRTypes
+from graphs.data.base import FitDataCache
+
+
+class TimeCache(FitDataCache):
+
+ # Whole data getters
+ def getRpsData(self, src):
+ """Return RPS data in {time: {key: rps}} format."""
+ return self._data[src.item.ID]['finalRps']
+
+ def getRepAmountData(self, src):
+ """Return rep amount data in {time: {key: amount}} format."""
+ return self._data[src.item.ID]['finalRepAmount']
+
+ # Specific data point getters
+ def getRpsDataPoint(self, src, time):
+ """Get RPS data by specified time in {key: rps} format."""
+ return self._getDataPoint(src=src, time=time, dataFunc=self.getRpsData)
+
+ def getRepAmountDataPoint(self, src, time):
+ """Get rep amount data by specified time in {key: amount} format."""
+ return self._getDataPoint(src=src, time=time, dataFunc=self.getRepAmountData)
+
+ # Preparation functions
+ def prepareRpsData(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(src=src, maxTime=maxTime)
+ fitCache = self._data[src.item.ID]
+ # Final cache has been generated already, don't do anything
+ if 'finalRps' in fitCache:
+ return
+ # Convert cache from segments with assigned values into points
+ # which are located at times when rps value changes
+ pointCache = {}
+ for key, rpsList in fitCache['internalRps'].items():
+ pointData = pointCache[key] = {}
+ prevRps = None
+ prevTimeEnd = None
+ for timeStart, timeEnd, rps in rpsList:
+ # First item
+ if not pointData:
+ pointData[timeStart] = rps
+ # Gap between items
+ elif floatUnerr(prevTimeEnd) < floatUnerr(timeStart):
+ pointData[prevTimeEnd] = RRTypes(0, 0, 0, 0)
+ pointData[timeStart] = rps
+ # Changed value
+ elif rps != prevRps:
+ pointData[timeStart] = rps
+ prevRps = rps
+ prevTimeEnd = timeEnd
+ # We have data in another form, do not need old one any longer
+ del fitCache['internalRps']
+ changesByTime = {}
+ for key, rpsMap in pointCache.items():
+ for time in rpsMap:
+ changesByTime.setdefault(time, []).append(key)
+ # Here we convert cache to following format:
+ # {time: {key: rps}
+ finalRpsCache = fitCache['finalRps'] = {}
+ timeRpsData = {}
+ for time in sorted(changesByTime):
+ timeRpsData = copy(timeRpsData)
+ for key in changesByTime[time]:
+ timeRpsData[key] = pointCache[key][time]
+ finalRpsCache[time] = timeRpsData
+
+ def prepareRepAmountData(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(src=src, maxTime=maxTime)
+ fitCache = self._data[src.item.ID]
+ # Final cache has been generated already, don't do anything
+ if 'finalRepAmount' in fitCache:
+ return
+ intCache = fitCache['internalRepAmount']
+ changesByTime = {}
+ for key, remAmountMap in intCache.items():
+ for time in remAmountMap:
+ changesByTime.setdefault(time, []).append(key)
+ # Here we convert cache to following format:
+ # {time: {key: hp repaired by key at this time}}
+ finalCache = fitCache['finalRepAmount'] = {}
+ timeRepAmountData = {}
+ for time in sorted(changesByTime):
+ timeRepAmountData = copy(timeRepAmountData)
+ for key in changesByTime[time]:
+ keyRepAmount = intCache[key][time]
+ if key in timeRepAmountData:
+ timeRepAmountData[key] = timeRepAmountData[key] + keyRepAmount
+ else:
+ timeRepAmountData[key] = keyRepAmount
+ finalCache[time] = timeRepAmountData
+ # We do not need internal cache once we have final
+ del fitCache['internalRepAmount']
+
+ # Private stuff
+ def _generateInternalForm(self, src, maxTime):
+ if self._isTimeCacheValid(src=src, maxTime=maxTime):
+ return
+ fitCache = self._data[src.item.ID] = {'maxTime': maxTime}
+ intCacheRps = fitCache['internalRps'] = {}
+ intCacheRepAmount = fitCache['internalRepAmount'] = {}
+
+ def addRps(rrKey, addedTimeStart, addedTimeFinish, addedRepAmounts):
+ if not addedRepAmounts:
+ return
+ repAmountSum = sum(addedRepAmounts, RRTypes(0, 0, 0, 0))
+ if repAmountSum.shield > 0 or repAmountSum.armor > 0 or repAmountSum.hull > 0:
+ addedRps = repAmountSum / (addedTimeFinish - addedTimeStart)
+ rrCacheRps = intCacheRps.setdefault(rrKey, [])
+ rrCacheRps.append((addedTimeStart, addedTimeFinish, addedRps))
+
+ def addRepAmount(rrKey, addedTime, addedRepAmount):
+ if addedRepAmount.shield > 0 or addedRepAmount.armor > 0 or addedRepAmount.hull > 0:
+ intCacheRepAmount.setdefault(rrKey, {})[addedTime] = addedRepAmount
+
+ # Modules
+ for mod in src.item.activeModulesIter():
+ if not mod.isRemoteRepping():
+ continue
+ cycleParams = mod.getCycleParameters(reloadOverride=True)
+ if cycleParams is None:
+ continue
+ currentTime = 0
+ nonstopCycles = 0
+ for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles():
+ cycleRepAmounts = []
+ repAmountParams = mod.getRepAmountParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
+ for repTimeMs, repAmount in repAmountParams.items():
+ cycleRepAmounts.append(repAmount)
+ addRepAmount(mod, currentTime + repTimeMs / 1000, repAmount)
+ addRps(mod, currentTime, currentTime + cycleTimeMs / 1000, cycleRepAmounts)
+ if inactiveTimeMs > 0:
+ nonstopCycles = 0
+ else:
+ nonstopCycles += 1
+ if currentTime > maxTime:
+ break
+ currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
+ # Drones
+ for drone in src.item.activeDronesIter():
+ if not drone.isRemoteRepping():
+ continue
+ cycleParams = drone.getCycleParameters(reloadOverride=True)
+ if cycleParams is None:
+ continue
+ currentTime = 0
+ repAmountParams = drone.getRepAmountParameters()
+ for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles():
+ cycleRepAmounts = []
+ for repTimeMs, repAmount in repAmountParams.items():
+ cycleRepAmounts.append(repAmount)
+ addRepAmount(drone, currentTime + repTimeMs / 1000, repAmount)
+ addRps(drone, currentTime, currentTime + cycleTimeMs / 1000, cycleRepAmounts)
+ if currentTime > maxTime:
+ break
+ currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
+
+ def _isTimeCacheValid(self, src, maxTime):
+ try:
+ cacheMaxTime = self._data[src.item.ID]['maxTime']
+ except KeyError:
+ return False
+ return maxTime <= cacheMaxTime
+
+ def _getDataPoint(self, src, time, dataFunc):
+ data = dataFunc(src)
+ timesBefore = [t for t in data if floatUnerr(t) <= floatUnerr(time)]
+ try:
+ time = max(timesBefore)
+ except ValueError:
+ return {}
+ else:
+ return data[time]
diff --git a/graphs/data/fitRemoteReps/calc.py b/graphs/data/fitRemoteReps/calc.py
new file mode 100644
index 000000000..74350dc55
--- /dev/null
+++ b/graphs/data/fitRemoteReps/calc.py
@@ -0,0 +1,41 @@
+# =============================================================================
+# Copyright (C) 2010 Diego Duclos
+#
+# This file is part of pyfa.
+#
+# pyfa is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# pyfa is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with pyfa. If not, see .
+# =============================================================================
+
+
+from eos.utils.float import floatUnerr
+from graphs.calc import calculateRangeFactor
+
+
+def getApplicationPerKey(src, distance):
+ applicationMap = {}
+ for mod in src.item.activeModulesIter():
+ if not mod.isRemoteRepping():
+ continue
+ applicationMap[mod] = 1 if distance is None else calculateRangeFactor(
+ srcOptimalRange=mod.maxRange or 0,
+ srcFalloffRange=mod.falloff or 0,
+ distance=distance)
+ for drone in src.item.activeDronesIter():
+ if not drone.isRemoteRepping():
+ continue
+ applicationMap[drone] = 1 if distance is None or distance <= src.item.extraAttributes['droneControlRange'] else 0
+ # Ensure consistent results - round off a little to avoid float errors
+ for k, v in applicationMap.items():
+ applicationMap[k] = floatUnerr(v)
+ return applicationMap
diff --git a/graphs/data/fitRemoteReps/getter.py b/graphs/data/fitRemoteReps/getter.py
index c5015c81f..5e99ebbf3 100644
--- a/graphs/data/fitRemoteReps/getter.py
+++ b/graphs/data/fitRemoteReps/getter.py
@@ -18,5 +18,169 @@
# =============================================================================
-from graphs.data.base import SmoothPointGetter
+import eos.config
+from eos.utils.spoolSupport import SpoolOptions, SpoolType
+from eos.utils.stats import RRTypes
+from graphs.data.base import PointGetter, SmoothPointGetter
+from .calc import getApplicationPerKey
+
+def applyReps(rrMap, applicationMap):
+ totalAmount = RRTypes(shield=0, armor=0, hull=0, capacitor=0)
+ for key, repAmount in rrMap.items():
+ totalAmount += repAmount * applicationMap.get(key, 0)
+ # We do not want to include energy transfers into final value
+ totalReps = totalAmount.shield + totalAmount.armor + totalAmount.hull
+ return totalReps
+
+
+# Y mixins
+class YRpsMixin:
+
+ def _getRepsPerKey(self, src, time):
+ # Use data from time cache if time was not specified
+ if time is not None:
+ return self._getTimeCacheDataPoint(src=src, time=time)
+ # Compose map ourselves using current fit settings if time is not specified
+ rpsMap = {}
+ defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
+ for mod in src.item.activeModulesIter():
+ if not mod.isRemoteRepping():
+ continue
+ rpsMap[mod] = mod.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))
+ for drone in src.item.activeDronesIter():
+ if not drone.isRemoteRepping():
+ continue
+ rpsMap[drone] = drone.getRemoteReps()
+ return rpsMap
+
+ def _prepareTimeCache(self, src, maxTime):
+ self.graph._timeCache.prepareRpsData(src=src, maxTime=maxTime)
+
+ def _getTimeCacheData(self, src):
+ return self.graph._timeCache.getRpsData(src=src)
+
+ def _getTimeCacheDataPoint(self, src, time):
+ return self.graph._timeCache.getRpsDataPoint(src=src, time=time)
+
+
+class YRepAmountMixin:
+
+ def _getRepsPerKey(self, src, time):
+ # Total reps given makes no sense without time specified
+ if time is None:
+ raise ValueError
+ return self._getTimeCacheDataPoint(src=src, time=time)
+
+ def _prepareTimeCache(self, src, maxTime):
+ self.graph._timeCache.prepareRepAmountData(src=src, maxTime=maxTime)
+
+ def _getTimeCacheData(self, src):
+ return self.graph._timeCache.getRepAmountData(src=src)
+
+ def _getTimeCacheDataPoint(self, src, time):
+ return self.graph._timeCache.getRepAmountDataPoint(src=src, time=time)
+
+
+# X mixins
+class XDistanceMixin(SmoothPointGetter):
+
+ _baseResolution = 50
+ _extraDepth = 2
+
+ def _getCommonData(self, miscParams, src, tgt):
+ # Prepare time cache here because we need to do it only once,
+ # and this function is called once per point info fetch
+ self._prepareTimeCache(src=src, maxTime=miscParams['time'])
+ return {'rrMap': self._getRepsPerKey(src=src, time=miscParams['time'])}
+
+ def _calculatePoint(self, x, miscParams, src, tgt, commonData):
+ distance = x
+ applicationMap = getApplicationPerKey(src=src, distance=distance)
+ y = applyReps(
+ rrMap=commonData['rrMap'],
+ applicationMap=applicationMap)
+ return y
+
+
+class XTimeMixin(PointGetter):
+
+ def getRange(self, xRange, miscParams, src, tgt):
+ xs = []
+ ys = []
+ minTime, maxTime = xRange
+ # Prepare time cache and various shared data
+ self._prepareTimeCache(src=src, maxTime=maxTime)
+ timeCache = self._getTimeCacheData(src=src)
+ applicationMap = getApplicationPerKey(src=src, distance=miscParams['distance'])
+ # Custom iteration for time graph to show all data points
+ currentRepAmount = None
+ currentTime = None
+ for currentTime in sorted(timeCache):
+ prevRepAmount = currentRepAmount
+ currentRepAmountData = timeCache[currentTime]
+ currentRepAmount = applyReps(rrMap=currentRepAmountData, applicationMap=applicationMap)
+ if currentTime < minTime:
+ continue
+ # First set of data points
+ if not xs:
+ # Start at exactly requested time, at last known value
+ initialRepAmount = prevRepAmount or 0
+ xs.append(minTime)
+ ys.append(initialRepAmount)
+ # If current time is bigger then starting, extend plot to that time with old value
+ if currentTime > minTime:
+ xs.append(currentTime)
+ ys.append(initialRepAmount)
+ # If new value is different, extend it with new point to the new value
+ if currentRepAmount != prevRepAmount:
+ xs.append(currentTime)
+ ys.append(currentRepAmount)
+ continue
+ # Last data point
+ if currentTime >= maxTime:
+ xs.append(maxTime)
+ ys.append(prevRepAmount)
+ break
+ # Anything in-between
+ if currentRepAmount != prevRepAmount:
+ if prevRepAmount is not None:
+ xs.append(currentTime)
+ ys.append(prevRepAmount)
+ xs.append(currentTime)
+ ys.append(currentRepAmount)
+ # Special case - there are no remote reppers
+ if currentRepAmount 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(currentRepAmount or 0)
+ return xs, ys
+
+ def getPoint(self, x, miscParams, src, tgt):
+ time = x
+ # Prepare time cache and various data
+ self._prepareTimeCache(src=src, maxTime=time)
+ repAmountData = self._getTimeCacheDataPoint(src=src, time=time)
+ applicationMap = getApplicationPerKey(src=src, distance=miscParams['distance'])
+ y = applyReps(rrMap=repAmountData, applicationMap=applicationMap)
+ return y
+
+
+# Final getters
+class Distance2RpsGetter(XDistanceMixin, YRpsMixin):
+ pass
+
+
+class Distance2RepAmountGetter(XDistanceMixin, YRepAmountMixin):
+ pass
+
+
+class Time2RpsGetter(XTimeMixin, YRpsMixin):
+ pass
+
+
+class Time2RepAmountGetter(XTimeMixin, YRepAmountMixin):
+ pass
diff --git a/graphs/data/fitRemoteReps/graph.py b/graphs/data/fitRemoteReps/graph.py
index e299e44ac..15fa83fad 100644
--- a/graphs/data/fitRemoteReps/graph.py
+++ b/graphs/data/fitRemoteReps/graph.py
@@ -19,10 +19,26 @@
from graphs.data.base import FitGraph, XDef, YDef, Input
+from service.const import GraphCacheCleanupReason
+from .cache import TimeCache
+from .getter import Distance2RpsGetter, Distance2RepAmountGetter, Time2RpsGetter, Time2RepAmountGetter
class FitRemoteRepsGraph(FitGraph):
+ def __init__(self):
+ super().__init__()
+ self._timeCache = TimeCache()
+
+ def _clearInternalCache(self, reason, extraData):
+ # Here, we care only about fit changes, graph changes and option switches
+ # - Input changes are irrelevant as time cache cares only about
+ # time input, and it regenerates once time goes beyond cached value
+ if reason in (GraphCacheCleanupReason.fitChanged, GraphCacheCleanupReason.fitRemoved):
+ self._timeCache.clearForFit(extraData)
+ elif reason == GraphCacheCleanupReason.graphSwitched:
+ self._timeCache.clearAll()
+
# UI stuff
internalName = 'remoteRepsGraph'
name = 'Remote Repairs'
@@ -33,12 +49,16 @@ class FitRemoteRepsGraph(FitGraph):
YDef(handle='rps', unit='HP/s', label='Repair speed'),
YDef(handle='total', unit='HP', label='Total repaired')]
inputs = [
- Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), secondaryTooltip='When set, uses repairing ship\'s exact RR stats at a given time\nWhen not set, uses attacker\'s RR stats as shown in stats panel of main window'),
+ Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), secondaryTooltip='When set, uses repairing ship\'s exact RR stats at a given time\nWhen not set, uses repairing ship\'s RR 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 repairing ship and the target, as seen in overview (surface-to-surface)', secondaryTooltip='Distance between the repairing ship and the target, as seen in overview (surface-to-surface)')]
srcExtraCols = ('ShieldRR', 'ArmorRR', 'HullRR')
# Calculation stuff
_normalizers = {('distance', 'km'): lambda v, src, tgt: None if v is None else v * 1000}
_limiters = {'time': lambda src, tgt: (0, 2500)}
- _getters = {}
+ _getters = {
+ ('distance', 'rps'): Distance2RpsGetter,
+ ('distance', 'total'): Distance2RepAmountGetter,
+ ('time', 'rps'): Time2RpsGetter,
+ ('time', 'total'): Time2RepAmountGetter}
_denormalizers = {('distance', 'km'): lambda v, src, tgt: None if v is None else v / 1000}
diff --git a/graphs/data/fitWarpTime/cache.py b/graphs/data/fitWarpTime/cache.py
index d8d1eda52..25b705096 100644
--- a/graphs/data/fitWarpTime/cache.py
+++ b/graphs/data/fitWarpTime/cache.py
@@ -40,8 +40,8 @@ class SubwarpSpeedCache(FitDataCache):
'Cynosural Field Generator',
'Clone Vat Bay',
'Jump Portal Generator')
- for mod in src.item.modules:
- if mod.item is not None and mod.item.group.name in disallowedGroups and mod.state >= FittingModuleState.ACTIVE:
+ for mod in src.item.activeModulesIter():
+ if mod.item is not None and mod.item.group.name in disallowedGroups:
modStates[mod] = mod.state
mod.state = FittingModuleState.ONLINE
projFitStates = {}