diff --git a/graphs/data/fitEwarStats/getter.py b/graphs/data/fitEwarStats/getter.py index df7b07ff6..8f34352c3 100644 --- a/graphs/data/fitEwarStats/getter.py +++ b/graphs/data/fitEwarStats/getter.py @@ -408,3 +408,163 @@ class Distance2JamChanceGetter(SmoothPointGetter): for jamStrength in jamStrengths: retainLockChance *= 1 - min(1, jamStrength / sensorStrength) return (1 - retainLockChance) * 100 + + +class SensorStrength2JamChanceGetter(SmoothPointGetter): + + _baseResolution = 50 + _extraDepth = 2 + + ECM_ATTRS_GENERAL = ('scanGravimetricStrengthBonus', 'scanLadarStrengthBonus', 'scanMagnetometricStrengthBonus', 'scanRadarStrengthBonus') + ECM_ATTRS_FIGHTERS = ('fighterAbilityECMStrengthGravimetric', 'fighterAbilityECMStrengthLadar', 'fighterAbilityECMStrengthMagnetometric', 'fighterAbilityECMStrengthRadar') + SCAN_TYPES = ('Gravimetric', 'Ladar', 'Magnetometric', 'Radar') + + def _getCommonData(self, miscParams, src, tgt): + resonance = 1 - (miscParams.get('resist') or 0) + distance = miscParams.get('distance') + distance = None if distance is None else distance * 1000 + ecms = [] + for mod in src.item.activeModulesIter(): + for effectName in ('remoteECMFalloff', 'structureModuleEffectECM'): + if effectName in mod.item.effects: + ecms.append(( + tuple(mod.getModifiedItemAttr(a) or 0 for a in self.ECM_ATTRS_GENERAL), + mod.maxRange or 0, mod.falloff or 0)) + if 'doomsdayAOEECM' in mod.item.effects: + ecms.append(( + tuple(mod.getModifiedItemAttr(a) or 0 for a in self.ECM_ATTRS_GENERAL), + max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange')), + mod.falloff or 0)) + for drone in src.item.activeDronesIter(): + if 'entityECMFalloff' in drone.item.effects: + ecms.extend(drone.amountActive * (( + tuple(drone.getModifiedItemAttr(a) or 0 for a in self.ECM_ATTRS_GENERAL), + math.inf, 0),)) + for fighter, ability in src.item.activeFighterAbilityIter(): + if ability.effect.name == 'fighterAbilityECM': + ecms.append(( + tuple(fighter.getModifiedItemAttr(a) or 0 for a in self.ECM_ATTRS_FIGHTERS), + math.inf, 0)) + # Determine target's sensor type if target is available + targetScanTypeIndex = None + if tgt is not None: + maxStr = -1 + for i, scanType in enumerate(self.SCAN_TYPES): + currStr = tgt.item.ship.getModifiedItemAttr('scan%sStrength' % scanType) or 0 + if currStr > maxStr: + maxStr = currStr + targetScanTypeIndex = i + return {'ecms': ecms, 'targetScanTypeIndex': targetScanTypeIndex, 'distance': distance, 'resonance': resonance} + + def _calculatePoint(self, x, miscParams, src, tgt, commonData): + sensorStrength = x # This is the variable X-axis value + jamStrengths = [] + targetScanTypeIndex = commonData['targetScanTypeIndex'] + distance = commonData['distance'] + resonance = commonData['resonance'] + inLockRange = checkLockRange(src=src, distance=distance) if distance is not None else True + inDroneRange = checkDroneControlRange(src=src, distance=distance) if distance is not None else True + for strengths, optimal, falloff in commonData['ecms']: + if not inLockRange or not inDroneRange: + continue + if distance is not None: + rangeFactor = calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance) + else: + rangeFactor = 1 + # Use the strength matching the target's sensor type + if targetScanTypeIndex is not None and targetScanTypeIndex < len(strengths): + strength = strengths[targetScanTypeIndex] * resonance + effectiveStrength = strength * rangeFactor + if effectiveStrength > 0: + jamStrengths.append(effectiveStrength) + if not jamStrengths: + return 0 + if sensorStrength <= 0: + return 100 # If sensor strength is 0, 100% jam chance + # Calculate jam chance: 1 - (1 - (ecmStrength / sensorStrength)) ^ numJammers + retainLockChance = 1 + for jamStrength in jamStrengths: + retainLockChance *= 1 - min(1, jamStrength / sensorStrength) + return (1 - retainLockChance) * 100 + + +class Resist2JamChanceGetter(SmoothPointGetter): + + _baseResolution = 50 + _extraDepth = 2 + + ECM_ATTRS_GENERAL = ('scanGravimetricStrengthBonus', 'scanLadarStrengthBonus', 'scanMagnetometricStrengthBonus', 'scanRadarStrengthBonus') + ECM_ATTRS_FIGHTERS = ('fighterAbilityECMStrengthGravimetric', 'fighterAbilityECMStrengthLadar', 'fighterAbilityECMStrengthMagnetometric', 'fighterAbilityECMStrengthRadar') + SCAN_TYPES = ('Gravimetric', 'Ladar', 'Magnetometric', 'Radar') + + def _getCommonData(self, miscParams, src, tgt): + distance = miscParams.get('distance') + distance = None if distance is None else distance * 1000 + ecms = [] + for mod in src.item.activeModulesIter(): + for effectName in ('remoteECMFalloff', 'structureModuleEffectECM'): + if effectName in mod.item.effects: + ecms.append(( + tuple(mod.getModifiedItemAttr(a) or 0 for a in self.ECM_ATTRS_GENERAL), + mod.maxRange or 0, mod.falloff or 0)) + if 'doomsdayAOEECM' in mod.item.effects: + ecms.append(( + tuple(mod.getModifiedItemAttr(a) or 0 for a in self.ECM_ATTRS_GENERAL), + max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange')), + mod.falloff or 0)) + for drone in src.item.activeDronesIter(): + if 'entityECMFalloff' in drone.item.effects: + ecms.extend(drone.amountActive * (( + tuple(drone.getModifiedItemAttr(a) or 0 for a in self.ECM_ATTRS_GENERAL), + math.inf, 0),)) + for fighter, ability in src.item.activeFighterAbilityIter(): + if ability.effect.name == 'fighterAbilityECM': + ecms.append(( + tuple(fighter.getModifiedItemAttr(a) or 0 for a in self.ECM_ATTRS_FIGHTERS), + math.inf, 0)) + # Determine target's sensor type and get sensor strength if target is available + targetScanTypeIndex = None + sensorStrength = None + if tgt is not None: + maxStr = -1 + for i, scanType in enumerate(self.SCAN_TYPES): + currStr = tgt.item.ship.getModifiedItemAttr('scan%sStrength' % scanType) or 0 + if currStr > maxStr: + maxStr = currStr + targetScanTypeIndex = i + sensorStrength = max([tgt.item.ship.getModifiedItemAttr('scan%sStrength' % scanType) + for scanType in self.SCAN_TYPES]) or 0 + return {'ecms': ecms, 'targetScanTypeIndex': targetScanTypeIndex, 'sensorStrength': sensorStrength, 'distance': distance} + + def _calculatePoint(self, x, miscParams, src, tgt, commonData): + resist = x # This is the variable X-axis value (resistance, 0-1) + resonance = 1 - resist + jamStrengths = [] + targetScanTypeIndex = commonData['targetScanTypeIndex'] + sensorStrength = commonData['sensorStrength'] + distance = commonData['distance'] + inLockRange = checkLockRange(src=src, distance=distance) if distance is not None else True + inDroneRange = checkDroneControlRange(src=src, distance=distance) if distance is not None else True + for strengths, optimal, falloff in commonData['ecms']: + if not inLockRange or not inDroneRange: + continue + if distance is not None: + rangeFactor = calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance) + else: + rangeFactor = 1 + # Use the strength matching the target's sensor type + if targetScanTypeIndex is not None and targetScanTypeIndex < len(strengths): + strength = strengths[targetScanTypeIndex] * resonance + effectiveStrength = strength * rangeFactor + if effectiveStrength > 0: + jamStrengths.append(effectiveStrength) + if not jamStrengths: + return 0 + # Use the target's actual sensor strength + if tgt is None or sensorStrength is None or sensorStrength <= 0: + return 0 + # Calculate jam chance: 1 - (1 - (ecmStrength / sensorStrength)) ^ numJammers + retainLockChance = 1 + for jamStrength in jamStrengths: + retainLockChance *= 1 - min(1, jamStrength / sensorStrength) + return (1 - retainLockChance) * 100 diff --git a/graphs/data/fitEwarStats/graph.py b/graphs/data/fitEwarStats/graph.py index 588ed8671..ff6af9920 100644 --- a/graphs/data/fitEwarStats/graph.py +++ b/graphs/data/fitEwarStats/graph.py @@ -22,7 +22,7 @@ import wx from graphs.data.base import FitGraph, Input, XDef, YDef from .getter import (Distance2DampStrLockRangeGetter, Distance2EcmStrMaxGetter, Distance2GdStrRangeGetter, Distance2JamChanceGetter, Distance2NeutingStrGetter, - Distance2TdStrOptimalGetter, Distance2TpStrGetter, Distance2WebbingStrGetter) + Resist2JamChanceGetter, SensorStrength2JamChanceGetter, Distance2TdStrOptimalGetter, Distance2TpStrGetter, Distance2WebbingStrGetter) _t = wx.GetTranslation @@ -32,7 +32,10 @@ class FitEwarStatsGraph(FitGraph): internalName = 'ewarStatsGraph' name = _t('Electronic Warfare Stats') hasTargets = True - xDefs = [XDef(handle='distance', unit='km', label=_t('Distance'), mainInput=('distance', 'km'))] + xDefs = [ + XDef(handle='distance', unit='km', label=_t('Distance'), mainInput=('distance', 'km')), + XDef(handle='sensorStrength', unit=None, label=_t('Target sensor strength'), mainInput=('sensorStrength', None)), + XDef(handle='resist', unit='%', label=_t('Target resistance'), mainInput=('resist', '%'))] yDefs = [ YDef(handle='neutStr', unit=None, label=_t('Cap neutralized per second'), selectorLabel=_t('Neuts: cap per second')), YDef(handle='webStr', unit='%', label=_t('Speed reduction'), selectorLabel=_t('Webs: speed reduction')), @@ -44,18 +47,23 @@ class FitEwarStatsGraph(FitGraph): YDef(handle='tpStr', unit='%', label=_t('Signature radius increase'), selectorLabel=_t('TPs: signature radius increase'))] inputs = [ Input(handle='distance', unit='km', label=_t('Distance'), iconID=1391, defaultValue=None, defaultRange=(0, 100)), + Input(handle='sensorStrength', unit=None, label=_t('Target sensor strength'), iconID=1394, defaultValue=None, defaultRange=(0, 100)), Input(handle='resist', unit='%', label=_t('Target resistance'), iconID=1393, defaultValue=0, defaultRange=(0, 100))] # Calculation stuff _normalizers = { ('distance', 'km'): lambda v, src, tgt: None if v is None else v * 1000, ('resist', '%'): lambda v, src, tgt: None if v is None else v / 100} - _limiters = {'resist': lambda src, tgt: (0, 1)} + _limiters = { + 'resist': lambda src, tgt: (0, 1), + 'sensorStrength': lambda src, tgt: (1, 200)} _getters = { ('distance', 'neutStr'): Distance2NeutingStrGetter, ('distance', 'webStr'): Distance2WebbingStrGetter, ('distance', 'ecmStrMax'): Distance2EcmStrMaxGetter, ('distance', 'jamChance'): Distance2JamChanceGetter, + ('sensorStrength', 'jamChance'): SensorStrength2JamChanceGetter, + ('resist', 'jamChance'): Resist2JamChanceGetter, ('distance', 'dampStrLockRange'): Distance2DampStrLockRangeGetter, ('distance', 'tdStrOptimal'): Distance2TdStrOptimalGetter, ('distance', 'gdStrRange'): Distance2GdStrRangeGetter,