Add RR graphs
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
6
graphs/data/fitDamageStats/cache/time.py
vendored
6
graphs/data/fitDamageStats/cache/time.py
vendored
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
205
graphs/data/fitRemoteReps/cache.py
Normal file
205
graphs/data/fitRemoteReps/cache.py
Normal 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]
|
||||||
41
graphs/data/fitRemoteReps/calc.py
Normal file
41
graphs/data/fitRemoteReps/calc.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 = {}
|
||||||
|
|||||||
Reference in New Issue
Block a user