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 = {}