def getCycleParametersPerEffect(self, reloadOverride=None): factorReload = reloadOverride if reloadOverride is not None else self.owner.factorReload # Assume it can cycle infinitely if not factorReload: return { a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.cycleTime > 0 } limitedAbilities = [ a for a in self.abilities if a.numShots > 0 and a.cycleTime > 0 ] if len(limitedAbilities) == 0: return { a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.cycleTime > 0 } validAbilities = [a for a in self.abilities if a.cycleTime > 0] if len(validAbilities) == 0: return {} mostLimitedAbility = min(limitedAbilities, key=lambda a: a.cycleTime * a.numShots) durationToRefuel = mostLimitedAbility.cycleTime * mostLimitedAbility.numShots # find out how many shots various abilities will do until reload, and how much time # "extra" cycle will last (None for no extra cycle) cyclesUntilRefuel = { mostLimitedAbility.effectID: (mostLimitedAbility.numShots, None) } for ability in (a for a in validAbilities if a is not mostLimitedAbility): fullCycles = int(floatUnerr(durationToRefuel / ability.cycleTime)) extraShotTime = floatUnerr(durationToRefuel - (fullCycles * ability.cycleTime)) if extraShotTime == 0: extraShotTime = None cyclesUntilRefuel[ability.effectID] = (fullCycles, extraShotTime) refuelTimes = {} for ability in validAbilities: spentShots, extraShotTime = cyclesUntilRefuel[ability.effectID] if extraShotTime is not None: spentShots += 1 refuelTimes[ability.effectID] = ability.getReloadTime(spentShots) refuelTime = max(refuelTimes.values()) cycleParams = {} for ability in validAbilities: regularShots, extraShotTime = cyclesUntilRefuel[ability.effectID] sequence = [] if extraShotTime is not None: if regularShots > 0: sequence.append( CycleInfo(ability.cycleTime, 0, regularShots)) sequence.append(CycleInfo(extraShotTime, refuelTime, 1)) else: regularShotsNonReload = regularShots - 1 if regularShotsNonReload > 0: sequence.append( CycleInfo(ability.cycleTime, 0, regularShotsNonReload)) sequence.append(CycleInfo(ability.cycleTime, refuelTime, 1)) cycleParams[ability.effectID] = CycleSequence(sequence, math.inf) return cycleParams
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]
def __eq__(self, other): if not isinstance(other, DmgTypes): return NotImplemented # Round for comparison's sake because often damage profiles are # generated from data which includes float errors return (floatUnerr(self.em) == floatUnerr(other.em) and floatUnerr(self.thermal) == floatUnerr(other.thermal) and floatUnerr(self.kinetic) == floatUnerr(other.kinetic) and floatUnerr(self.explosive) == floatUnerr(other.explosive) and floatUnerr(self.total) == floatUnerr(other.total))
def _prepareDpsVolleyData(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 '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 getDigitPlaces(minValue, maxValue): minDigits = 3 maxDigits = 5 currentDecision = minDigits for value in (floatUnerr(minValue), floatUnerr(maxValue)): for currentDigit in range(minDigits, maxDigits + 1): if round(value, currentDigit) == value: if currentDigit > currentDecision: currentDecision = currentDigit break # Max decimal places we can afford to show was not enough else: return maxDigits return currentDecision
def getDigitPlaces(minValue, maxValue): minDigits = 3 maxDigits = 5 currentDecision = minDigits for value in (floatUnerr(minValue), floatUnerr(maxValue)): for currentDigit in range(minDigits, maxDigits + 1): if round(value, currentDigit) == value: if currentDigit > currentDecision: currentDecision = currentDigit break # Max decimal places we can afford to show was not enough else: return maxDigits return currentDecision
def _calcPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, src, tgt): mainParamRange = self._normalizeMain(mainInput=mainInput, src=src, tgt=tgt) miscParams = self._normalizeMisc(miscInputs=miscInputs, src=src, tgt=tgt) mainParamRange = self._limitMain(mainParamRange=mainParamRange, src=src, tgt=tgt) miscParams = self._limitMisc(miscParams=miscParams, src=src, tgt=tgt) xs, ys = self._getPlotPoints( xRange=mainParamRange[1], miscParams=miscParams, xSpec=xSpec, ySpec=ySpec, src=src, tgt=tgt) ys = self._denormalizeValues(values=ys, axisSpec=ySpec, src=src, tgt=tgt) # Sometimes x denormalizer may fail (e.g. during conversion of 0 ship speed to %). # If both inputs and outputs are in %, do some extra processing to at least have # proper graph which shows the same value over whole specified relative parameter # range try: xs = self._denormalizeValues(values=xs, axisSpec=xSpec, src=src, tgt=tgt) except ZeroDivisionError: if mainInput.unit == xSpec.unit == '%' and len(set(floatUnerr(y) for y in ys)) == 1: xs = [min(mainInput.value), max(mainInput.value)] ys = [ys[0], ys[0]] else: raise else: # Same for NaN which means we tried to denormalize infinity values, which might be the # case for the ideal target profile with infinite signature radius if mainInput.unit == xSpec.unit == '%' and all(math.isnan(x) for x in xs): xs = [min(mainInput.value), max(mainInput.value)] ys = [ys[0], ys[0]] return xs, ys
def getVolleyParameters(self, spoolOptions=None, targetResists=None, ignoreState=False): if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState): return {0: DmgTypes(0, 0, 0, 0)} if self.__baseVolley is None: self.__baseVolley = {} dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr dmgMult = self.getModifiedItemAttr("damageMultiplier", 1) dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0) or self.getModifiedItemAttr("doomsdayWarningDuration", 0) dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0) dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0) if dmgDuration != 0 and dmgSubcycle != 0: subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle)) else: subcycles = 1 for i in range(subcycles): self.__baseVolley[dmgDelay + dmgSubcycle * i] = DmgTypes( em=(dmgGetter("emDamage", 0)) * dmgMult, thermal=(dmgGetter("thermalDamage", 0)) * dmgMult, kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult, explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult) spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self) spoolBoost = calculateSpoolup( self.getModifiedItemAttr("damageMultiplierBonusMax", 0), self.getModifiedItemAttr("damageMultiplierBonusPerCycle", 0), self.rawCycleTime / 1000, spoolType, spoolAmount)[0] spoolMultiplier = 1 + spoolBoost adjustedVolley = {} for volleyTime, volleyValue in self.__baseVolley.items(): adjustedVolley[volleyTime] = DmgTypes( em=volleyValue.em * spoolMultiplier * (1 - getattr(targetResists, "emAmount", 0)), thermal=volleyValue.thermal * spoolMultiplier * (1 - getattr(targetResists, "thermalAmount", 0)), kinetic=volleyValue.kinetic * spoolMultiplier * (1 - getattr(targetResists, "kineticAmount", 0)), explosive=volleyValue.explosive * spoolMultiplier * (1 - getattr(targetResists, "explosiveAmount", 0))) return adjustedVolley
def valToStr(val): if val is None: return '' val = floatUnerr(val) if int(val) == val: val = int(val) return str(val)
def getApplicationPerKey(src, distance): inLockRange = checkLockRange(src=src, distance=distance) inDroneRange = checkDroneControlRange(src=src, distance=distance) applicationMap = {} for mod in src.item.activeModulesIter(): if not mod.isRemoteRepping(): continue if not inLockRange: applicationMap[mod] = 0 else: applicationMap[mod] = calculateRangeFactor( srcOptimalRange=mod.maxRange or 0, srcFalloffRange=mod.falloff or 0, distance=distance) for drone in src.item.activeDronesIter(): if not drone.isRemoteRepping(): continue if not inLockRange or not inDroneRange: applicationMap[drone] = 0 else: applicationMap[drone] = 1 # Ensure consistent results - round off a little to avoid float errors for k, v in applicationMap.items(): applicationMap[k] = floatUnerr(v) return applicationMap
def __eq__(self, other): if not isinstance(other, RRTypes): return NotImplemented # Round for comparison's sake because often tanking numbers are # generated from data which includes float errors return (floatUnerr(self.shield) == floatUnerr(other.shield) and floatUnerr(self.armor) == floatUnerr(other.armor) and floatUnerr(self.hull) == floatUnerr(other.hull) and floatUnerr(self.capacitor) == floatUnerr(other.capacitor))
def prepareRpsData(self, src, ancReload, 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, ancReload=ancReload, maxTime=maxTime) fitCache = self._data[src.item.ID][ancReload] # 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 getCycleParametersPerEffect(self, reloadOverride=None): factorReload = reloadOverride if reloadOverride is not None else self.owner.factorReload # Assume it can cycle infinitely if not factorReload: return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.cycleTime > 0} limitedAbilities = [a for a in self.abilities if a.numShots > 0 and a.cycleTime > 0] if len(limitedAbilities) == 0: return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.cycleTime > 0} validAbilities = [a for a in self.abilities if a.cycleTime > 0] if len(validAbilities) == 0: return {} mostLimitedAbility = min(limitedAbilities, key=lambda a: a.cycleTime * a.numShots) durationToRefuel = mostLimitedAbility.cycleTime * mostLimitedAbility.numShots # find out how many shots various abilities will do until reload, and how much time # "extra" cycle will last (None for no extra cycle) cyclesUntilRefuel = {mostLimitedAbility.effectID: (mostLimitedAbility.numShots, None)} for ability in (a for a in validAbilities if a is not mostLimitedAbility): fullCycles = int(floatUnerr(durationToRefuel / ability.cycleTime)) extraShotTime = floatUnerr(durationToRefuel - (fullCycles * ability.cycleTime)) if extraShotTime == 0: extraShotTime = None cyclesUntilRefuel[ability.effectID] = (fullCycles, extraShotTime) refuelTimes = {} for ability in validAbilities: spentShots, extraShotTime = cyclesUntilRefuel[ability.effectID] if extraShotTime is not None: spentShots += 1 refuelTimes[ability.effectID] = ability.getReloadTime(spentShots) refuelTime = max(refuelTimes.values()) cycleParams = {} for ability in validAbilities: regularShots, extraShotTime = cyclesUntilRefuel[ability.effectID] sequence = [] if extraShotTime is not None: if regularShots > 0: sequence.append(CycleInfo(ability.cycleTime, 0, regularShots)) sequence.append(CycleInfo(extraShotTime, refuelTime, 1)) else: regularShotsNonReload = regularShots - 1 if regularShotsNonReload > 0: sequence.append(CycleInfo(ability.cycleTime, 0, regularShotsNonReload)) sequence.append(CycleInfo(ability.cycleTime, refuelTime, 1)) cycleParams[ability.effectID] = CycleSequence(sequence, math.inf) return cycleParams
def getVolleyParameters(self, spoolOptions=None, targetProfile=None, ignoreState=False): if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState): return {0: DmgTypes(0, 0, 0, 0)} if self.__baseVolley is None: self.__baseVolley = {} dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr dmgMult = self.getModifiedItemAttr("damageMultiplier", 1) # Some delay attributes have non-0 default value, so we have to pick according to effects if { 'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon' }.intersection(self.item.effects): dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0) elif {'doomsdayBeamDOT', 'doomsdaySlash', 'doomsdayConeDOT'}.intersection(self.item.effects): dmgDelay = self.getModifiedItemAttr("doomsdayWarningDuration", 0) else: dmgDelay = 0 dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0) dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0) # Reaper DD can damage each target only once if dmgDuration != 0 and dmgSubcycle != 0 and 'doomsdaySlash' not in self.item.effects: subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle)) else: subcycles = 1 for i in range(subcycles): self.__baseVolley[dmgDelay + dmgSubcycle * i] = DmgTypes( em=(dmgGetter("emDamage", 0)) * dmgMult, thermal=(dmgGetter("thermalDamage", 0)) * dmgMult, kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult, explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult) spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self) spoolBoost = calculateSpoolup( self.getModifiedItemAttr("damageMultiplierBonusMax", 0), self.getModifiedItemAttr("damageMultiplierBonusPerCycle", 0), self.rawCycleTime / 1000, spoolType, spoolAmount)[0] spoolMultiplier = 1 + spoolBoost adjustedVolley = {} for volleyTime, volleyValue in self.__baseVolley.items(): adjustedVolley[volleyTime] = DmgTypes( em=volleyValue.em * spoolMultiplier * (1 - getattr(targetProfile, "emAmount", 0)), thermal=volleyValue.thermal * spoolMultiplier * (1 - getattr(targetProfile, "thermalAmount", 0)), kinetic=volleyValue.kinetic * spoolMultiplier * (1 - getattr(targetProfile, "kineticAmount", 0)), explosive=volleyValue.explosive * spoolMultiplier * (1 - getattr(targetProfile, "explosiveAmount", 0))) return adjustedVolley
def getSigRadiusMult(src, tgt, tgtSpeed, srcScramRange, tgtScrammables, tpMods, tpDrones, tpFighters, distance): # Can blow non-immune ships and target profiles if tgt.isFit and tgt.item.ship.getModifiedItemAttr( 'disallowOffensiveModifiers'): return 1 inLockRange = checkLockRange(src=src, distance=distance) inDroneRange = checkDroneControlRange(src=src, distance=distance) initSig = tgt.getSigRadius() # No scrams or distance is longer than longest scram - nullify scrammables list if not inLockRange or srcScramRange is None or (distance is not None and distance > srcScramRange): tgtScrammables = () # TPing modules appliedMultipliers = {} if inLockRange: for tpData in tpMods: appliedBoost = tpData.boost * calculateRangeFactor( srcOptimalRange=tpData.optimal, srcFalloffRange=tpData.falloff, distance=distance) if appliedBoost: appliedMultipliers.setdefault(tpData.stackingGroup, []).append( (1 + appliedBoost / 100, tpData.resAttrID)) # TPing drones mobileTps = [] if inLockRange: mobileTps.extend(tpFighters) if inLockRange and inDroneRange: mobileTps.extend(tpDrones) droneOpt = GraphSettings.getInstance().get('mobileDroneMode') atkRadius = src.getRadius() for mtpData in mobileTps: # Faster than target or set to follow it - apply full TP if (droneOpt == GraphDpsDroneMode.auto and mtpData.speed >= tgtSpeed ) or droneOpt == GraphDpsDroneMode.followTarget: appliedMtpBoost = mtpData.boost # Otherwise project from the center of the ship else: if distance is None: rangeFactorDistance = None else: rangeFactorDistance = distance + atkRadius - mtpData.radius appliedMtpBoost = mtpData.boost * calculateRangeFactor( srcOptimalRange=mtpData.optimal, srcFalloffRange=mtpData.falloff, distance=rangeFactorDistance) appliedMultipliers.setdefault(mtpData.stackingGroup, []).append( (1 + appliedMtpBoost / 100, mtpData.resAttrID)) modifiedSig = tgt.getSigRadius(extraMultipliers=appliedMultipliers, ignoreAfflictors=tgtScrammables) if modifiedSig == math.inf and initSig == math.inf: return 1 mult = modifiedSig / initSig # Ensure consistent results - round off a little to avoid float errors return floatUnerr(mult)
def getNumCharges(self, charge): if charge is None: charges = 0 else: chargeVolume = charge.attributes['volume'].value containerCapacity = self.item.attributes['capacity'].value if chargeVolume is None or containerCapacity is None: charges = 0 else: charges = int(floatUnerr(containerCapacity / chargeVolume)) return charges
def getNumCharges(self, charge): if charge is None: charges = 0 else: chargeVolume = charge.volume containerCapacity = self.item.capacity if chargeVolume is None or containerCapacity is None: charges = 0 else: charges = int(floatUnerr(containerCapacity / chargeVolume)) return charges
def calculateSpoolup(modMaxValue, modStepValue, modCycleTime, spoolType, spoolAmount): """ Calculate damage multiplier increment based on passed parameters. Module cycle time is specified in seconds. Returns spoolup value, amount of cycles to reach it and time to reach it. """ if not modMaxValue or not modStepValue: return 0, 0, 0 if spoolType == SpoolType.SCALE: cycles = int(floatUnerr(spoolAmount * modMaxValue / modStepValue)) return cycles * modStepValue, cycles, cycles * modCycleTime elif spoolType == SpoolType.TIME: cycles = min(int(floatUnerr(spoolAmount / modCycleTime)), int(floatUnerr(modMaxValue / modStepValue))) return cycles * modStepValue, cycles, cycles * modCycleTime elif spoolType == SpoolType.CYCLES: cycles = min(int(spoolAmount), int(floatUnerr(modMaxValue / modStepValue))) return cycles * modStepValue, cycles, cycles * modCycleTime else: return 0, 0, 0
def getNumCharges(self, charge): if charge is None: charges = 0 else: chargeVolume = charge.volume containerCapacity = self.item.capacity if chargeVolume is None or containerCapacity is None: charges = 0 else: charges = int(floatUnerr(containerCapacity / chargeVolume)) return charges
def calculateSpoolup(modMaxValue, modStepValue, modCycleTime, spoolType, spoolAmount): """ Calculate damage multiplier increment based on passed parameters. Module cycle time is specified in seconds. Returns spoolup value, amount of cycles to reach it and time to reach it. """ if not modMaxValue or not modStepValue: return 0, 0, 0 if spoolType == SpoolType.SPOOL_SCALE: # Find out at which point of spoolup scale we're on, find out how many cycles # is enough to reach it and recalculate spoolup value for that amount of cycles cycles = math.ceil(floatUnerr(modMaxValue * spoolAmount / modStepValue)) spoolValue = min(modMaxValue, cycles * modStepValue) return spoolValue, cycles, cycles * modCycleTime elif spoolType == SpoolType.CYCLE_SCALE: # For cycle scale, find out max amount of cycles and scale against it cycles = round(spoolAmount * math.ceil(floatUnerr(modMaxValue / modStepValue))) spoolValue = min(modMaxValue, cycles * modStepValue) return spoolValue, cycles, cycles * modCycleTime elif spoolType == SpoolType.TIME: cycles = min( # How many full cycles mod had by passed time math.floor(floatUnerr(spoolAmount / modCycleTime)), # Max amount of cycles math.ceil(floatUnerr(modMaxValue / modStepValue))) spoolValue = min(modMaxValue, cycles * modStepValue) return spoolValue, cycles, cycles * modCycleTime elif spoolType == SpoolType.CYCLES: cycles = min( # Consider full cycles only math.floor(spoolAmount), # Max amount of cycles math.ceil(floatUnerr(modMaxValue / modStepValue))) spoolValue = min(modMaxValue, cycles * modStepValue) return spoolValue, cycles, cycles * modCycleTime else: return 0, 0, 0
def renderMutant(mutant, firstPrefix='', prefix=''): exportLines = [] mutatedAttrs = {} for attrID, mutator in mutant.mutators.items(): attrName = getAttributeInfo(attrID).name mutatedAttrs[attrName] = mutator.value exportLines.append('{}{}'.format(firstPrefix, mutant.baseItem.name)) exportLines.append('{}{}'.format(prefix, mutant.mutaplasmid.item.name)) customAttrsLine = ', '.join('{} {}'.format(a, floatUnerr(mutatedAttrs[a])) for a in sorted(mutatedAttrs)) exportLines.append('{}{}'.format(prefix, customAttrsLine)) return '\n'.join(exportLines)
def getText(self, stuff): if isinstance(stuff, BaseWrapper): stuff = stuff.item mult = 1 if isinstance(stuff, Fit): mult = floatUnerr(stuff.getDampMultScanRes()) if mult == 1: text = '' else: text = '{}%'.format( formatAmount((mult - 1) * 100, 3, 0, 0, forceSign=True)) return text
def calculateSpoolup(modMaxValue, modStepValue, modCycleTime, spoolType, spoolAmount): """ Calculate damage multiplier increment based on passed parameters. Module cycle time is specified in seconds. Returns spoolup value, amount of cycles to reach it and time to reach it. """ if not modMaxValue or not modStepValue: return 0, 0, 0 if spoolType == SpoolType.SCALE: cycles = int(floatUnerr(spoolAmount * modMaxValue / modStepValue)) return cycles * modStepValue, cycles, cycles * modCycleTime elif spoolType == SpoolType.TIME: cycles = min(int(floatUnerr(spoolAmount / modCycleTime)), int(floatUnerr(modMaxValue / modStepValue))) return cycles * modStepValue, cycles, cycles * modCycleTime elif spoolType == SpoolType.CYCLES: cycles = min(int(spoolAmount), int(floatUnerr(modMaxValue / modStepValue))) return cycles * modStepValue, cycles, cycles * modCycleTime else: return 0, 0, 0
def getTpMult(src, tgt, tgtSpeed, tpMods, tpDrones, tpFighters, distance): # Can blow non-immune ships and target profiles if tgt.isFit and tgt.item.ship.getModifiedItemAttr( 'disallowOffensiveModifiers'): return 1 untpedSig = tgt.getSigRadius() # Modules appliedMultipliers = {} for tpData in tpMods: appliedBoost = tpData.boost * calculateRangeFactor( srcOptimalRange=tpData.optimal, srcFalloffRange=tpData.falloff, distance=distance) if appliedBoost: appliedMultipliers.setdefault(tpData.stackingGroup, []).append( (1 + appliedBoost / 100, tpData.resAttrID)) # Drones and fighters mobileTps = [] mobileTps.extend(tpFighters) # Drones have range limit if distance is None or distance <= src.item.extraAttributes[ 'droneControlRange']: mobileTps.extend(tpDrones) droneOpt = GraphSettings.getInstance().get('mobileDroneMode') atkRadius = src.getRadius() for mtpData in mobileTps: # Faster than target or set to follow it - apply full TP if (droneOpt == GraphDpsDroneMode.auto and mtpData.speed >= tgtSpeed ) or droneOpt == GraphDpsDroneMode.followTarget: appliedMtpBoost = mtpData.boost # Otherwise project from the center of the ship else: if distance is None: rangeFactorDistance = None else: rangeFactorDistance = distance + atkRadius - mtpData.radius appliedMtpBoost = mtpData.boost * calculateRangeFactor( srcOptimalRange=mtpData.optimal, srcFalloffRange=mtpData.falloff, distance=rangeFactorDistance) appliedMultipliers.setdefault(mtpData.stackingGroup, []).append( (1 + appliedMtpBoost / 100, mtpData.resAttrID)) tpedSig = tgt.getSigRadius(extraMultipliers=appliedMultipliers) if tpedSig == math.inf and untpedSig == math.inf: return 1 mult = tpedSig / untpedSig # Ensure consistent results - round off a little to avoid float errors return floatUnerr(mult)
def missileMaxRangeData(self): if self.charge is None: return None try: chargeName = self.charge.group.name except AttributeError: pass else: if chargeName in ("Scanner Probe", "Survey Probe"): return None def calculateRange(maxVelocity, mass, agility, flightTime): # Source: http://www.eveonline.com/ingameboard.asp?a=topic&threadID=1307419&page=1#15 # D_m = V_m * (T_m + T_0*[exp(- T_m/T_0)-1]) accelTime = min(flightTime, mass * agility / 1000000) # Average distance done during acceleration duringAcceleration = maxVelocity / 2 * accelTime # Distance done after being at full speed fullSpeed = maxVelocity * (flightTime - accelTime) maxRange = duringAcceleration + fullSpeed return maxRange maxVelocity = self.getModifiedChargeAttr("maxVelocity") if not maxVelocity: return None shipRadius = self.owner.ship.getModifiedItemAttr("radius") # Flight time has bonus based on ship radius, see https://github.com/pyfa-org/Pyfa/issues/2083 flightTime = floatUnerr( self.getModifiedChargeAttr("explosionDelay") / 1000 + shipRadius / maxVelocity) mass = self.getModifiedChargeAttr("mass") agility = self.getModifiedChargeAttr("agility") lowerTime = math.floor(flightTime) higherTime = math.ceil(flightTime) lowerRange = calculateRange(maxVelocity, mass, agility, lowerTime) higherRange = calculateRange(maxVelocity, mass, agility, higherTime) # Fof range limit is supposedly calculated based on overview (surface-to-surface) range if 'fofMissileLaunching' in self.charge.effects: rangeLimit = self.getModifiedChargeAttr("maxFOFTargetRange") if rangeLimit: lowerRange = min(lowerRange, rangeLimit) higherRange = min(higherRange, rangeLimit) # Make range center-to-surface, as missiles spawn in the center of the ship lowerRange = max(0, lowerRange - shipRadius) higherRange = max(0, higherRange - shipRadius) higherChance = flightTime - lowerTime return lowerRange, higherRange, higherChance
def getText(self, mod): if isinstance(mod, Mode): return '' fit = Fit.getInstance().getFit(self.fittingView.getActiveFit()) if fit is None: return '' capUse = mod.capUse # Do not show cap diff numbers if mod.item is not None and mod.item.group.name in regenGroups: capRegenDiff = fit.getCapRegenGainFromMod(mod) else: capRegenDiff = 0 capDiff = floatUnerr(capRegenDiff - capUse) if capDiff: return formatAmount(capDiff, 3, 0, 3, forceSign=True) else: return ''
def getVolleyParameters(self, spoolOptions=None, targetResists=None, ignoreState=False): if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState): return {0: DmgTypes(0, 0, 0, 0)} if self.__baseVolley is None: self.__baseVolley = {} dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr dmgMult = self.getModifiedItemAttr("damageMultiplier", 1) dmgDelay = self.getModifiedItemAttr( "damageDelayDuration", 0) or self.getModifiedItemAttr( "doomsdayWarningDuration", 0) dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0) dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0) if dmgDuration != 0 and dmgSubcycle != 0: subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle)) else: subcycles = 1 for i in range(subcycles): self.__baseVolley[dmgDelay + dmgSubcycle * i] = DmgTypes( em=(dmgGetter("emDamage", 0)) * dmgMult, thermal=(dmgGetter("thermalDamage", 0)) * dmgMult, kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult, explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult) spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self) spoolBoost = calculateSpoolup( self.getModifiedItemAttr("damageMultiplierBonusMax", 0), self.getModifiedItemAttr("damageMultiplierBonusPerCycle", 0), self.rawCycleTime / 1000, spoolType, spoolAmount)[0] spoolMultiplier = 1 + spoolBoost adjustedVolley = {} for volleyTime, volleyValue in self.__baseVolley.items(): adjustedVolley[volleyTime] = DmgTypes( em=volleyValue.em * spoolMultiplier * (1 - getattr(targetResists, "emAmount", 0)), thermal=volleyValue.thermal * spoolMultiplier * (1 - getattr(targetResists, "thermalAmount", 0)), kinetic=volleyValue.kinetic * spoolMultiplier * (1 - getattr(targetResists, "kineticAmount", 0)), explosive=volleyValue.explosive * spoolMultiplier * (1 - getattr(targetResists, "explosiveAmount", 0))) return adjustedVolley
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
def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): inLockRange = checkLockRange(src=src, distance=distance) inDroneRange = checkDroneControlRange(src=src, distance=distance) applicationMap = {} for mod in src.item.activeModulesIter(): if not mod.isDealingDamage(): continue if "ChainLightning" in mod.item.effects: if inLockRange: applicationMap[mod] = getVortonMult(mod=mod, distance=distance, tgtSpeed=tgtSpeed, tgtSigRadius=tgtSigRadius) elif mod.hardpoint == FittingHardpoint.TURRET: if inLockRange: applicationMap[mod] = getTurretMult(mod=mod, src=src, tgt=tgt, atkSpeed=atkSpeed, atkAngle=atkAngle, distance=distance, tgtSpeed=tgtSpeed, tgtAngle=tgtAngle, tgtSigRadius=tgtSigRadius) else: applicationMap[mod] = 0 elif mod.hardpoint == FittingHardpoint.MISSILE: # FoF missiles can shoot beyond lock range if inLockRange or (mod.charge is not None and 'fofMissileLaunching' in mod.charge.effects): applicationMap[mod] = getLauncherMult( mod=mod, distance=distance, tgtSpeed=tgtSpeed, tgtSigRadius=tgtSigRadius) else: applicationMap[mod] = 0 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, src=src, tgt=tgt, distance=distance, tgtSigRadius=tgtSigRadius) elif mod.item.group.name == 'Structure Guided Bomb Launcher': if inLockRange: applicationMap[mod] = getGuidedBombMult( mod=mod, src=src, distance=distance, tgtSigRadius=tgtSigRadius) else: applicationMap[mod] = 0 elif mod.item.group.name in ('Super Weapon', 'Structure Doomsday Weapon'): # Only single-target DDs need locks if not inLockRange and { 'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon' }.intersection(mod.item.effects): applicationMap[mod] = 0 else: applicationMap[mod] = getDoomsdayMult( mod=mod, tgt=tgt, distance=distance, tgtSigRadius=tgtSigRadius) for drone in src.item.activeDronesIter(): if not drone.isDealingDamage(): continue if inLockRange and inDroneRange: applicationMap[drone] = getDroneMult(drone=drone, src=src, tgt=tgt, atkSpeed=atkSpeed, atkAngle=atkAngle, distance=distance, tgtSpeed=tgtSpeed, tgtAngle=tgtAngle, tgtSigRadius=tgtSigRadius) else: applicationMap[drone] = 0 for fighter in src.item.activeFightersIter(): if not fighter.isDealingDamage(): continue for ability in fighter.abilities: if not ability.dealsDamage or not ability.active: continue # Bomb launching doesn't need locks if inLockRange or ability.effect.name == 'fighterAbilityLaunchBomb': applicationMap[(fighter, ability.effectID)] = getFighterAbilityMult( fighter=fighter, ability=ability, src=src, tgt=tgt, distance=distance, tgtSpeed=tgtSpeed, tgtSigRadius=tgtSigRadius) else: applicationMap[(fighter, ability.effectID)] = 0 # Ensure consistent results - round off a little to avoid float errors for k, v in applicationMap.items(): applicationMap[k] = floatUnerr(v) return applicationMap
def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): applicationMap = {} for mod in src.item.activeModulesIter(): if not mod.isDealingDamage(): continue if mod.hardpoint == FittingHardpoint.TURRET: applicationMap[mod] = getTurretMult(mod=mod, src=src, tgt=tgt, atkSpeed=atkSpeed, atkAngle=atkAngle, distance=distance, tgtSpeed=tgtSpeed, tgtAngle=tgtAngle, tgtSigRadius=tgtSigRadius) elif mod.hardpoint == FittingHardpoint.MISSILE: applicationMap[mod] = getLauncherMult(mod=mod, src=src, 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, src=src, tgt=tgt, distance=distance, tgtSigRadius=tgtSigRadius) elif mod.item.group.name == 'Structure Guided Bomb Launcher': applicationMap[mod] = getGuidedBombMult(mod=mod, src=src, 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 src.item.activeDronesIter(): if not drone.isDealingDamage(): continue applicationMap[drone] = getDroneMult(drone=drone, src=src, tgt=tgt, atkSpeed=atkSpeed, atkAngle=atkAngle, distance=distance, tgtSpeed=tgtSpeed, tgtAngle=tgtAngle, tgtSigRadius=tgtSigRadius) for fighter in src.item.activeFightersIter(): 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, src=src, tgt=tgt, distance=distance, tgtSpeed=tgtSpeed, tgtSigRadius=tgtSigRadius) # Ensure consistent results - round off a little to avoid float errors for k, v in applicationMap.items(): applicationMap[k] = floatUnerr(v) return applicationMap
def OnWheel(self, event): amount = 0.1 * event.GetWheelRotation() / event.GetWheelDelta() self._length = floatUnerr(min(max(self._length + amount, 0.0), 1.0)) self.Refresh() self.SendChangeEvent()
def getTackledSpeed(src, tgt, currentUntackledSpeed, srcScramRange, tgtScrammables, webMods, webDrones, webFighters, distance): # Can slow down non-immune ships and target profiles if tgt.isFit and tgt.item.ship.getModifiedItemAttr( 'disallowOffensiveModifiers'): return currentUntackledSpeed maxUntackledSpeed = tgt.getMaxVelocity() # What's immobile cannot be slowed if maxUntackledSpeed == 0: return maxUntackledSpeed inLockRange = checkLockRange(src=src, distance=distance) inDroneRange = checkDroneControlRange(src=src, distance=distance) speedRatio = currentUntackledSpeed / maxUntackledSpeed # No scrams or distance is longer than longest scram - nullify scrammables list if not inLockRange or srcScramRange is None or (distance is not None and distance > srcScramRange): tgtScrammables = () appliedMultipliers = {} # Modules first, they are always applied the same way if inLockRange: for wData in webMods: appliedBoost = wData.boost * calculateRangeFactor( srcOptimalRange=wData.optimal, srcFalloffRange=wData.falloff, distance=distance) if appliedBoost: appliedMultipliers.setdefault(wData.stackingGroup, []).append( (1 + appliedBoost / 100, wData.resAttrID)) maxTackledSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers, ignoreAfflictors=tgtScrammables) currentTackledSpeed = maxTackledSpeed * speedRatio # Drones and fighters mobileWebs = [] if inLockRange: mobileWebs.extend(webFighters) if inLockRange and inDroneRange: mobileWebs.extend(webDrones) atkRadius = src.getRadius() # As mobile webs either follow the target or stick to the attacking ship, # if target is within mobile web optimal - it can be applied unconditionally longEnoughMws = [ mw for mw in mobileWebs if distance is None or distance <= mw.optimal - atkRadius + mw.radius ] if longEnoughMws: for mwData in longEnoughMws: appliedMultipliers.setdefault(mwData.stackingGroup, []).append( (1 + mwData.boost / 100, mwData.resAttrID)) mobileWebs.remove(mwData) maxTackledSpeed = tgt.getMaxVelocity( extraMultipliers=appliedMultipliers, ignoreAfflictors=tgtScrammables) currentTackledSpeed = maxTackledSpeed * speedRatio # Apply remaining webs, from fastest to slowest droneOpt = GraphSettings.getInstance().get('mobileDroneMode') while mobileWebs: # Process in batches unified by speed to save up resources fastestMwSpeed = max(mobileWebs, key=lambda mw: mw.speed).speed fastestMws = [mw for mw in mobileWebs if mw.speed == fastestMwSpeed] for mwData in fastestMws: # Faster than target or set to follow it - apply full slowdown if (droneOpt == GraphDpsDroneMode.auto and mwData.speed >= currentTackledSpeed ) or droneOpt == GraphDpsDroneMode.followTarget: appliedMwBoost = mwData.boost # Otherwise project from the center of the ship else: if distance is None: rangeFactorDistance = None else: rangeFactorDistance = distance + atkRadius - mwData.radius appliedMwBoost = mwData.boost * calculateRangeFactor( srcOptimalRange=mwData.optimal, srcFalloffRange=mwData.falloff, distance=rangeFactorDistance) appliedMultipliers.setdefault(mwData.stackingGroup, []).append( (1 + appliedMwBoost / 100, mwData.resAttrID)) mobileWebs.remove(mwData) maxTackledSpeed = tgt.getMaxVelocity( extraMultipliers=appliedMultipliers, ignoreAfflictors=tgtScrammables) currentTackledSpeed = maxTackledSpeed * speedRatio # Ensure consistent results - round off a little to avoid float errors return floatUnerr(currentTackledSpeed)
def getWebbedSpeed(src, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighters, distance): # Can slow down non-immune ships and target profiles if tgt.isFit and tgt.item.ship.getModifiedItemAttr( 'disallowOffensiveModifiers'): return currentUnwebbedSpeed maxUnwebbedSpeed = tgt.getMaxVelocity() try: speedRatio = currentUnwebbedSpeed / maxUnwebbedSpeed except ZeroDivisionError: currentWebbedSpeed = 0 else: appliedMultipliers = {} # Modules first, they are applied always the same way for wData in webMods: appliedBoost = wData.boost * _calcRangeFactor( atkOptimalRange=wData.optimal, atkFalloffRange=wData.falloff, distance=distance) if appliedBoost: appliedMultipliers.setdefault(wData.stackingGroup, []).append( (1 + appliedBoost / 100, wData.resAttrID)) maxWebbedSpeed = tgt.getMaxVelocity( extraMultipliers=appliedMultipliers) currentWebbedSpeed = maxWebbedSpeed * speedRatio # Drones and fighters mobileWebs = [] mobileWebs.extend(webFighters) # Drones have range limit if distance is None or distance <= src.item.extraAttributes[ 'droneControlRange']: mobileWebs.extend(webDrones) atkRadius = src.getRadius() # As mobile webs either follow the target or stick to the attacking ship, # if target is within mobile web optimal - it can be applied unconditionally longEnoughMws = [ mw for mw in mobileWebs if distance is None or distance <= mw.optimal - atkRadius + mw.radius ] if longEnoughMws: for mwData in longEnoughMws: appliedMultipliers.setdefault(mwData.stackingGroup, []).append( (1 + mwData.boost / 100, mwData.resAttrID)) mobileWebs.remove(mwData) maxWebbedSpeed = tgt.getMaxVelocity( extraMultipliers=appliedMultipliers) currentWebbedSpeed = maxWebbedSpeed * speedRatio # Apply remaining webs, from fastest to slowest droneOpt = GraphSettings.getInstance().get('mobileDroneMode') while mobileWebs: # Process in batches unified by speed to save up resources fastestMwSpeed = max(mobileWebs, key=lambda mw: mw.speed).speed fastestMws = [ mw for mw in mobileWebs if mw.speed == fastestMwSpeed ] for mwData in fastestMws: # Faster than target or set to follow it - apply full slowdown if (droneOpt == GraphDpsDroneMode.auto and mwData.speed >= currentWebbedSpeed ) or droneOpt == GraphDpsDroneMode.followTarget: appliedMwBoost = mwData.boost # Otherwise project from the center of the ship else: if distance is None: rangeFactorDistance = None else: rangeFactorDistance = distance + atkRadius - mwData.radius appliedMwBoost = mwData.boost * _calcRangeFactor( atkOptimalRange=mwData.optimal, atkFalloffRange=mwData.falloff, distance=rangeFactorDistance) appliedMultipliers.setdefault(mwData.stackingGroup, []).append( (1 + appliedMwBoost / 100, mwData.resAttrID)) mobileWebs.remove(mwData) maxWebbedSpeed = tgt.getMaxVelocity( extraMultipliers=appliedMultipliers) currentWebbedSpeed = maxWebbedSpeed * speedRatio # Ensure consistent results - round off a little to avoid float errors return floatUnerr(currentWebbedSpeed)