Exemplo n.º 1
0
 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
Exemplo n.º 2
0
 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]
Exemplo n.º 3
0
 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))
Exemplo n.º 4
0
 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
Exemplo n.º 5
0
 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
Exemplo n.º 6
0
 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
Exemplo n.º 7
0
 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
Exemplo n.º 8
0
 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
Exemplo n.º 9
0
def valToStr(val):
    if val is None:
        return ''
    val = floatUnerr(val)
    if int(val) == val:
        val = int(val)
    return str(val)
Exemplo n.º 10
0
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
Exemplo n.º 11
0
 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))
Exemplo n.º 12
0
 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
Exemplo n.º 13
0
 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
Exemplo n.º 14
0
 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
Exemplo n.º 15
0
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)
Exemplo n.º 16
0
 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
Exemplo n.º 17
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
Exemplo n.º 18
0
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
Exemplo n.º 19
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
Exemplo n.º 20
0
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
Exemplo n.º 21
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)
Exemplo n.º 22
0
 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
Exemplo n.º 24
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)
Exemplo n.º 25
0
    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
Exemplo n.º 26
0
 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 ''
Exemplo n.º 27
0
 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
Exemplo n.º 28
0
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
Exemplo n.º 29
0
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
Exemplo n.º 30
0
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
Exemplo n.º 31
0
 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()
Exemplo n.º 32
0
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)
Exemplo n.º 33
0
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)