# ============================================================================= # 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 . # ============================================================================= import eos.config from eos.const import FittingHardpoint from eos.utils.spoolSupport import SpoolType, SpoolOptions from eos.utils.stats import DmgTypes from gui.builtinGraphs.base import FitGraph, XDef, YDef, Input, VectorDef from service.const import GraphCacheCleanupReason from service.settings import GraphSettings from .calc import ( getTurretMult, getLauncherMult, getDroneMult, getFighterAbilityMult, getSmartbombMult, getDoomsdayMult, getBombMult, getGuidedBombMult, getWebbedSpeed, getTpMult) from .helper import getTgtMaxVelocity, getTgtSigRadius from .projectedCache import ProjectedDataCache from .timeCache import TimeCache class FitDamageStatsGraph(FitGraph): def __init__(self): super().__init__() self._timeCache = TimeCache() self._projectedCache = ProjectedDataCache() def _clearInternalCache(self, reason, extraData): # Here, we care only about fit changes and graph changes. # - Input changes are irrelevant as time cache cares only about # time input, and it regenerates once time goes beyond cached value # - Option changes are irrelevant as cache contains "raw" damage # values which do not rely on any graph options if reason in (GraphCacheCleanupReason.fitChanged, GraphCacheCleanupReason.fitRemoved): self._timeCache.clearForFit(extraData) self._projectedCache.clearForFit(extraData) elif reason == GraphCacheCleanupReason.graphSwitched: self._timeCache.clearAll() self._projectedCache.clearAll() # UI stuff internalName = 'dmgStatsGraph' name = 'Damage Stats' xDefs = [ XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km')), XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')), XDef(handle='tgtSpeed', unit='m/s', label='Target speed', mainInput=('tgtSpeed', '%')), XDef(handle='tgtSpeed', unit='%', label='Target speed', mainInput=('tgtSpeed', '%')), XDef(handle='tgtSigRad', unit='m', label='Target signature radius', mainInput=('tgtSigRad', '%')), XDef(handle='tgtSigRad', unit='%', label='Target signature radius', mainInput=('tgtSigRad', '%'))] yDefs = [ YDef(handle='dps', unit=None, label='DPS'), YDef(handle='volley', unit=None, label='Volley'), YDef(handle='damage', unit=None, label='Damage inflicted')] inputs = [ Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), mainOnly=False), Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=50, defaultRange=(0, 100), mainOnly=False), Input(handle='tgtSpeed', unit='%', label='Target speed', iconID=1389, defaultValue=100, defaultRange=(0, 100), mainOnly=False), Input(handle='tgtSigRad', unit='%', label='Target signature', iconID=1390, defaultValue=100, defaultRange=(100, 200), mainOnly=True)] srcVectorDef = VectorDef(lengthHandle='atkSpeed', lengthUnit='%', angleHandle='atkAngle', angleUnit='degrees', label='Attacker') tgtVectorDef = VectorDef(lengthHandle='tgtSpeed', lengthUnit='%', angleHandle='tgtAngle', angleUnit='degrees', label='Target') hasTargets = True srcExtraCols = ('Dps', 'Volley', 'Speed', 'Radius') tgtExtraCols = ('Speed', 'SigRadius', 'Radius') # Calculation stuff _normalizers = { ('distance', 'km'): lambda v, fit, tgt: v * 1000, ('atkSpeed', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('maxVelocity'), ('tgtSpeed', '%'): lambda v, fit, tgt: v / 100 * getTgtMaxVelocity(tgt), ('tgtSigRad', '%'): lambda v, fit, tgt: v / 100 * getTgtSigRadius(tgt)} _limiters = { 'time': lambda fit, tgt: (0, 2500)} _denormalizers = { ('distance', 'km'): lambda v, fit, tgt: v / 1000, ('tgtSpeed', '%'): lambda v, fit, tgt: v * 100 / getTgtMaxVelocity(tgt), ('tgtSigRad', '%'): lambda v, fit, tgt: v * 100 / getTgtSigRadius(tgt)} def _distance2dps(self, mainInput, miscInputs, fit, tgt): return self._xDistanceGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, dmgFunc=self._getDpsPerKey, timeCachePrepFunc=self._timeCache.prepareDpsData) def _distance2volley(self, mainInput, miscInputs, fit, tgt): return self._xDistanceGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, dmgFunc=self._getVolleyPerKey, timeCachePrepFunc=self._timeCache.prepareVolleyData) def _distance2damage(self, mainInput, miscInputs, fit, tgt): return self._xDistanceGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, dmgFunc=self._getDmgPerKey, timeCachePrepFunc=self._timeCache.prepareDmgData) def _time2dps(self, mainInput, miscInputs, fit, tgt): return self._xTimeGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, timeCachePrepFunc=self._timeCache.prepareDpsData, timeCacheGetFunc=self._timeCache.getDpsData) def _time2volley(self, mainInput, miscInputs, fit, tgt): return self._xTimeGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, timeCachePrepFunc=self._timeCache.prepareVolleyData, timeCacheGetFunc=self._timeCache.getVolleyData) def _time2damage(self, mainInput, miscInputs, fit, tgt): return self._xTimeGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, timeCachePrepFunc=self._timeCache.prepareDmgData, timeCacheGetFunc=self._timeCache.getDmgData) def _tgtSpeed2dps(self, mainInput, miscInputs, fit, tgt): return self._xTgtSpeedGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, dmgFunc=self._getDpsPerKey, timeCachePrepFunc=self._timeCache.prepareDpsData) def _tgtSpeed2volley(self, mainInput, miscInputs, fit, tgt): return self._xTgtSpeedGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, dmgFunc=self._getVolleyPerKey, timeCachePrepFunc=self._timeCache.prepareVolleyData) def _tgtSpeed2damage(self, mainInput, miscInputs, fit, tgt): return self._xTgtSpeedGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, dmgFunc=self._getDmgPerKey, timeCachePrepFunc=self._timeCache.prepareDmgData) def _tgtSigRad2dps(self, mainInput, miscInputs, fit, tgt): return self._xTgtSigRadiusGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, dmgFunc=self._getDpsPerKey, timeCachePrepFunc=self._timeCache.prepareDpsData) def _tgtSigRad2volley(self, mainInput, miscInputs, fit, tgt): return self._xTgtSigRadiusGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, dmgFunc=self._getVolleyPerKey, timeCachePrepFunc=self._timeCache.prepareVolleyData) def _tgtSigRad2damage(self, mainInput, miscInputs, fit, tgt): return self._xTgtSigRadiusGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, dmgFunc=self._getDmgPerKey, timeCachePrepFunc=self._timeCache.prepareDmgData) _getters = { ('distance', 'dps'): _distance2dps, ('distance', 'volley'): _distance2volley, ('distance', 'damage'): _distance2damage, ('time', 'dps'): _time2dps, ('time', 'volley'): _time2volley, ('time', 'damage'): _time2damage, ('tgtSpeed', 'dps'): _tgtSpeed2dps, ('tgtSpeed', 'volley'): _tgtSpeed2volley, ('tgtSpeed', 'damage'): _tgtSpeed2damage, ('tgtSigRad', 'dps'): _tgtSigRad2dps, ('tgtSigRad', 'volley'): _tgtSigRad2volley, ('tgtSigRad', 'damage'): _tgtSigRad2damage} # Point getter helpers def _xDistanceGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCachePrepFunc): xs = [] ys = [] applyProjected = GraphSettings.getInstance().get('applyProjected') # Process inputs into more convenient form miscInputMap = dict(miscInputs) # Get all data we need for all distances into maps/caches timeCachePrepFunc(fit, miscInputMap['time']) dmgMap = dmgFunc(fit=fit, time=miscInputMap['time']) # Go through distances and calculate distance-dependent data for distance in self._iterLinear(mainInput[1]): tgtSpeed = miscInputMap['tgtSpeed'] tgtSigRadius = getTgtSigRadius(tgt) if applyProjected: webMods, tpMods = self._projectedCache.getProjModData(fit) webDrones, tpDrones = self._projectedCache.getProjDroneData(fit) webFighters, tpFighters = self._projectedCache.getProjFighterData(fit) tgtSpeed = getWebbedSpeed( fit=fit, tgt=tgt, currentUnwebbedSpeed=tgtSpeed, webMods=webMods, webDrones=webDrones, webFighters=webFighters, distance=distance) tgtSigRadius = tgtSigRadius * getTpMult( fit=fit, tgt=tgt, tgtSpeed=tgtSpeed, tpMods=tpMods, tpDrones=tpDrones, tpFighters=tpFighters, distance=distance) applicationMap = self._getApplicationPerKey( fit=fit, tgt=tgt, atkSpeed=miscInputMap['atkSpeed'], atkAngle=miscInputMap['atkAngle'], distance=distance, tgtSpeed=tgtSpeed, tgtAngle=miscInputMap['tgtAngle'], tgtSigRadius=tgtSigRadius) dmg = self._aggregate(dmgMap=dmgMap, applicationMap=applicationMap).total xs.append(distance) ys.append(dmg) return xs, ys def _xTimeGetter(self, mainInput, miscInputs, fit, tgt, timeCachePrepFunc, timeCacheGetFunc): xs = [] ys = [] minTime, maxTime = mainInput[1] # Process inputs into more convenient form miscInputMap = dict(miscInputs) tgtSpeed = miscInputMap['tgtSpeed'] tgtSigRadius = getTgtSigRadius(tgt) if GraphSettings.getInstance().get('applyProjected'): webMods, tpMods = self._projectedCache.getProjModData(fit) webDrones, tpDrones = self._projectedCache.getProjDroneData(fit) webFighters, tpFighters = self._projectedCache.getProjFighterData(fit) tgtSpeed = getWebbedSpeed( fit=fit, tgt=tgt, currentUnwebbedSpeed=tgtSpeed, webMods=webMods, webDrones=webDrones, webFighters=webFighters, distance=miscInputMap['distance']) tgtSigRadius = tgtSigRadius * getTpMult( fit=fit, tgt=tgt, tgtSpeed=tgtSpeed, tpMods=tpMods, tpDrones=tpDrones, tpFighters=tpFighters, distance=miscInputMap['distance']) # Get all data we need for all times into maps/caches applicationMap = self._getApplicationPerKey( fit=fit, tgt=tgt, atkSpeed=miscInputMap['atkSpeed'], atkAngle=miscInputMap['atkAngle'], distance=miscInputMap['distance'], tgtSpeed=tgtSpeed, tgtAngle=miscInputMap['tgtAngle'], tgtSigRadius=tgtSigRadius) timeCachePrepFunc(fit, maxTime) timeCache = timeCacheGetFunc(fit) # Custom iteration for time graph to show all data points currentDmg = None currentTime = None for currentTime in sorted(timeCache): prevDmg = currentDmg currentDmgData = timeCache[currentTime] currentDmg = self._aggregate(dmgMap=currentDmgData, applicationMap=applicationMap).total if currentTime < minTime: continue # First set of data points if not xs: # Start at exactly requested time, at last known value initialDmg = prevDmg or 0 xs.append(minTime) ys.append(initialDmg) # If current time is bigger then starting, extend plot to that time with old value if currentTime > minTime: xs.append(currentTime) ys.append(initialDmg) # If new value is different, extend it with new point to the new value if currentDmg != prevDmg: xs.append(currentTime) ys.append(currentDmg) continue # Last data point if currentTime >= maxTime: xs.append(maxTime) ys.append(prevDmg) break # Anything in-between if currentDmg != prevDmg: if prevDmg is not None: xs.append(currentTime) ys.append(prevDmg) xs.append(currentTime) ys.append(currentDmg) if maxTime > (currentTime or 0): xs.append(maxTime) ys.append(currentDmg or 0) return xs, ys def _xTgtSpeedGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCachePrepFunc): xs = [] ys = [] applyProjected = GraphSettings.getInstance().get('applyProjected') # Process inputs into more convenient form miscInputMap = dict(miscInputs) # Get all data we need for all target speeds into maps/caches timeCachePrepFunc(fit, miscInputMap['time']) dmgMap = dmgFunc(fit=fit, time=miscInputMap['time']) # Go through target speeds and calculate distance-dependent data for tgtSpeed in self._iterLinear(mainInput[1]): # Get separate internal speed to calculate proper application, for graph # itself we still want to show pre-modification speed on X axis tgtSpeedInternal = tgtSpeed tgtSigRadius = getTgtSigRadius(tgt) if applyProjected: webMods, tpMods = self._projectedCache.getProjModData(fit) webDrones, tpDrones = self._projectedCache.getProjDroneData(fit) webFighters, tpFighters = self._projectedCache.getProjFighterData(fit) tgtSpeedInternal = getWebbedSpeed( fit=fit, tgt=tgt, currentUnwebbedSpeed=tgtSpeedInternal, webMods=webMods, webDrones=webDrones, webFighters=webFighters, distance=miscInputMap['distance']) tgtSigRadius = tgtSigRadius * getTpMult( fit=fit, tgt=tgt, tgtSpeed=tgtSpeedInternal, tpMods=tpMods, tpDrones=tpDrones, tpFighters=tpFighters, distance=miscInputMap['distance']) applicationMap = self._getApplicationPerKey( fit=fit, tgt=tgt, atkSpeed=miscInputMap['atkSpeed'], atkAngle=miscInputMap['atkAngle'], distance=miscInputMap['distance'], tgtSpeed=tgtSpeedInternal, tgtAngle=miscInputMap['tgtAngle'], tgtSigRadius=tgtSigRadius) dmg = self._aggregate(dmgMap=dmgMap, applicationMap=applicationMap).total xs.append(tgtSpeed) ys.append(dmg) return xs, ys def _xTgtSigRadiusGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCachePrepFunc): xs = [] ys = [] # Process inputs into more convenient form miscInputMap = dict(miscInputs) tgtSpeed = miscInputMap['tgtSpeed'] tgtSigMult = 1 if GraphSettings.getInstance().get('applyProjected'): webMods, tpMods = self._projectedCache.getProjModData(fit) webDrones, tpDrones = self._projectedCache.getProjDroneData(fit) webFighters, tpFighters = self._projectedCache.getProjFighterData(fit) tgtSpeed = getWebbedSpeed( fit=fit, tgt=tgt, currentUnwebbedSpeed=tgtSpeed, webMods=webMods, webDrones=webDrones, webFighters=webFighters, distance=miscInputMap['distance']) tgtSigMult = getTpMult( fit=fit, tgt=tgt, tgtSpeed=tgtSpeed, tpMods=tpMods, tpDrones=tpDrones, tpFighters=tpFighters, distance=miscInputMap['distance']) # Get all data we need for all target speeds into maps/caches timeCachePrepFunc(fit, miscInputMap['time']) dmgMap = dmgFunc(fit=fit, time=miscInputMap['time']) # Go through target speeds and calculate distance-dependent data for tgtSigRadius in self._iterLinear(mainInput[1]): # Separate variable to show base signature on X axis and use modified # signature in calculations tgtSigRadiusInternal = tgtSigRadius * tgtSigMult applicationMap = self._getApplicationPerKey( fit=fit, tgt=tgt, atkSpeed=miscInputMap['atkSpeed'], atkAngle=miscInputMap['atkAngle'], distance=miscInputMap['distance'], tgtSpeed=tgtSpeed, tgtAngle=miscInputMap['tgtAngle'], tgtSigRadius=tgtSigRadiusInternal) dmg = self._aggregate(dmgMap=dmgMap, applicationMap=applicationMap).total xs.append(tgtSigRadius) ys.append(dmg) return xs, ys # Damage data per key getters def _getDpsPerKey(self, fit, time): # Use data from time cache if time was not specified if time is not None: return self._timeCache.getDpsDataPoint(fit, time) # Compose map ourselves using current fit settings if time is not specified dpsMap = {} defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] for mod in fit.modules: if not mod.isDealingDamage(): continue dpsMap[mod] = mod.getDps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)) for drone in fit.drones: if not drone.isDealingDamage(): continue dpsMap[drone] = drone.getDps() for fighter in fit.fighters: if not fighter.isDealingDamage(): continue for effectID, effectDps in fighter.getDpsPerEffect().items(): dpsMap[(fighter, effectID)] = effectDps return dpsMap def _getVolleyPerKey(self, fit, time): # Use data from time cache if time was not specified if time is not None: return self._timeCache.getVolleyDataPoint(fit, time) # Compose map ourselves using current fit settings if time is not specified volleyMap = {} defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] for mod in fit.modules: if not mod.isDealingDamage(): continue volleyMap[mod] = mod.getVolley(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)) for drone in fit.drones: if not drone.isDealingDamage(): continue volleyMap[drone] = drone.getVolley() for fighter in fit.fighters: if not fighter.isDealingDamage(): continue for effectID, effectVolley in fighter.getVolleyPerEffect().items(): volleyMap[(fighter, effectID)] = effectVolley return volleyMap def _getDmgPerKey(self, fit, time): # Damage inflicted makes no sense without time specified if time is None: raise ValueError return self._timeCache.getDmgDataPoint(fit, time) # Application getter def _getApplicationPerKey(self, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): applicationMap = {} for mod in fit.modules: if not mod.isDealingDamage(): continue if mod.hardpoint == FittingHardpoint.TURRET: applicationMap[mod] = getTurretMult( mod=mod, fit=fit, tgt=tgt, atkSpeed=atkSpeed, atkAngle=atkAngle, distance=distance, tgtSpeed=tgtSpeed, tgtAngle=tgtAngle, tgtSigRadius=tgtSigRadius) elif mod.hardpoint == FittingHardpoint.MISSILE: applicationMap[mod] = getLauncherMult( mod=mod, fit=fit, distance=distance, tgtSpeed=tgtSpeed, tgtSigRadius=tgtSigRadius) elif mod.item.group.name in ('Smart Bomb', 'Structure Area Denial Module'): applicationMap[mod] = getSmartbombMult( mod=mod, distance=distance) elif mod.item.group.name == 'Missile Launcher Bomb': applicationMap[mod] = getBombMult( mod=mod, fit=fit, tgt=tgt, distance=distance, tgtSigRadius=tgtSigRadius) elif mod.item.group.name == 'Structure Guided Bomb Launcher': applicationMap[mod] = getGuidedBombMult( mod=mod, fit=fit, distance=distance, tgtSigRadius=tgtSigRadius) elif mod.item.group.name in ('Super Weapon', 'Structure Doomsday Weapon'): applicationMap[mod] = getDoomsdayMult( mod=mod, tgt=tgt, distance=distance, tgtSigRadius=tgtSigRadius) for drone in fit.drones: if not drone.isDealingDamage(): continue applicationMap[drone] = getDroneMult( drone=drone, fit=fit, tgt=tgt, atkSpeed=atkSpeed, atkAngle=atkAngle, distance=distance, tgtSpeed=tgtSpeed, tgtAngle=tgtAngle, tgtSigRadius=tgtSigRadius) for fighter in fit.fighters: if not fighter.isDealingDamage(): continue for ability in fighter.abilities: if not ability.dealsDamage or not ability.active: continue applicationMap[(fighter, ability.effectID)] = getFighterAbilityMult( fighter=fighter, ability=ability, fit=fit, distance=distance, tgtSpeed=tgtSpeed, tgtSigRadius=tgtSigRadius) return applicationMap # Calculate damage from maps def _aggregate(self, dmgMap, applicationMap): total = DmgTypes(0, 0, 0, 0) for key, dmg in dmgMap.items(): total += dmg * applicationMap.get(key, 0) return total FitDamageStatsGraph.register()