Add RR graphs

This commit is contained in:
DarkPhoenix
2019-08-18 21:27:20 +03:00
parent 3c967ba9eb
commit edd261c677
10 changed files with 461 additions and 27 deletions

View File

@@ -1575,12 +1575,16 @@ class Fit:
if drone.amountActive > 0: if drone.amountActive > 0:
yield drone yield drone
def activeFighterAbilityIter(self): def activeFightersIter(self):
for fighter in self.fighters: for fighter in self.fighters:
if fighter.active: if fighter.active:
for ability in fighter.abilities: yield fighter
if ability.active:
yield fighter, ability def activeFighterAbilityIter(self):
for fighter in self.activeFightersIter():
for ability in fighter.abilities:
if ability.active:
yield fighter, ability
def __deepcopy__(self, memo=None): def __deepcopy__(self, memo=None):
fitCopy = Fit() fitCopy = Fit()

View File

@@ -549,8 +549,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
for rrAmount in repAmountParams.values(): for rrAmount in repAmountParams.values():
rrDuringCycle += rrAmount rrDuringCycle += rrAmount
rrFactor = 1 / (avgCycleTime / 1000) rrFactor = 1 / (avgCycleTime / 1000)
rrDuringCycle *= rrFactor rps = rrDuringCycle * rrFactor
return rrDuringCycle return rps
def getSpoolData(self, spoolOptions=None): def getSpoolData(self, spoolOptions=None):
weaponMultMax = self.getModifiedItemAttr("damageMultiplierBonusMax", 0) weaponMultMax = self.getModifiedItemAttr("damageMultiplierBonusMax", 0)

View File

@@ -173,7 +173,7 @@ class TimeCache(FitDataCache):
intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg
# Modules # Modules
for mod in src.item.modules: for mod in src.item.activeModulesIter():
if not mod.isDealingDamage(): if not mod.isDealingDamage():
continue continue
cycleParams = mod.getCycleParameters(reloadOverride=True) cycleParams = mod.getCycleParameters(reloadOverride=True)
@@ -196,7 +196,7 @@ class TimeCache(FitDataCache):
break break
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
# Drones # Drones
for drone in src.item.drones: for drone in src.item.activeDronesIter():
if not drone.isDealingDamage(): if not drone.isDealingDamage():
continue continue
cycleParams = drone.getCycleParameters(reloadOverride=True) cycleParams = drone.getCycleParameters(reloadOverride=True)
@@ -214,7 +214,7 @@ class TimeCache(FitDataCache):
break break
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
# Fighters # Fighters
for fighter in src.item.fighters: for fighter in src.item.activeFightersIter():
if not fighter.isDealingDamage(): if not fighter.isDealingDamage():
continue continue
cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True) cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True)

View File

