Reorganize graph folder structure
This commit is contained in:
22
graphs/data/fitDamageStats/cache/__init__.py
vendored
Normal file
22
graphs/data/fitDamageStats/cache/__init__.py
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# =============================================================================
|
||||
# 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 .projected import ProjectedDataCache
|
||||
from .time import TimeCache
|
||||
131
graphs/data/fitDamageStats/cache/projected.py
vendored
Normal file
131
graphs/data/fitDamageStats/cache/projected.py
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from eos.const import FittingModuleState
|
||||
from eos.modifiedAttributeDict import getResistanceAttrID
|
||||
from graphs.data.base import FitDataCache
|
||||
|
||||
|
||||
ModProjData = namedtuple('ModProjData', ('boost', 'optimal', 'falloff', 'stackingGroup', 'resAttrID'))
|
||||
MobileProjData = namedtuple('MobileProjData', ('boost', 'optimal', 'falloff', 'stackingGroup', 'resAttrID', 'speed', 'radius'))
|
||||
|
||||
|
||||
class ProjectedDataCache(FitDataCache):
|
||||
|
||||
def getProjModData(self, fit):
|
||||
try:
|
||||
projectedData = self._data[fit.ID]['modules']
|
||||
except KeyError:
|
||||
# Format of items for both: (boost strength, optimal, falloff, stacking group, resistance attr ID)
|
||||
webMods = []
|
||||
tpMods = []
|
||||
projectedData = self._data.setdefault(fit.ID, {})['modules'] = (webMods, tpMods)
|
||||
for mod in fit.modules:
|
||||
if mod.state <= FittingModuleState.ONLINE:
|
||||
continue
|
||||
for webEffectName in ('remoteWebifierFalloff', 'structureModuleEffectStasisWebifier'):
|
||||
if webEffectName in mod.item.effects:
|
||||
webMods.append(ModProjData(
|
||||
mod.getModifiedItemAttr('speedFactor'),
|
||||
mod.maxRange or 0,
|
||||
mod.falloff or 0,
|
||||
'default',
|
||||
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects[webEffectName])))
|
||||
if 'doomsdayAOEWeb' in mod.item.effects:
|
||||
webMods.append(ModProjData(
|
||||
mod.getModifiedItemAttr('speedFactor'),
|
||||
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - fit.ship.getModifiedItemAttr('radius')),
|
||||
mod.falloff or 0,
|
||||
'default',
|
||||
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects['doomsdayAOEWeb'])))
|
||||
for tpEffectName in ('remoteTargetPaintFalloff', 'structureModuleEffectTargetPainter'):
|
||||
if tpEffectName in mod.item.effects:
|
||||
tpMods.append(ModProjData(
|
||||
mod.getModifiedItemAttr('signatureRadiusBonus'),
|
||||
mod.maxRange or 0,
|
||||
mod.falloff or 0,
|
||||
'default',
|
||||
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects[tpEffectName])))
|
||||
if 'doomsdayAOEPaint' in mod.item.effects:
|
||||
tpMods.append(ModProjData(
|
||||
mod.getModifiedItemAttr('signatureRadiusBonus'),
|
||||
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - fit.ship.getModifiedItemAttr('radius')),
|
||||
mod.falloff or 0,
|
||||
'default',
|
||||
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects['doomsdayAOEPaint'])))
|
||||
return projectedData
|
||||
|
||||
def getProjDroneData(self, fit):
|
||||
try:
|
||||
projectedData = self._data[fit.ID]['drones']
|
||||
except KeyError:
|
||||
# Format of items for both: (boost strength, optimal, falloff, stacking group, resistance attr ID, drone speed, drone radius)
|
||||
webDrones = []
|
||||
tpDrones = []
|
||||
projectedData = self._data.setdefault(fit.ID, {})['drones'] = (webDrones, tpDrones)
|
||||
for drone in fit.drones:
|
||||
if drone.amountActive <= 0:
|
||||
continue
|
||||
if 'remoteWebifierEntity' in drone.item.effects:
|
||||
webDrones.extend(drone.amountActive * (MobileProjData(
|
||||
drone.getModifiedItemAttr('speedFactor'),
|
||||
drone.maxRange or 0,
|
||||
drone.falloff or 0,
|
||||
'default',
|
||||
getResistanceAttrID(modifyingItem=drone, effect=drone.item.effects['remoteWebifierEntity']),
|
||||
drone.getModifiedItemAttr('maxVelocity'),
|
||||
drone.getModifiedItemAttr('radius')),))
|
||||
if 'remoteTargetPaintEntity' in drone.item.effects:
|
||||
tpDrones.extend(drone.amountActive * (MobileProjData(
|
||||
drone.getModifiedItemAttr('signatureRadiusBonus'),
|
||||
drone.maxRange or 0,
|
||||
drone.falloff or 0,
|
||||
'default',
|
||||
getResistanceAttrID(modifyingItem=drone, effect=drone.item.effects['remoteTargetPaintEntity']),
|
||||
drone.getModifiedItemAttr('maxVelocity'),
|
||||
drone.getModifiedItemAttr('radius')),))
|
||||
return projectedData
|
||||
|
||||
def getProjFighterData(self, fit):
|
||||
try:
|
||||
projectedData = self._data[fit.ID]['fighters']
|
||||
except KeyError:
|
||||
# Format of items for both: (boost strength, optimal, falloff, stacking group, resistance attr ID, fighter speed, fighter radius)
|
||||
webFighters = []
|
||||
tpFighters = []
|
||||
projectedData = self._data.setdefault(fit.ID, {})['fighters'] = (webFighters, tpFighters)
|
||||
for fighter in fit.fighters:
|
||||
if not fighter.active:
|
||||
continue
|
||||
for ability in fighter.abilities:
|
||||
if not ability.active:
|
||||
continue
|
||||
if ability.effect.name == 'fighterAbilityStasisWebifier':
|
||||
webFighters.append(MobileProjData(
|
||||
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierSpeedPenalty') * fighter.amountActive,
|
||||
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierOptimalRange'),
|
||||
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierFalloffRange'),
|
||||
'default',
|
||||
getResistanceAttrID(modifyingItem=fighter, effect=fighter.item.effects['fighterAbilityStasisWebifier']),
|
||||
fighter.getModifiedItemAttr('maxVelocity'),
|
||||
fighter.getModifiedItemAttr('radius')))
|
||||
return projectedData
|
||||
254
graphs/data/fitDamageStats/cache/time.py
vendored
Normal file
254
graphs/data/fitDamageStats/cache/time.py
vendored
Normal file
@@ -0,0 +1,254 @@
|
||||
# =============================================================================
|
||||
# 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 DmgTypes
|
||||
from graphs.data.base import FitDataCache
|
||||
|
||||
|
||||
class TimeCache(FitDataCache):
|
||||
|
||||
# Whole data getters
|
||||
def getDpsData(self, fit):
|
||||
"""Return DPS data in {time: {key: dps}} format."""
|
||||
return self._data[fit.ID]['finalDps']
|
||||
|
||||
def getVolleyData(self, fit):
|
||||
"""Return volley data in {time: {key: volley}} format."""
|
||||
return self._data[fit.ID]['finalVolley']
|
||||
|
||||
def getDmgData(self, fit):
|
||||
"""Return inflicted damage data in {time: {key: damage}} format."""
|
||||
return self._data[fit.ID]['finalDmg']
|
||||
|
||||
# Specific data point getters
|
||||
def getDpsDataPoint(self, fit, time):
|
||||
"""Get DPS data by specified time in {key: dps} format."""
|
||||
return self._getDataPoint(fit, time, self.getDpsData)
|
||||
|
||||
def getVolleyDataPoint(self, fit, time):
|
||||
"""Get volley data by specified time in {key: volley} format."""
|
||||
return self._getDataPoint(fit, time, self.getVolleyData)
|
||||
|
||||
def getDmgDataPoint(self, fit, time):
|
||||
"""Get inflicted damage data by specified time in {key: dmg} format."""
|
||||
return self._getDataPoint(fit, time, self.getDmgData)
|
||||
|
||||
# Preparation functions
|
||||
def prepareDpsData(self, fit, maxTime):
|
||||
self._prepareDpsVolleyData(fit, maxTime)
|
||||
|
||||
def prepareVolleyData(self, fit, maxTime):
|
||||
self._prepareDpsVolleyData(fit, maxTime)
|
||||
|
||||
def prepareDmgData(self, fit, maxTime):
|
||||
# Time is none means that time parameter has to be ignored,
|
||||
# we do not need cache for that
|
||||
if maxTime is None:
|
||||
return
|
||||
self._generateInternalForm(fit, maxTime)
|
||||
fitCache = self._data[fit.ID]
|
||||
# Final cache has been generated already, don't do anything
|
||||
if 'finalDmg' in fitCache:
|
||||
return
|
||||
intCache = fitCache['internalDmg']
|
||||
changesByTime = {}
|
||||
for key, dmgMap in intCache.items():
|
||||
for time in dmgMap:
|
||||
changesByTime.setdefault(time, []).append(key)
|
||||
# Here we convert cache to following format:
|
||||
# {time: {key: damage done by key at this time}}
|
||||
finalCache = fitCache['finalDmg'] = {}
|
||||
timeDmgData = {}
|
||||
for time in sorted(changesByTime):
|
||||
timeDmgData = copy(timeDmgData)
|
||||
for key in changesByTime[time]:
|
||||
keyDmg = intCache[key][time]
|
||||
if key in timeDmgData:
|
||||
timeDmgData[key] = timeDmgData[key] + keyDmg
|
||||
else:
|
||||
timeDmgData[key] = keyDmg
|
||||
finalCache[time] = timeDmgData
|
||||
# We do not need internal cache once we have final
|
||||
del fitCache['internalDmg']
|
||||
|
||||
# Private stuff
|
||||
def _prepareDpsVolleyData(self, fit, maxTime):
|
||||
# Time is none means that time parameter has to be ignored,
|
||||
# we do not need cache for that
|
||||
if maxTime is None:
|
||||
return True
|
||||
self._generateInternalForm(fit, maxTime)
|
||||
fitCache = self._data[fit.ID]
|
||||
# Final cache has been generated already, don't do anything
|
||||
if 'finalDps' in fitCache and 'finalVolley' in fitCache:
|
||||
return
|
||||
# Convert cache from segments with assigned values into points
|
||||
# which are located at times when dps/volley values change
|
||||
pointCache = {}
|
||||
for key, dmgList in fitCache['internalDpsVolley'].items():
|
||||
pointData = pointCache[key] = {}
|
||||
prevDps = None
|
||||
prevVolley = None
|
||||
prevTimeEnd = None
|
||||
for timeStart, timeEnd, dps, volley in dmgList:
|
||||
# First item
|
||||
if not pointData:
|
||||
pointData[timeStart] = (dps, volley)
|
||||
# Gap between items
|
||||
elif floatUnerr(prevTimeEnd) < floatUnerr(timeStart):
|
||||
pointData[prevTimeEnd] = (DmgTypes(0, 0, 0, 0), DmgTypes(0, 0, 0, 0))
|
||||
pointData[timeStart] = (dps, volley)
|
||||
# Changed value
|
||||
elif dps != prevDps or volley != prevVolley:
|
||||
pointData[timeStart] = (dps, volley)
|
||||
prevDps = dps
|
||||
prevVolley = volley
|
||||
prevTimeEnd = timeEnd
|
||||
# We have data in another form, do not need old one any longer
|
||||
del fitCache['internalDpsVolley']
|
||||
changesByTime = {}
|
||||
for key, dmgMap in pointCache.items():
|
||||
for time in dmgMap:
|
||||
changesByTime.setdefault(time, []).append(key)
|
||||
# Here we convert cache to following format:
|
||||
# {time: {key: (dps, volley}}
|
||||
finalDpsCache = fitCache['finalDps'] = {}
|
||||
finalVolleyCache = fitCache['finalVolley'] = {}
|
||||
timeDpsData = {}
|
||||
timeVolleyData = {}
|
||||
for time in sorted(changesByTime):
|
||||
timeDpsData = copy(timeDpsData)
|
||||
timeVolleyData = copy(timeVolleyData)
|
||||
for key in changesByTime[time]:
|
||||
dps, volley = pointCache[key][time]
|
||||
timeDpsData[key] = dps
|
||||
timeVolleyData[key] = volley
|
||||
finalDpsCache[time] = timeDpsData
|
||||
finalVolleyCache[time] = timeVolleyData
|
||||
|
||||
def _generateInternalForm(self, fit, maxTime):
|
||||
if self._isTimeCacheValid(fit, maxTime):
|
||||
return
|
||||
fitCache = self._data[fit.ID] = {'maxTime': maxTime}
|
||||
intCacheDpsVolley = fitCache['internalDpsVolley'] = {}
|
||||
intCacheDmg = fitCache['internalDmg'] = {}
|
||||
|
||||
def addDpsVolley(ddKey, addedTimeStart, addedTimeFinish, addedVolleys):
|
||||
if not addedVolleys:
|
||||
return
|
||||
volleySum = sum(addedVolleys, DmgTypes(0, 0, 0, 0))
|
||||
if volleySum.total > 0:
|
||||
addedDps = volleySum / (addedTimeFinish - addedTimeStart)
|
||||
# We can take "just best" volley, no matter target resistances, because all
|
||||
# known items have the same damage type ratio throughout their cycle - and
|
||||
# applying resistances doesn't change final outcome
|
||||
bestVolley = max(addedVolleys, key=lambda v: v.total)
|
||||
ddCacheDps = intCacheDpsVolley.setdefault(ddKey, [])
|
||||
ddCacheDps.append((addedTimeStart, addedTimeFinish, addedDps, bestVolley))
|
||||
|
||||
def addDmg(ddKey, addedTime, addedDmg):
|
||||
if addedDmg.total == 0:
|
||||
return
|
||||
intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg
|
||||
|
||||
# Modules
|
||||
for mod in fit.modules:
|
||||
if not mod.isDealingDamage():
|
||||
continue
|
||||
cycleParams = mod.getCycleParameters(reloadOverride=True)
|
||||
if cycleParams is None:
|
||||
continue
|
||||
currentTime = 0
|
||||
nonstopCycles = 0
|
||||
for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles():
|
||||
cycleVolleys = []
|
||||
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
|
||||
for volleyTimeMs, volley in volleyParams.items():
|
||||
cycleVolleys.append(volley)
|
||||
addDmg(mod, currentTime + volleyTimeMs / 1000, volley)
|
||||
addDpsVolley(mod, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
|
||||
if inactiveTimeMs > 0:
|
||||
nonstopCycles = 0
|
||||
else:
|
||||
nonstopCycles += 1
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
|
||||
# Drones
|
||||
for drone in fit.drones:
|
||||
if not drone.isDealingDamage():
|
||||
continue
|
||||
cycleParams = drone.getCycleParameters(reloadOverride=True)
|
||||
if cycleParams is None:
|
||||
continue
|
||||
currentTime = 0
|
||||
volleyParams = drone.getVolleyParameters()
|
||||
for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles():
|
||||
cycleVolleys = []
|
||||
for volleyTimeMs, volley in volleyParams.items():
|
||||
cycleVolleys.append(volley)
|
||||
addDmg(drone, currentTime + volleyTimeMs / 1000, volley)
|
||||
addDpsVolley(drone, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
|
||||
# Fighters
|
||||
for fighter in fit.fighters:
|
||||
if not fighter.isDealingDamage():
|
||||
continue
|
||||
cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True)
|
||||
if cycleParams is None:
|
||||
continue
|
||||
volleyParams = fighter.getVolleyParametersPerEffect()
|
||||
for effectID, abilityCycleParams in cycleParams.items():
|
||||
if effectID not in volleyParams:
|
||||
continue
|
||||
currentTime = 0
|
||||
abilityVolleyParams = volleyParams[effectID]
|
||||
for cycleTimeMs, inactiveTimeMs in abilityCycleParams.iterCycles():
|
||||
cycleVolleys = []
|
||||
for volleyTimeMs, volley in abilityVolleyParams.items():
|
||||
cycleVolleys.append(volley)
|
||||
addDmg((fighter, effectID), currentTime + volleyTimeMs / 1000, volley)
|
||||
addDpsVolley((fighter, effectID), currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
|
||||
|
||||
def _isTimeCacheValid(self, fit, maxTime):
|
||||
try:
|
||||
cacheMaxTime = self._data[fit.ID]['maxTime']
|
||||
except KeyError:
|
||||
return False
|
||||
return maxTime <= cacheMaxTime
|
||||
|
||||
def _getDataPoint(self, fit, time, dataFunc):
|
||||
data = dataFunc(fit)
|
||||
timesBefore = [t for t in data if floatUnerr(t) <= floatUnerr(time)]
|
||||
try:
|
||||
time = max(timesBefore)
|
||||
except ValueError:
|
||||
return {}
|
||||
else:
|
||||
return data[time]
|
||||
Reference in New Issue
Block a user