@@ -30,7 +30,7 @@ from service.settings import GraphSettings
def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
applicationMap = {} applicationMap = {}
for mod in src.item.modules: for mod in src.item.activeModulesIter():
if not mod.isDealingDamage(): if not mod.isDealingDamage():
continue continue
if mod.hardpoint == FittingHardpoint.TURRET: if mod.hardpoint == FittingHardpoint.TURRET:
@@ -74,7 +74,7 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
tgt=tgt, tgt=tgt,
distance=distance, distance=distance,
tgtSigRadius=tgtSigRadius) tgtSigRadius=tgtSigRadius)
for drone in src.item.drones: for drone in src.item.activeDronesIter():
if not drone.isDealingDamage(): if not drone.isDealingDamage():
continue continue
applicationMap[drone] = getDroneMult( applicationMap[drone] = getDroneMult(
@@ -87,7 +87,7 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
tgtSpeed=tgtSpeed, tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle, tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius) tgtSigRadius=tgtSigRadius)
for fighter in src.item.fighters: for fighter in src.item.activeFightersIter():
if not fighter.isDealingDamage(): if not fighter.isDealingDamage():
continue continue
for ability in fighter.abilities: for ability in fighter.abilities:
@@ -112,8 +112,8 @@ def getTurretMult(mod, src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngl
atkSpeed=atkSpeed, atkSpeed=atkSpeed,
atkAngle=atkAngle, atkAngle=atkAngle,
atkRadius=src.getRadius(), atkRadius=src.getRadius(),
atkOptimalRange=mod.maxRange, atkOptimalRange=mod.maxRange or 0,
atkFalloffRange=mod.falloff, atkFalloffRange=mod.falloff or 0,
atkTracking=mod.getModifiedItemAttr('trackingSpeed'), atkTracking=mod.getModifiedItemAttr('trackingSpeed'),
atkOptimalSigRadius=mod.getModifiedItemAttr('optimalSigRadius'), atkOptimalSigRadius=mod.getModifiedItemAttr('optimalSigRadius'),
distance=distance, distance=distance,
@@ -224,8 +224,8 @@ def getDroneMult(drone, src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAng
atkSpeed=min(atkSpeed, droneSpeed), atkSpeed=min(atkSpeed, droneSpeed),
atkAngle=atkAngle, atkAngle=atkAngle,
atkRadius=droneRadius, atkRadius=droneRadius,
atkOptimalRange=drone.maxRange, atkOptimalRange=drone.maxRange or 0,
atkFalloffRange=drone.falloff, atkFalloffRange=drone.falloff or 0,
atkTracking=drone.getModifiedItemAttr('trackingSpeed'), atkTracking=drone.getModifiedItemAttr('trackingSpeed'),
atkOptimalSigRadius=drone.getModifiedItemAttr('optimalSigRadius'), atkOptimalSigRadius=drone.getModifiedItemAttr('optimalSigRadius'),
distance=cthDistance, distance=cthDistance,

View File

@@ -51,15 +51,15 @@ class YDpsMixin:
# Compose map ourselves using current fit settings if time is not specified # Compose map ourselves using current fit settings if time is not specified
dpsMap = {} dpsMap = {}
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
for mod in src.item.modules: for mod in src.item.activeModulesIter():
if not mod.isDealingDamage(): if not mod.isDealingDamage():
continue continue
dpsMap[mod] = mod.getDps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)) 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(): if not drone.isDealingDamage():
continue continue
dpsMap[drone] = drone.getDps() dpsMap[drone] = drone.getDps()
for fighter in src.item.fighters: for fighter in src.item.activeFightersIter():
if not fighter.isDealingDamage(): if not fighter.isDealingDamage():
continue continue
for effectID, effectDps in fighter.getDpsPerEffect().items(): 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 # Compose map ourselves using current fit settings if time is not specified
volleyMap = {} volleyMap = {}
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
for mod in src.item.modules: for mod in src.item.activeModulesIter():
if not mod.isDealingDamage(): if not mod.isDealingDamage():
continue continue
volleyMap[mod] = mod.getVolley(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)) 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(): if not drone.isDealingDamage():
continue continue
volleyMap[drone] = drone.getVolley() volleyMap[drone] = drone.getVolley()
for fighter in src.item.fighters: for fighter in src.item.activeFightersIter():
if not fighter.isDealingDamage(): if not fighter.isDealingDamage():
continue continue
for effectID, effectVolley in fighter.getVolleyPerEffect().items(): for effectID, effectVolley in fighter.getVolleyPerEffect().items():

View File

@@ -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 <http://www.gnu.org/licenses/>.
# =============================================================================
from copy import copy
from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import SpoolOptions, SpoolType
from eos.utils.stats import 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]

View File

@@ -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 <http://www.gnu.org/licenses/>.
# =============================================================================
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

View File

@@ -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

View File

@@ -19,10 +19,26 @@
from graphs.data.base import FitGraph, XDef, YDef, Input 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): 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 # UI stuff
internalName = 'remoteRepsGraph' internalName = 'remoteRepsGraph'
name = 'Remote Repairs' name = 'Remote Repairs'
@@ -33,12 +49,16 @@ class FitRemoteRepsGraph(FitGraph):
YDef(handle='rps', unit='HP/s', label='Repair speed'), YDef(handle='rps', unit='HP/s', label='Repair speed'),
YDef(handle='total', unit='HP', label='Total repaired')] YDef(handle='total', unit='HP', label='Total repaired')]
inputs = [ 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)')] 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') srcExtraCols = ('ShieldRR', 'ArmorRR', 'HullRR')
# Calculation stuff # Calculation stuff
_normalizers = {('distance', 'km'): lambda v, src, tgt: None if v is None else v * 1000} _normalizers = {('distance', 'km'): lambda v, src, tgt: None if v is None else v * 1000}
_limiters = {'time': lambda src, tgt: (0, 2500)} _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} _denormalizers = {('distance', 'km'): lambda v, src, tgt: None if v is None else v / 1000}

View File

@@ -40,8 +40,8 @@ class SubwarpSpeedCache(FitDataCache):
'Cynosural Field Generator', 'Cynosural Field Generator',
'Clone Vat Bay', 'Clone Vat Bay',
'Jump Portal Generator') 'Jump Portal Generator')
for mod in src.item.modules: for mod in src.item.activeModulesIter():
if mod.item is not None and mod.item.group.name in disallowedGroups and mod.state >= FittingModuleState.ACTIVE: if mod.item is not None and mod.item.group.name in disallowedGroups:
modStates[mod] = mod.state modStates[mod] = mod.state
mod.state = FittingModuleState.ONLINE mod.state = FittingModuleState.ONLINE
projFitStates = {} projFitStates = {}