Files
pyfa/eos/saveddata/fit.py
2025-12-09 14:41:40 +01:00

1962 lines
85 KiB
Python

# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
import datetime
import time
from copy import deepcopy
from itertools import chain
from math import ceil, log, sqrt
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
import eos.db
from eos import capSim
from eos.calc import calculateLockTime, calculateMultiplier
from eos.const import CalcType, FitSystemSecurity, FittingHardpoint, FittingModuleState, FittingSlot, ImplantLocation
from eos.effectHandlerHelpers import (
HandledBoosterList, HandledDroneCargoList, HandledImplantList,
HandledModuleList, HandledProjectedDroneList, HandledProjectedModList)
from eos.saveddata.character import Character
from eos.saveddata.citadel import Citadel
from eos.saveddata.damagePattern import DamagePattern
from eos.saveddata.module import Module
from eos.saveddata.ship import Ship
from eos.saveddata.targetProfile import TargetProfile
from eos.utils.float import floatUnerr
from eos.utils.stats import DmgTypes, RRTypes
pyfalog = Logger(__name__)
def _t(x):
return x
class FitLite:
def __init__(self, id=None, name=None, shipID=None, shipName=None, shipNameShort=None):
self.ID = id
self.name = name
self.shipID = shipID
self.shipName = shipName
self.shipNameShort = shipNameShort
def __repr__(self):
return 'FitLite(ID={})'.format(self.ID)
class Fit:
"""Represents a fitting, with modules, ship, implants, etc."""
PEAK_RECHARGE = 0.25
def __init__(self, ship=None, name=""):
"""Initialize a fit from the program"""
self.__ship = None
self.__mode = None
# use @mode.setter's to set __attr and IDs. This will set mode as well
self.ship = ship
if self.ship:
self.ship.owner = self
self.__modules = HandledModuleList()
self.__drones = HandledDroneCargoList()
self.__fighters = HandledDroneCargoList()
self.__cargo = HandledDroneCargoList()
self.__implants = HandledImplantList()
self.__boosters = HandledBoosterList()
# self.__projectedFits = {}
self.__projectedModules = HandledProjectedModList()
self.__projectedDrones = HandledProjectedDroneList()
self.__projectedFighters = HandledProjectedDroneList()
self.__character = None
self.__owner = None
self.projected = False
self.name = name
self.timestamp = time.time()
self.created = None
self.modified = None
self.modeID = None
self.build()
@reconstructor
def init(self):
"""Initialize a fit from the database and validate"""
self.__ship = None
self.__mode = None
if self.shipID:
item = eos.db.getItem(self.shipID)
if item is None:
pyfalog.error("Item (id: {0}) does not exist", self.shipID)
return
try:
try:
self.__ship = Ship(item, self)
except ValueError:
self.__ship = Citadel(item, self)
# @todo extra attributes is now useless, however it set to be
# the same as ship attributes for ease (so we don't have to
# change all instances in source). Remove this at some point
self.extraAttributes = self.__ship.itemModifiedAttributes
except ValueError:
pyfalog.error("Item (id: {0}) is not a Ship", self.shipID)
return
if self.modeID and self.__ship:
item = eos.db.getItem(self.modeID)
# Don't need to verify if it's a proper item, as validateModeItem assures this
self.__mode = self.ship.validateModeItem(item, owner=self)
else:
self.__mode = self.ship.validateModeItem(None, owner=self)
self.build()
def build(self):
self.__extraDrains = []
self.__ehp = None
self.__weaponDpsMap = {}
self.__weaponVolleyMap = {}
self.__remoteRepMap = {}
self.__minerYield = None
self.__droneYield = None
self.__minerDrain = None
self.__droneDrain = None
self.__droneDps = None
self.__droneVolley = None
self.__sustainableTank = None
self.__effectiveSustainableTank = None
self.__effectiveTank = None
self.__calculated = False
self.__capStable = None
self.__capState = None
self.__capUsed = None
self.__capRecharge = None
self.__savedCapSimData = {}
self.__calculatedTargets = []
self.factorReload = False
self.boostsFits = set()
self.gangBoosts = None
self.__ecmProjectedList = []
self.commandBonuses = {}
# Reps received, as a list of (amount, cycle time in seconds)
self._hullRr = []
self._armorRr = []
self._armorRrPreSpool = []
self._armorRrFullSpool = []
self._shieldRr = []
def clearFactorReloadDependentData(self):
# Here we clear all data known to rely on cycle parameters
# (which, in turn, relies on factor reload flag)
self.__weaponDpsMap.clear()
self.__droneDps = None
self.__remoteRepMap.clear()
self.__capStable = None
self.__capState = None
self.__capUsed = None
self.__capRecharge = None
self.__savedCapSimData.clear()
# Ancillary tank modules affect this
self.__sustainableTank = None
self.__effectiveSustainableTank = None
@property
def targetProfile(self):
if self.__userTargetProfile is not None:
return self.__userTargetProfile
if self.__builtinTargetProfileID is not None:
return TargetProfile.getBuiltinById(self.__builtinTargetProfileID)
return None
@targetProfile.setter
def targetProfile(self, targetProfile):
if targetProfile is None:
self.__userTargetProfile = None
self.__builtinTargetProfileID = None
elif targetProfile.builtin:
self.__userTargetProfile = None
self.__builtinTargetProfileID = targetProfile.ID
else:
self.__userTargetProfile = targetProfile
self.__builtinTargetProfileID = None
self.__weaponDpsMap = {}
self.__weaponVolleyMap = {}
self.__droneDps = None
self.__droneVolley = None
@property
def damagePattern(self):
if self.__userDamagePattern is not None:
return self.__userDamagePattern
if self.__builtinDamagePatternID is not None:
pattern = DamagePattern.getBuiltinById(self.__builtinDamagePatternID)
if pattern is not None:
return pattern
return DamagePattern.getDefaultBuiltin()
@damagePattern.setter
def damagePattern(self, damagePattern):
if damagePattern is None:
self.__userDamagePattern = None
self.__builtinDamagePatternID = None
elif damagePattern.builtin:
self.__userDamagePattern = None
self.__builtinDamagePatternID = damagePattern.ID
else:
self.__userDamagePattern = damagePattern
self.__builtinDamagePatternID = None
self.__ehp = None
self.__effectiveTank = None
@property
def isInvalid(self):
return self.__ship is None
@property
def mode(self):
return self.__mode
@mode.setter
def mode(self, mode):
if self.__mode is not None:
self.__mode.owner = None
self.__mode = mode
self.modeID = mode.item.ID if mode is not None else None
if mode is not None:
mode.owner = self
@property
def modifiedCoalesce(self):
"""
This is a property that should get whichever date is available for the fit. @todo: migrate old timestamp data
and ensure created / modified are set in database to get rid of this
"""
return self.modified or self.created or datetime.datetime.fromtimestamp(self.timestamp)
@property
def character(self):
return self.__character if self.__character is not None else Character.getAll0()
@character.setter
def character(self, char):
self.__character = char
@property
def calculated(self):
return self.__calculated
@calculated.setter
def calculated(self, bool):
# todo: brief explaination hwo this works
self.__calculated = bool
@property
def ship(self):
return self.__ship
@ship.setter
def ship(self, ship):
if self.__ship is not None:
self.__ship.owner = None
self.__ship = ship
self.shipID = ship.item.ID if ship is not None else None
if ship is not None:
ship.owner = self
# set mode of new ship
self.mode = self.ship.validateModeItem(None, owner=self) if ship is not None else None
# set fit attributes the same as ship
self.extraAttributes = self.ship.itemModifiedAttributes
@property
def isStructure(self):
return isinstance(self.ship, Citadel)
@property
def drones(self):
return self.__drones
@property
def fighters(self):
return self.__fighters
@property
def cargo(self):
return self.__cargo
@property
def modules(self):
return self.__modules
@property
def implants(self):
return self.__implants
@property
def boosters(self):
return self.__boosters
@property
def projectedModules(self):
return self.__projectedModules
@property
def projectedFits(self):
# only in extreme edge cases will the fit be invalid, but to be sure do
# not return them.
return [fit for fit in list(self.projectedFitDict.values()) if not fit.isInvalid]
@property
def commandFits(self):
return [fit for fit in list(self.commandFitDict.values()) if not fit.isInvalid]
def getProjectionInfo(self, fitID):
return self.projectedOnto.get(fitID, None)
def getCommandInfo(self, fitID):
return self.boostedOnto.get(fitID, None)
@property
def projectedDrones(self):
return self.__projectedDrones
@property
def projectedFighters(self):
return self.__projectedFighters
def getWeaponDps(self, spoolOptions=None):
if spoolOptions not in self.__weaponDpsMap:
self.calculateWeaponDmgStats(spoolOptions)
return self.__weaponDpsMap[spoolOptions]
def getWeaponVolley(self, spoolOptions=None):
if spoolOptions not in self.__weaponVolleyMap:
self.calculateWeaponDmgStats(spoolOptions)
return self.__weaponVolleyMap[spoolOptions]
def getDroneDps(self):
if self.__droneDps is None:
self.calculateDroneDmgStats()
return self.__droneDps
def getDroneVolley(self):
if self.__droneVolley is None:
self.calculateDroneDmgStats()
return self.__droneVolley
def getTotalDps(self, spoolOptions=None):
return self.getDroneDps() + self.getWeaponDps(spoolOptions=spoolOptions)
def getTotalVolley(self, spoolOptions=None):
return self.getDroneVolley() + self.getWeaponVolley(spoolOptions=spoolOptions)
@property
def minerYield(self):
if self.__minerYield is None:
self.calculatemining()
return self.__minerYield
@property
def minerDrain(self):
if self.__minerDrain is None:
self.calculatemining()
return self.__minerDrain
@property
def droneYield(self):
if self.__droneYield is None:
self.calculatemining()
return self.__droneYield
@property
def droneDrain(self):
if self.__droneDrain is None:
self.calculatemining()
return self.__droneDrain
@property
def totalYield(self):
return self.droneYield + self.minerYield
@property
def totalDrain(self):
return self.droneDrain + self.minerDrain
@property
def maxTargets(self):
maxTargets = min(self.extraAttributes["maxTargetsLockedFromSkills"],
self.ship.getModifiedItemAttr("maxLockedTargets"))
return ceil(floatUnerr(maxTargets))
@property
def maxTargetRange(self):
return self.ship.getModifiedItemAttr("maxTargetRange")
@property
def scanStrength(self):
return max([self.ship.getModifiedItemAttr("scan%sStrength" % scanType)
for scanType in ("Magnetometric", "Ladar", "Radar", "Gravimetric")])
@property
def scanType(self):
maxStr = -1
type_ = None
for scanType in (_t("Magnetometric"), _t("Ladar"), _t("Radar"), _t("Gravimetric")):
currStr = self.ship.getModifiedItemAttr("scan%sStrength" % scanType)
if currStr > maxStr:
maxStr = currStr
type_ = scanType
elif currStr == maxStr:
type_ = _t("Multispectral")
return type_
@property
def jamChance(self):
sensors = self.scanStrength
retainLockChance = 1
for jamStr in self.__ecmProjectedList:
retainLockChance *= 1 - min(1, jamStr / sensors)
return (1 - retainLockChance) * 100
@property
def maxSpeed(self):
speedLimit = self.ship.getModifiedItemAttr("speedLimit")
if speedLimit and self.ship.getModifiedItemAttr("maxVelocity") > speedLimit:
return speedLimit
return self.ship.getModifiedItemAttr("maxVelocity")
@property
def alignTime(self):
agility = self.ship.getModifiedItemAttr("agility") or 0
mass = self.ship.getModifiedItemAttr("mass")
return -log(0.25) * agility * mass / 1000000
@property
def implantSource(self):
return self.implantLocation
@implantSource.setter
def implantSource(self, source):
self.implantLocation = source
@property
def appliedImplants(self):
if self.implantLocation == ImplantLocation.CHARACTER:
return self.character.implants
else:
return self.implants
@validates("ID", "ownerID", "shipID")
def validator(self, key, val):
map = {
"ID": lambda _val: isinstance(_val, int),
"ownerID": lambda _val: isinstance(_val, int) or _val is None,
"shipID": lambda _val: isinstance(_val, int) or _val is None
}
if not map[key](val):
raise ValueError(str(val) + " is not a valid value for " + key)
else:
return val
def canFit(self, item):
# Whereas Module.fits() deals with current state of the fit in order to determine if somethign fits (for example maxGroupFitted which can be modified by effects),
# this function should be used against Items to see if the item is even allowed on the fit with rules that don't change
fitsOnType = set()
fitsOnGroup = set()
shipType = item.attributes.get("fitsToShipType", None)
if shipType is not None:
fitsOnType.add(shipType.value)
fitsOnType.update([item.attributes[attr].value for attr in item.attributes if attr.startswith("canFitShipType")])
fitsOnGroup.update([item.attributes[attr].value for attr in item.attributes if attr.startswith("canFitShipGroup")])
if (len(fitsOnGroup) > 0 or len(fitsOnType) > 0) \
and self.ship.item.group.ID not in fitsOnGroup \
and self.ship.item.ID not in fitsOnType:
return False
# Citadel modules are now under a new category, so we can check this to ensure only structure modules can fit on a citadel
if isinstance(self.ship, Citadel) is not item.isStandup:
return False
return True
def clear(self, projected=False, command=False):
self.__effectiveTank = None
self.__weaponDpsMap = {}
self.__weaponVolleyMap = {}
self.__remoteRepMap = {}
self.__minerYield = None
self.__droneYield = None
self.__minerDrain = None
self.__droneDrain = None
self.__effectiveSustainableTank = None
self.__sustainableTank = None
self.__droneDps = None
self.__droneVolley = None
self.__ehp = None
self.__calculated = False
self.__capStable = None
self.__capState = None
self.__capUsed = None
self.__capRecharge = None
self.__savedCapSimData.clear()
self.__ecmProjectedList = []
# self.commandBonuses = {}
del self.__calculatedTargets[:]
del self.__extraDrains[:]
if self.ship:
self.ship.clear()
c = chain(
self.modules,
self.drones,
self.fighters,
self.boosters,
self.implants,
self.projectedDrones,
self.projectedModules,
self.projectedFighters,
(self.character, self.extraAttributes),
)
for stuff in c:
if stuff is not None and stuff != self:
stuff.clear()
self._hullRr.clear()
self._armorRr.clear()
self._armorRrPreSpool.clear()
self._armorRrFullSpool.clear()
self._shieldRr.clear()
# If this is the active fit that we are clearing, not a projected fit,
# then this will run and clear the projected ships and flag the next
# iteration to skip this part to prevent recursion.
# if not projected:
# for stuff in self.projectedFits:
# if stuff is not None and stuff != self:
# stuff.clear(projected=True)
#
# if not command:
# for stuff in self.commandFits:
# if stuff is not None and stuff != self:
# stuff.clear(command=True)
# Methods to register and get the thing currently affecting the fit,
# so we can correctly map "Affected By"
def register(self, currModifier, origin=None):
self.__modifier = currModifier
self.__origin = origin
if hasattr(currModifier, "itemModifiedAttributes"):
if hasattr(currModifier.itemModifiedAttributes, "fit"):
currModifier.itemModifiedAttributes.fit = origin or self
if hasattr(currModifier, "chargeModifiedAttributes"):
if hasattr(currModifier.chargeModifiedAttributes, "fit"):
currModifier.chargeModifiedAttributes.fit = origin or self
def getModifier(self):
return self.__modifier
def getOrigin(self):
return self.__origin
def addCommandBonus(self, warfareBuffID, value, module, effect, runTime="normal"):
# oh fuck this is so janky
# @todo should we pass in min/max to this function, or is abs okay?
# (abs is old method, ccp now provides the aggregate function in their data)
if warfareBuffID not in self.commandBonuses or abs(self.commandBonuses[warfareBuffID][1]) < abs(value):
self.commandBonuses[warfareBuffID] = (runTime, value, module, effect)
def addProjectedEcm(self, strength):
self.__ecmProjectedList.append(strength)
def __runCommandBoosts(self, runTime="normal"):
pyfalog.debug("Applying gang boosts for {0}", repr(self))
for warfareBuffID in list(self.commandBonuses.keys()):
# Unpack all data required to run effect properly
effect_runTime, value, thing, effect = self.commandBonuses[warfareBuffID]
if runTime != effect_runTime:
continue
# This should always be a gang effect, otherwise it wouldn't be added to commandBonuses
if effect.isType("gang"):
self.register(thing)
if warfareBuffID == 10: # Shield Burst: Shield Harmonizing: Shield Resistance
for damageType in ("Em", "Explosive", "Thermal", "Kinetic"):
self.ship.boostItemAttr("shield%sDamageResonance" % damageType, value, stackingPenalties=True)
if warfareBuffID == 11: # Shield Burst: Active Shielding: Repair Duration/Capacitor
self.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill("Shield Operation") or
mod.item.requiresSkill("Shield Emission Systems") or
mod.item.requiresSkill("Capital Shield Emission Systems"),
"capacitorNeed", value)
self.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill("Shield Operation") or
mod.item.requiresSkill("Shield Emission Systems") or
mod.item.requiresSkill("Capital Shield Emission Systems"),
"duration", value)
if warfareBuffID == 12: # Shield Burst: Shield Extension: Shield HP
self.ship.boostItemAttr("shieldCapacity", value)
if warfareBuffID == 13: # Armor Burst: Armor Energizing: Armor Resistance
for damageType in ("Em", "Thermal", "Explosive", "Kinetic"):
self.ship.boostItemAttr("armor%sDamageResonance" % damageType, value, stackingPenalties=True)
if warfareBuffID == 14: # Armor Burst: Rapid Repair: Repair Duration/Capacitor
self.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or
mod.item.requiresSkill("Repair Systems") or
mod.item.requiresSkill("Capital Remote Armor Repair Systems"),
"capacitorNeed", value)
self.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or
mod.item.requiresSkill("Repair Systems") or
mod.item.requiresSkill("Capital Remote Armor Repair Systems"),
"duration", value)
if warfareBuffID == 15: # Armor Burst: Armor Reinforcement: Armor HP
self.ship.boostItemAttr("armorHP", value)
if warfareBuffID == 16: # Information Burst: Sensor Optimization: Scan Resolution
self.ship.boostItemAttr("scanResolution", value, stackingPenalties=True)
if warfareBuffID == 17: # Information Burst: Electronic Superiority: EWAR Range and Strength
groups = ("ECM", "Sensor Dampener", "Weapon Disruptor", "Target Painter")
self.modules.filteredItemBoost(lambda mod: mod.item.group.name in groups, "maxRange", value,
stackingPenalties=True)
self.modules.filteredItemBoost(lambda mod: mod.item.group.name in groups,
"falloffEffectiveness", value, stackingPenalties=True)
for scanType in ("Magnetometric", "Radar", "Ladar", "Gravimetric"):
self.modules.filteredItemBoost(lambda mod: mod.item.group.name == "ECM",
"scan%sStrengthBonus" % scanType, value,
stackingPenalties=True)
for attr in ("missileVelocityBonus", "explosionDelayBonus", "aoeVelocityBonus", "falloffBonus",
"maxRangeBonus", "aoeCloudSizeBonus", "trackingSpeedBonus"):
self.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Weapon Disruptor",
attr, value)
for attr in ("maxTargetRangeBonus", "scanResolutionBonus"):
self.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Sensor Dampener",
attr, value)
self.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Target Painter",
"signatureRadiusBonus", value, stackingPenalties=True)
if warfareBuffID == 18: # Information Burst: Electronic Hardening: Scan Strength
for scanType in ("Gravimetric", "Radar", "Ladar", "Magnetometric"):
self.ship.boostItemAttr("scan%sStrength" % scanType, value, stackingPenalties=True)
if warfareBuffID == 19: # Information Burst: Electronic Hardening: RSD/RWD Resistance
self.ship.boostItemAttr("sensorDampenerResistance", value)
self.ship.boostItemAttr("weaponDisruptionResistance", value)
if warfareBuffID == 20: # Skirmish Burst: Evasive Maneuvers: Signature Radius
self.ship.boostItemAttr("signatureRadius", value, stackingPenalties=True)
if warfareBuffID == 21: # Skirmish Burst: Interdiction Maneuvers: Tackle Range
groups = ("Stasis Web", "Warp Scrambler")
self.modules.filteredItemBoost(lambda mod: mod.item.group.name in groups, "maxRange", value,
stackingPenalties=True)
if warfareBuffID == 22: # Skirmish Burst: Rapid Deployment: AB/MWD Speed Increase
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Afterburner") or
mod.item.requiresSkill("High Speed Maneuvering"),
"speedFactor", value, stackingPenalties=True)
if warfareBuffID == 23: # Mining Burst: Mining Laser Field Enhancement: Mining Range
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or
mod.item.requiresSkill("Ice Harvesting") or
mod.item.requiresSkill("Gas Cloud Harvesting"),
"maxRange", value, stackingPenalties=True)
if warfareBuffID == 24: # Mining Burst: Mining Laser Optimization: Mining Capacitor/Duration
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or
mod.item.requiresSkill("Ice Harvesting") or
mod.item.requiresSkill("Gas Cloud Harvesting"),
"capacitorNeed", value, stackingPenalties=True)
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or
mod.item.requiresSkill("Ice Harvesting") or
mod.item.requiresSkill("Gas Cloud Harvesting"),
"duration", value, stackingPenalties=True)
if warfareBuffID == 25: # Mining Burst: Mining Equipment Preservation: Crystal Volatility
self.modules.filteredChargeBoost(lambda mod: mod.item.requiresSkill("Mining"),
"crystalVolatilityChance", value, stackingPenalties=True)
if warfareBuffID == 26: # Information Burst: Sensor Optimization: Targeting Range
self.ship.boostItemAttr("maxTargetRange", value, stackingPenalties=True)
if warfareBuffID == 60: # Skirmish Burst: Evasive Maneuvers: Agility
self.ship.boostItemAttr("agility", value, stackingPenalties=True)
# Titan effects
if warfareBuffID == 39: # Avatar Effect Generator : Capacitor Recharge bonus
self.ship.boostItemAttr("rechargeRate", value, stackingPenalties=True)
if warfareBuffID == 40: # Avatar Effect Generator : Kinetic resistance bonus
for attr in ("armorKineticDamageResonance", "shieldKineticDamageResonance", "kineticDamageResonance"):
self.ship.boostItemAttr(attr, value, stackingPenalties=True)
if warfareBuffID == 41: # Avatar Effect Generator : EM resistance penalty
for attr in ("armorEmDamageResonance", "shieldEmDamageResonance", "emDamageResonance"):
self.ship.boostItemAttr(attr, value, stackingPenalties=True)
if warfareBuffID == 42: # Erebus Effect Generator : Armor HP bonus
self.ship.boostItemAttr("armorHP", value)
if warfareBuffID == 43: # Erebus Effect Generator : Explosive resistance bonus
for attr in ("armorExplosiveDamageResonance", "shieldExplosiveDamageResonance", "explosiveDamageResonance"):
self.ship.boostItemAttr(attr, value, stackingPenalties=True)
if warfareBuffID == 44: # Erebus Effect Generator : Thermal resistance penalty
for attr in ("armorThermalDamageResonance", "shieldThermalDamageResonance", "thermalDamageResonance"):
self.ship.boostItemAttr(attr, value, stackingPenalties=True)
if warfareBuffID == 45: # Ragnarok Effect Generator : Signature Radius bonus
self.ship.boostItemAttr("signatureRadius", value, stackingPenalties=True)
if warfareBuffID == 46: # Ragnarok Effect Generator : Thermal resistance bonus
for attr in ("armorThermalDamageResonance", "shieldThermalDamageResonance", "thermalDamageResonance"):
self.ship.boostItemAttr(attr, value, stackingPenalties=True)
if warfareBuffID == 47: # Ragnarok Effect Generator : Explosive resistance penaly
for attr in ("armorExplosiveDamageResonance", "shieldExplosiveDamageResonance", "explosiveDamageResonance"):
self.ship.boostItemAttr(attr, value, stackingPenalties=True)
if warfareBuffID == 48: # Leviathan Effect Generator : Shield HP bonus
self.ship.boostItemAttr("shieldCapacity", value)
if warfareBuffID == 49: # Leviathan Effect Generator : EM resistance bonus
for attr in ("armorEmDamageResonance", "shieldEmDamageResonance", "emDamageResonance"):
self.ship.boostItemAttr(attr, value, stackingPenalties=True)
if warfareBuffID == 50: # Leviathan Effect Generator : Kinetic resistance penalty
for attr in ("armorKineticDamageResonance", "shieldKineticDamageResonance", "kineticDamageResonance"):
self.ship.boostItemAttr(attr, value, stackingPenalties=True)
if warfareBuffID == 51: # Avatar Effect Generator : Velocity penalty
self.ship.boostItemAttr("maxVelocity", value, stackingPenalties=True)
if warfareBuffID == 52: # Erebus Effect Generator : Shield RR penalty
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Shield Emission Systems"), "shieldBonus", value, stackingPenalties=True)
if warfareBuffID == 53: # Leviathan Effect Generator : Armor RR penalty
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems"),
"armorDamageAmount", value, stackingPenalties=True)
if warfareBuffID == 54: # Ragnarok Effect Generator : Laser and Hybrid Optimal penalty
groups = ("Energy Weapon", "Hybrid Weapon")
self.modules.filteredItemBoost(lambda mod: mod.item.group.name in groups, "maxRange", value, stackingPenalties=True)
# Localized environment effects
if warfareBuffID == 79: # AOE_Beacon_bioluminescence_cloud
self.ship.boostItemAttr("signatureRadius", value, stackingPenalties=True)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"signatureRadius", value, stackingPenalties=True)
if warfareBuffID == 80: # AOE_Beacon_caustic_cloud_inertia
self.ship.boostItemAttr("agility", value, stackingPenalties=True)
if warfareBuffID == 81: # AOE_Beacon_caustic_cloud_velocity
self.ship.boostItemAttr("maxVelocity", value, stackingPenalties=True)
if warfareBuffID == 88: # AOE_Beacon_filament_cloud_shield_booster_shield_bonus
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Shield Operation"),
"shieldBonus", value, stackingPenalties=True)
if warfareBuffID == 89: # AOE_Beacon_filament_cloud_shield_booster_duration
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Shield Operation"),
"duration", value, stackingPenalties=True)
# Abyssal Weather Effects
if warfareBuffID == 90: # Weather_electric_storm_EM_resistance_penalty
for tankType in ("shield", "armor"):
self.ship.boostItemAttr("{}EmDamageResonance".format(tankType), value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"{}EmDamageResonance".format(tankType), value)
self.ship.boostItemAttr("emDamageResonance", value) # for hull
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"emDamageResonance", value) #for hull
if warfareBuffID == 92: # Weather_electric_storm_capacitor_recharge_bonus
self.ship.boostItemAttr("rechargeRate", value, stackingPenalties=True)
if warfareBuffID == 93: # Weather_xenon_gas_explosive_resistance_penalty
for tankType in ("shield", "armor"):
self.ship.boostItemAttr("{}ExplosiveDamageResonance".format(tankType), value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"{}ExplosiveDamageResonance".format(tankType), value)
self.ship.boostItemAttr("explosiveDamageResonance", value) # for hull
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"explosiveDamageResonance", value) # for hull
if warfareBuffID == 94: # Weather_xenon_gas_shield_hp_bonus
self.ship.boostItemAttr("shieldCapacity", value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"shieldCapacity", value)
if warfareBuffID == 95: # Weather_infernal_thermal_resistance_penalty
for tankType in ("shield", "armor"):
self.ship.boostItemAttr("{}ThermalDamageResonance".format(tankType), value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"{}ThermalDamageResonance".format(tankType), value)
self.ship.boostItemAttr("thermalDamageResonance", value) # for hull
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"thermalDamageResonance", value) # for hull
if warfareBuffID == 96: # Weather_infernal_armor_hp_bonus
self.ship.boostItemAttr("armorHP", value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"armorHP", value)
if warfareBuffID == 97: # Weather_darkness_turret_range_penalty
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"),
"maxRange", value, stackingPenalties=True)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"maxRange", value, stackingPenalties=True)
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"),
"falloff", value, stackingPenalties=True)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"falloff", value, stackingPenalties=True)
if warfareBuffID == 98: # Weather_darkness_velocity_bonus
self.ship.boostItemAttr("maxVelocity", value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"maxVelocity", value)
if warfareBuffID == 99: # Weather_caustic_toxin_kinetic_resistance_penalty
for tankType in ("shield", "armor"):
self.ship.boostItemAttr("{}KineticDamageResonance".format(tankType), value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"{}KineticDamageResonance".format(tankType), value)
self.ship.boostItemAttr("kineticDamageResonance", value) # for hull
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"kineticDamageResonance", value) # for hull
if warfareBuffID == 100: # Weather_caustic_toxin_scan_resolution_bonus
self.ship.boostItemAttr("scanResolution", value, stackingPenalties=True)
if warfareBuffID == 2405: # Insurgency Suppression Bonus: Interdiction Range
self.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill("Navigation"),
"maxRange", value, stackingPenalties=True)
self.modules.filteredItemBoost(
lambda mod: mod.item.group.name == "Stasis Web",
"maxRange", value, stackingPenalties=True)
# Sov upgrades buffs
if warfareBuffID == 2433: # Sov System Modifier Shield HP Bonus
self.ship.boostItemAttr("shieldCapacity", value)
if warfareBuffID == 2434: # Sov System Modifier Capacitor Capacity Bonus
self.ship.boostItemAttr("capacitorCapacity", value)
if warfareBuffID == 2435: # Sov System Modifier Armor HP Bonus
self.ship.boostItemAttr("armorHP", value)
if warfareBuffID == 2436: # Sov System Modifier Overheating Bonus - Includes Ewar
for attr in (
'overloadDurationBonus', 'overloadRofBonus', 'overloadSelfDurationBonus',
'overloadHardeningBonus', 'overloadDamageModifier', 'overloadRangeBonus',
'overloadSpeedFactorBonus', 'overloadECMStrengthBonus', 'overloadECCMStrenghtBonus',
'overloadArmorDamageAmount', 'overloadShieldBonus', 'overloadTrackingModuleStrengthBonus',
'overloadSensorModuleStrengthBonus', 'overloadPainterStrengthBonus',
):
self.modules.filteredItemBoost(lambda mod: attr in mod.itemModifiedAttributes, attr, value)
if warfareBuffID == 2437: # Sov System Modifier Capacitor Recharge Bonus
self.ship.boostItemAttr("rechargeRate", value)
if warfareBuffID == 2438: # Sov System Modifier Targeting and DScan Range Bonus
self.ship.boostItemAttr("maxTargetRange", value)
self.ship.boostItemAttr("maxDirectionalScanRange", value)
if warfareBuffID == 2439: # Sov System Modifier Scan Resolution Bonus
self.ship.boostItemAttr("scanResolution", value)
if warfareBuffID == 2440: # Sov System Modifier Warp Speed Addition
self.ship.increaseItemAttr('warpSpeedMultiplier', value)
if warfareBuffID == 2441: # Sov System Modifier Shield Booster Bonus
self.modules.filteredItemBoost(
lambda mod: (mod.item.requiresSkill("Shield Operation")
or mod.item.requiresSkill("Capital Shield Operation")),
"shieldBonus", value, stackingPenalties=True)
if warfareBuffID == 2442: # Sov System Modifier Armor Repairer Bonus
self.modules.filteredItemBoost(
lambda mod: (mod.item.requiresSkill("Repair Systems")
or mod.item.requiresSkill("Capital Repair Systems")),
"armorDamageAmount", value, stackingPenalties=True)
if warfareBuffID == 2464: # Expedition Burst: Probe Strength
self.modules.filteredChargeBoost(
lambda mod: mod.charge.requiresSkill('Astrometrics'),
'baseSensorStrength', value, stackingPenalties=True)
if warfareBuffID == 2465: # Expedition Burst: Directional Scanner, Hacking and Salvager Range
self.ship.boostItemAttr("maxDirectionalScanRange", value)
self.modules.filteredItemBoost(
lambda mod: mod.item.group.name in ("Data Miners", "Salvager"), "maxRange", value, stackingPenalties=True)
if warfareBuffID == 2466: # Expedition Burst: Maximum Scan Deviation Modifier
self.modules.filteredChargeBoost(
lambda mod: mod.charge.requiresSkill('Astrometrics'),
'baseMaxScanDeviation', value, stackingPenalties=True)
if warfareBuffID == 2468: # Expedition Burst: Virus Coherence
self.modules.filteredItemIncrease(
lambda mod: mod.item.group.name == "Data Miners", "virusCoherence", value)
if warfareBuffID == 2474: # Mining burst charges
self.ship.forceItemAttr("miningScannerUpgrade", value)
if warfareBuffID == 2481: # Expedition Burst: Salvager duration bonus
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Salvaging"), "duration", value)
if warfareBuffID == 2516: # Mining Burst: Mining Crit Chance
self.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill("Mining") or mod.item.requiresSkill("Ice Harvesting"),
"miningCritChance", value)
if warfareBuffID == 2517: # Mining Burst: Mining Residue Chance Reduction
self.modules.filteredItemBoost(
lambda mod: (
mod.item.requiresSkill("Mining")
or mod.item.requiresSkill("Ice Harvesting")
or mod.item.requiresSkill("Gas Cloud Harvesting")),
"miningWasteProbability", value, stackingPenalties=True)
del self.commandBonuses[warfareBuffID]
def __resetDependentCalcs(self):
self.calculated = False
for value in list(self.projectedOnto.values()):
if value.victim_fit: # removing a self-projected fit causes victim fit to be None. @todo: look into why. :3
value.victim_fit.calculated = False
def calculateModifiedAttributes(self, targetFit=None, type=CalcType.LOCAL):
"""
The fit calculation function. It should be noted that this is a recursive function - if the local fit has
projected fits, this function will be called for those projected fits to be calculated.
Args:
targetFit:
If this is set, signals that we are currently calculating a remote fit (projected or command) that
should apply it's remote effects to the targetFit. If None, signals that we are currently calcing the
local fit
type:
The type of calculation our current iteration is in. This helps us determine the interactions between
fits that rely on others for proper calculations
"""
pyfalog.info("Starting fit calculation on: {0}, calc: {1}", repr(self), CalcType(type).name)
# If we are projecting this fit onto another one, collect the projection info for later use
# We also deal with self-projection here by setting self as a copy (to get a new fit object) to apply onto original fit
# First and foremost, if we're looking at a local calc, reset the calculated state of fits that this fit affects
# Thankfully, due to the way projection mechanics currently work, we don't have to traverse down a projection
# tree to (resetting the first degree of projection will suffice)
if targetFit is None:
# This resets all fits that local projects onto, allowing them to recalc when loaded
self.__resetDependentCalcs()
# For fits that are under local's Command, we do the same thing
for value in list(self.boostedOnto.values()):
# apparently this is a thing that happens when removing a command fit from a fit and then switching to
# that command fit. Same as projected clears, figure out why.
if value.boosted_fit:
value.boosted_fit.__resetDependentCalcs()
if targetFit and type == CalcType.PROJECTED:
pyfalog.debug("Calculating projections from {0} to target {1}", repr(self), repr(targetFit))
projectionInfo = self.getProjectionInfo(targetFit.ID)
# Start applying any command fits that we may have.
# We run the command calculations first so that they can calculate fully and store the command effects on the
# target fit to be used later on in the calculation. This does not apply when we're already calculating a
# command fit.
if type != CalcType.COMMAND and self.commandFits and not self.__calculated:
for fit in self.commandFits:
commandInfo = fit.getCommandInfo(self.ID)
# Continue loop if we're trying to apply ourselves or if this fit isn't active
if not commandInfo.active or self == commandInfo.booster_fit:
continue
commandInfo.booster_fit.calculateModifiedAttributes(self, CalcType.COMMAND)
# If we're not explicitly asked to project fit onto something,
# set self as target fit
if targetFit is None:
targetFit = self
# If fit is calculated and we have nothing to do here, get out
# A note on why we only do this for local fits. There may be
# gains that we can do here after some evaluation, but right
# now we need the projected and command fits to continue in
# this function even if they are already calculated, since it
# is during those calculations that they apply their effect
# to the target fits. todo: We could probably skip local fit
# calculations if calculated, and instead to projections and
# command stuffs. ninja edit: this is probably already being
# done with the calculated conditional in the calc loop
if self.__calculated and type == CalcType.LOCAL:
pyfalog.debug("Fit has already been calculated and is local, returning: {0}", self)
return
if not self.__calculated:
pyfalog.info("Fit is not yet calculated; will be running local calcs for {}".format(repr(self)))
self.clear()
# Loop through our run times here. These determine which effects are run in which order.
for runTime in ("early", "normal", "late"):
# pyfalog.debug("Run time: {0}", runTime)
# Items that are unrestricted. These items are run on the local fit
# first and then projected onto the target fit it one is designated
u = [
(self.character, self.ship),
self.drones,
self.fighters,
self.boosters,
self.appliedImplants,
self.modules
] if not self.isStructure else [
# Ensure a restricted set for citadels
(self.character, self.ship),
self.fighters,
self.modules
]
# Items that are restricted. These items are only run on the local
# fit. They are NOT projected onto the target fit. # See issue 354
r = [(self.mode,), self.projectedDrones, self.projectedFighters, self.projectedModules]
# chain unrestricted and restricted into one iterable
c = chain.from_iterable(u + r)
for item in c:
# Registering the item about to affect the fit allows us to
# track "Affected By" relations correctly
if item is not None:
# apply effects locally if this is first time running them on fit
if not self.__calculated:
self.register(item)
item.calculateModifiedAttributes(self, runTime, False)
# Run command effects against target fit. We only have to worry about modules
if type == CalcType.COMMAND and item in self.modules:
# Apply the gang boosts to target fit
# targetFit.register(item, origin=self)
item.calculateModifiedAttributes(targetFit, runTime, False, True)
# pyfalog.debug("Command Bonuses: {}".format(self.commandBonuses))
# If we are calculating our local or projected fit and have command bonuses, apply them
if type != CalcType.COMMAND and self.commandBonuses:
self.__runCommandBoosts(runTime)
# Run projection effects against target fit. Projection effects have been broken out of the main loop,
# see GH issue #1081
if type == CalcType.PROJECTED and projectionInfo:
self.__runProjectionEffects(runTime, targetFit, projectionInfo)
# Recursive command ships (A <-> B) get marked as calculated, which means that they aren't recalced when changing
# tabs. See GH issue 1193
if type == CalcType.COMMAND and targetFit in self.commandFits:
pyfalog.debug("{} is in the command listing for COMMAND ({}), do not mark self as calculated (recursive)".format(repr(targetFit), repr(self)))
else:
self.__calculated = True
# Only apply projected fits if fit it not projected itself.
if type == CalcType.LOCAL:
for fit in self.projectedFits:
projInfo = fit.getProjectionInfo(self.ID)
if projInfo.active:
if fit == self:
# If doing self projection, no need to run through the recursion process. Simply run the
# projection effects on ourselves
pyfalog.debug("Running self-projection for {0}", repr(self))
for runTime in ("early", "normal", "late"):
self.__runProjectionEffects(runTime, self, projInfo)
else:
fit.calculateModifiedAttributes(self, type=CalcType.PROJECTED)
pyfalog.debug('Done with fit calculation')
def __runProjectionEffects(self, runTime, targetFit, projectionInfo):
"""
To support a simpler way of doing self projections (so that we don't have to make a copy of the fit and
recalculate), this function was developed to be a common source of projected effect application.
"""
for item in chain(self.drones, self.fighters):
if item is not None:
# apply effects onto target fit x amount of times
for _ in range(projectionInfo.amount):
targetFit.register(item, origin=self)
item.calculateModifiedAttributes(
targetFit, runTime, forceProjected=True,
forcedProjRange=0)
for mod in self.modules:
for _ in range(projectionInfo.amount):
targetFit.register(mod, origin=self)
mod.calculateModifiedAttributes(
targetFit, runTime, forceProjected=True,
forcedProjRange=projectionInfo.projectionRange)
def fill(self):
"""
Fill this fit's module slots with enough dummy slots so that all slots are used.
This is mostly for making the life of gui's easier.
GUI's can call fill() and then stop caring about empty slots completely.
todo: want to get rid of using this from the gui/commands, and instead make it a more built-in feature within
recalc. Figure out a way to keep track of any changes to slot layout and call this automatically
"""
if self.ship is None:
return {}
# Look for any dummies of that type to remove
posToRemove = {}
for slotType in (
FittingSlot.LOW.value, FittingSlot.MED.value, FittingSlot.HIGH.value, FittingSlot.RIG.value, FittingSlot.SUBSYSTEM.value, FittingSlot.SERVICE.value):
amount = self.getSlotsFree(slotType, True)
if amount > 0:
for _ in range(int(amount)):
self.modules.append(Module.buildEmpty(slotType))
if amount < 0:
for mod in self.modules:
if mod.isEmpty and mod.slot == slotType:
pos = self.modules.index(mod)
posToRemove[pos] = slotType
amount += 1
if amount == 0:
break
for pos in sorted(posToRemove, reverse=True):
mod = self.modules[pos]
self.modules.remove(mod)
return posToRemove
def unfill(self):
for i in range(len(self.modules) - 1, -1, -1):
mod = self.modules[i]
if mod.isEmpty:
del self.modules[i]
def clearTail(self):
tailPositions = {}
for mod in reversed(self.modules):
if not mod.isEmpty:
break
tailPositions[self.modules.index(mod)] = mod.slot
for pos in sorted(tailPositions, reverse=True):
self.modules.remove(self.modules[pos])
return tailPositions
@property
def modCount(self):
x = 0
for i in range(len(self.modules) - 1, -1, -1):
mod = self.modules[i]
if not mod.isEmpty:
x += 1
return x
@staticmethod
def getItemAttrSum(dict, attr):
amount = 0
for mod in dict:
add = mod.getModifiedItemAttr(attr)
if add is not None:
amount += add
return amount
@staticmethod
def getItemAttrOnlineSum(dict, attr):
amount = 0
for mod in dict:
add = mod.getModifiedItemAttr(attr) if mod.state >= FittingModuleState.ONLINE else None
if add is not None:
amount += add
return amount
def getHardpointsUsed(self, type):
amount = 0
for mod in self.modules:
if mod.hardpoint is type and not mod.isEmpty:
amount += 1
return amount
def getSlotsUsed(self, type, countDummies=False):
amount = 0
for mod in chain(self.modules, self.fighters):
if mod.slot is type and (not getattr(mod, "isEmpty", False) or countDummies):
if type in (FittingSlot.F_HEAVY, FittingSlot.F_SUPPORT, FittingSlot.F_LIGHT, FittingSlot.FS_HEAVY, FittingSlot.FS_LIGHT,
FittingSlot.FS_SUPPORT) and not mod.active:
continue
amount += 1
return amount
slots = {
FittingSlot.LOW: "lowSlots",
FittingSlot.MED: "medSlots",
FittingSlot.HIGH: "hiSlots",
FittingSlot.RIG: "rigSlots",
FittingSlot.SUBSYSTEM: "maxSubSystems",
FittingSlot.SERVICE: "serviceSlots",
FittingSlot.F_LIGHT: "fighterLightSlots",
FittingSlot.F_SUPPORT: "fighterSupportSlots",
FittingSlot.F_HEAVY: "fighterHeavySlots",
FittingSlot.FS_LIGHT: "fighterStandupLightSlots",
FittingSlot.FS_SUPPORT: "fighterStandupSupportSlots",
FittingSlot.FS_HEAVY: "fighterStandupHeavySlots",
}
def getSlotsFree(self, type, countDummies=False):
if type in (FittingSlot.MODE, FittingSlot.SYSTEM):
# These slots don't really exist, return default 0
return 0
slotsUsed = self.getSlotsUsed(type, countDummies)
totalSlots = self.ship.getModifiedItemAttr(self.slots[type]) or 0
return int(totalSlots - slotsUsed)
def getNumSlots(self, type):
return self.ship.getModifiedItemAttr(self.slots[type]) or 0
def getHardpointsFree(self, type):
if type == FittingHardpoint.NONE:
return 1
elif type == FittingHardpoint.TURRET:
return self.ship.getModifiedItemAttr('turretSlotsLeft') - self.getHardpointsUsed(FittingHardpoint.TURRET)
elif type == FittingHardpoint.MISSILE:
return self.ship.getModifiedItemAttr('launcherSlotsLeft') - self.getHardpointsUsed(FittingHardpoint.MISSILE)
else:
raise ValueError("%d is not a valid value for Hardpoint Enum", type)
@property
def calibrationUsed(self):
return self.getItemAttrOnlineSum(self.modules, 'upgradeCost')
@property
def pgUsed(self):
return round(self.getItemAttrOnlineSum(self.modules, "power"), 2)
@property
def cpuUsed(self):
return round(self.getItemAttrOnlineSum(self.modules, "cpu"), 2)
@property
def droneBandwidthUsed(self):
amount = 0
for d in self.drones:
amount += d.getModifiedItemAttr("droneBandwidthUsed") * d.amountActive
return amount
@property
def droneBayUsed(self):
amount = 0
for d in self.drones:
amount += d.item.attributes['volume'].value * d.amount
return amount
@property
def fighterBayUsed(self):
amount = 0
for f in self.fighters:
amount += f.item.attributes['volume'].value * f.amount
return amount
@property
def fighterTubesUsed(self):
amount = 0
for f in self.fighters:
if f.active:
amount += 1
return amount
@property
def fighterTubesTotal(self):
return self.ship.getModifiedItemAttr("fighterTubes")
@property
def cargoBayUsed(self):
amount = 0
for c in self.cargo:
amount += c.getModifiedItemAttr("volume") * c.amount
return amount
@property
def activeDrones(self):
amount = 0
for d in self.drones:
amount += d.amountActive
return amount
@property
def probeSize(self):
"""
Expresses how difficult a target is to probe down with scan probes
"""
sigRad = self.ship.getModifiedItemAttr("signatureRadius")
sensorStr = float(self.scanStrength)
probeSize = sigRad / sensorStr if sensorStr != 0 else None
# http://www.eveonline.com/ingameboard.asp?a=topic&threadID=1532170&page=2#42
if probeSize is not None:
# Probe size is capped at 1.08
probeSize = max(probeSize, 1.08)
return probeSize
@property
def warpSpeed(self):
base = self.ship.getModifiedItemAttr("baseWarpSpeed") or 1
multiplier = self.ship.getModifiedItemAttr("warpSpeedMultiplier") or 1
return base * multiplier
@property
def maxWarpDistance(self):
capacity = self.ship.getModifiedItemAttr("capacitorCapacity")
mass = self.ship.getModifiedItemAttr("mass")
warpCapNeed = self.ship.getModifiedItemAttr("warpCapacitorNeed")
if not warpCapNeed:
return 0
return capacity / (mass * warpCapNeed)
@property
def capStable(self):
if self.__capStable is None:
self.simulateCap()
return self.__capStable
@property
def capState(self):
"""
If the cap is stable, the capacitor state is the % at which it is stable.
If the cap is unstable, this is the amount of time before it runs out
"""
if self.__capState is None:
self.simulateCap()
return self.__capState
@property
def capUsed(self):
if self.__capUsed is None:
self.simulateCap()
return self.__capUsed
@property
def capRecharge(self):
if self.__capRecharge is None:
self.simulateCap()
return self.__capRecharge
@property
def capDelta(self):
return (self.__capRecharge or 0) - (self.__capUsed or 0)
def calculateCapRecharge(self, percent=PEAK_RECHARGE, capacity=None, rechargeRate=None):
if capacity is None:
capacity = self.ship.getModifiedItemAttr("capacitorCapacity")
if rechargeRate is None:
rechargeRate = self.ship.getModifiedItemAttr("rechargeRate") / 1000.0
return 10 / rechargeRate * sqrt(percent) * (1 - sqrt(percent)) * capacity
def calculateShieldRecharge(self, percent=PEAK_RECHARGE):
capacity = self.ship.getModifiedItemAttr("shieldCapacity")
rechargeRate = self.ship.getModifiedItemAttr("shieldRechargeRate") / 1000.0
return 10 / rechargeRate * sqrt(percent) * (1 - sqrt(percent)) * capacity
def addDrain(self, src, cycleTime, capNeed, clipSize=0, reloadTime=0):
""" Used for both cap drains and cap fills (fills have negative capNeed) """
energyNeutralizerSignatureResolution = src.getModifiedItemAttr("energyNeutralizerSignatureResolution")
signatureRadius = self.ship.getModifiedItemAttr("signatureRadius")
# Signature reduction, uses the bomb formula as per CCP Larrikin
if energyNeutralizerSignatureResolution:
capNeed = capNeed * min(1, signatureRadius / energyNeutralizerSignatureResolution)
if capNeed:
self.__extraDrains.append((cycleTime, capNeed, clipSize, reloadTime))
def removeDrain(self, i):
del self.__extraDrains[i]
def iterDrains(self):
return self.__extraDrains.__iter__()
def __generateDrain(self):
drains = []
capUsed = 0
capAdded = 0
for mod in self.activeModulesIter():
if (mod.getModifiedItemAttr("capacitorNeed") or 0) != 0:
cycleTime = mod.rawCycleTime or 0
reactivationTime = mod.getModifiedItemAttr("moduleReactivationDelay") or 0
fullCycleTime = cycleTime + reactivationTime
reloadTime = mod.reloadTime
if fullCycleTime > 0:
capNeed = mod.capUse
if capNeed > 0:
capUsed += capNeed
else:
capAdded -= capNeed
# If this is a turret, don't stagger activations
disableStagger = mod.hardpoint == FittingHardpoint.TURRET
drains.append((
int(fullCycleTime),
mod.getModifiedItemAttr("capacitorNeed") or 0,
mod.numShots or 0,
disableStagger,
reloadTime,
mod.item.group.name == 'Capacitor Booster'))
for fullCycleTime, capNeed, clipSize, reloadTime in self.iterDrains():
drains.append((
int(fullCycleTime),
capNeed,
clipSize,
# Stagger incoming effects for cap simulation
False,
reloadTime,
False))
if capNeed > 0:
capUsed += capNeed / (fullCycleTime / 1000.0)
else:
capAdded += -capNeed / (fullCycleTime / 1000.0)
return drains, capUsed, capAdded
def simulateCap(self):
drains, self.__capUsed, self.__capRecharge = self.__generateDrain()
self.__capRecharge += self.calculateCapRecharge()
sim = self.__runCapSim(drains=drains)
if sim is not None:
capState = (sim.cap_stable_low + sim.cap_stable_high) / (2 * sim.capacitorCapacity)
self.__capStable = capState > 0
self.__capState = min(100, capState * 100) if self.__capStable else sim.t / 1000.0
else:
self.__capStable = True
self.__capState = 100
def getCapSimData(self, startingCap):
if startingCap not in self.__savedCapSimData:
self.__runCapSim(startingCap=startingCap, tMax=3600, optimizeRepeats=False)
return self.__savedCapSimData[startingCap]
def __runCapSim(self, drains=None, startingCap=None, tMax=None, optimizeRepeats=True):
if drains is None:
drains, nil, nil = self.__generateDrain()
if tMax is None:
tMax = 6 * 60 * 60 * 1000
else:
tMax *= 1000
if len(drains) > 0:
sim = capSim.CapSimulator()
sim.init(drains)
sim.capacitorCapacity = self.ship.getModifiedItemAttr("capacitorCapacity")
sim.capacitorRecharge = self.ship.getModifiedItemAttr("rechargeRate")
sim.startingCapacity = startingCap = self.ship.getModifiedItemAttr("capacitorCapacity") if startingCap is None else startingCap
sim.stagger = True
sim.scale = False
sim.t_max = tMax
sim.reload = self.factorReload
sim.optimize_repeats = optimizeRepeats
sim.run()
# We do not want to store partial results
if not sim.result_optimized_repeats:
self.__savedCapSimData[startingCap] = sim.saved_changes
return sim
else:
self.__savedCapSimData[startingCap] = []
return None
def getCapRegenGainFromMod(self, mod):
"""Return how much cap regen do we gain from having this module"""
currentRegen = self.calculateCapRecharge()
nomodRegen = self.calculateCapRecharge(
capacity=self.ship.getModifiedItemAttrExtended("capacitorCapacity", ignoreAfflictors=[mod]),
rechargeRate=self.ship.getModifiedItemAttrExtended("rechargeRate", ignoreAfflictors=[mod]) / 1000.0)
return currentRegen - nomodRegen
def getRemoteReps(self, spoolOptions=None):
if spoolOptions not in self.__remoteRepMap:
remoteReps = RRTypes(0, 0, 0, 0)
for module in self.modules:
remoteReps += module.getRemoteReps(spoolOptions=spoolOptions)
for drone in self.drones:
remoteReps += drone.getRemoteReps()
self.__remoteRepMap[spoolOptions] = remoteReps
return self.__remoteRepMap[spoolOptions]
@property
def hp(self):
hp = {}
for (type, attr) in (('shield', 'shieldCapacity'), ('armor', 'armorHP'), ('hull', 'hp')):
hp[type] = self.ship.getModifiedItemAttr(attr)
return hp
@property
def ehp(self):
if self.__ehp is None:
if self.damagePattern is None:
ehp = self.hp
else:
ehp = self.damagePattern.calculateEhp(self.ship)
self.__ehp = ehp
return self.__ehp
@property
def tank(self):
reps = {
"passiveShield": self.calculateShieldRecharge(),
"shieldRepair": self.extraAttributes["shieldRepair"] + self._getAppliedShieldRr(),
"armorRepair": self.extraAttributes["armorRepair"] + self._getAppliedArmorRr(),
"armorRepairPreSpool": self.extraAttributes["armorRepairPreSpool"] + self._getAppliedArmorPreSpoolRr(),
"armorRepairFullSpool": self.extraAttributes["armorRepairFullSpool"] + self._getAppliedArmorFullSpoolRr(),
"hullRepair": self.extraAttributes["hullRepair"] + self._getAppliedHullRr()
}
return reps
@property
def effectiveTank(self):
if self.__effectiveTank is None:
if self.damagePattern is None:
ehps = self.tank
else:
ehps = self.damagePattern.calculateEffectiveTank(self, self.tank)
self.__effectiveTank = ehps
return self.__effectiveTank
@property
def sustainableTank(self):
if self.__sustainableTank is None:
self.calculateSustainableTank()
return self.__sustainableTank
@property
def effectiveSustainableTank(self):
if self.__effectiveSustainableTank is None:
if self.damagePattern is None:
tank = self.sustainableTank
else:
tank = self.damagePattern.calculateEffectiveTank(self, self.sustainableTank)
self.__effectiveSustainableTank = tank
return self.__effectiveSustainableTank
def calculateSustainableTank(self):
if self.__sustainableTank is None:
sustainable = {
"passiveShield": self.calculateShieldRecharge(),
"shieldRepair": self.extraAttributes["shieldRepair"] + self._getAppliedShieldRr(),
"armorRepair": self.extraAttributes["armorRepair"] + self._getAppliedArmorRr(),
"armorRepairPreSpool": self.extraAttributes["armorRepairPreSpool"] + self._getAppliedArmorPreSpoolRr(),
"armorRepairFullSpool": self.extraAttributes["armorRepairFullSpool"] + self._getAppliedArmorFullSpoolRr(),
"hullRepair": self.extraAttributes["hullRepair"] + self._getAppliedHullRr()
}
if not self.capStable or self.factorReload:
# Map a local repairer type to the attribute it uses
groupAttrMap = {
"Shield Booster": "shieldBonus",
"Ancillary Shield Booster": "shieldBonus",
"Armor Repair Unit": "armorDamageAmount",
"Ancillary Armor Repairer": "armorDamageAmount",
"Hull Repair Unit": "structureDamageAmount"
}
# Map local repairer type to tank type
groupStoreMap = {
"Shield Booster": "shieldRepair",
"Ancillary Shield Booster": "shieldRepair",
"Armor Repair Unit": "armorRepair",
"Ancillary Armor Repairer": "armorRepair",
"Hull Repair Unit": "hullRepair"
}
repairers = []
localAdjustment = {"shieldRepair": 0, "armorRepair": 0, "hullRepair": 0}
capUsed = self.capUsed
for tankType in localAdjustment:
dict = self.extraAttributes.getAfflictions(tankType)
if self in dict:
for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in dict[self]:
if not used:
continue
if afflictor.projected:
continue
if afflictor.item.group.name not in groupAttrMap:
continue
usesCap = True
try:
if afflictor.capUse:
capUsed -= afflictor.capUse
else:
usesCap = False
except AttributeError:
usesCap = False
# Normal Repairers
if usesCap and not afflictor.charge:
cycleTime = afflictor.rawCycleTime
amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name])
localAdjustment[tankType] -= amount / (cycleTime / 1000.0)
repairers.append(afflictor)
# Ancillary Armor reps etc
elif usesCap and afflictor.charge:
cycleTime = afflictor.rawCycleTime
amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name])
if afflictor.charge.name == "Nanite Repair Paste":
multiplier = afflictor.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
else:
multiplier = 1
localAdjustment[tankType] -= amount * multiplier / (cycleTime / 1000.0)
repairers.append(afflictor)
# Ancillary Shield boosters etc
elif not usesCap and afflictor.item.group.name in ("Ancillary Shield Booster", "Ancillary Remote Shield Booster"):
cycleTime = afflictor.rawCycleTime
amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name])
if self.factorReload and afflictor.charge:
reloadtime = afflictor.reloadTime
else:
reloadtime = 0.0
offdutycycle = reloadtime / ((max(afflictor.numShots, 1) * cycleTime) + reloadtime)
localAdjustment[tankType] -= amount * offdutycycle / (cycleTime / 1000.0)
# Sort repairers by efficiency. We want to use the most efficient repairers first
repairers.sort(key=lambda _mod: _mod.getModifiedItemAttr(
groupAttrMap[_mod.item.group.name]) * (_mod.getModifiedItemAttr(
"chargedArmorDamageMultiplier") or 1) / _mod.getModifiedItemAttr("capacitorNeed"), reverse=True)
# Loop through every module until we're above peak recharge
# Most efficient first, as we sorted earlier.
# calculate how much the repper can rep stability & add to total
totalPeakRecharge = self.capRecharge
for afflictor in repairers:
if capUsed > totalPeakRecharge:
break
if self.factorReload and afflictor.charge:
reloadtime = afflictor.reloadTime
else:
reloadtime = 0.0
cycleTime = afflictor.rawCycleTime
capPerSec = afflictor.capUse
if capPerSec is not None and cycleTime is not None:
# Check how much this repper can work
sustainability = min(1, (totalPeakRecharge - capUsed) / capPerSec)
amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name])
# Add the sustainable amount
if not afflictor.charge:
localAdjustment[groupStoreMap[afflictor.item.group.name]] += sustainability * amount / (
cycleTime / 1000.0)
else:
if afflictor.charge.name == "Nanite Repair Paste":
multiplier = afflictor.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
else:
multiplier = 1
ondutycycle = (max(afflictor.numShots, 1) * cycleTime) / (
(max(afflictor.numShots, 1) * cycleTime) + reloadtime)
localAdjustment[groupStoreMap[
afflictor.item.group.name]] += sustainability * amount * ondutycycle * multiplier / (
cycleTime / 1000.0)
capUsed += capPerSec
sustainable["shieldRepair"] += localAdjustment["shieldRepair"]
sustainable["armorRepair"] += localAdjustment["armorRepair"]
sustainable["armorRepairPreSpool"] += localAdjustment["armorRepair"]
sustainable["armorRepairFullSpool"] += localAdjustment["armorRepair"]
sustainable["hullRepair"] += localAdjustment["hullRepair"]
self.__sustainableTank = sustainable
return self.__sustainableTank
def calculateLockTime(self, radius):
scanRes = self.ship.getModifiedItemAttr("scanResolution")
if scanRes is not None and scanRes > 0:
return calculateLockTime(srcScanRes=scanRes, tgtSigRadius=radius)
else:
return self.ship.getModifiedItemAttr("scanSpeed") / 1000.0
def calculatemining(self):
minerYield = 0
minerDrain = 0
droneYield = 0
droneDrain = 0
for mod in self.modules:
minerYield += mod.getMiningYPS()
minerDrain += mod.getMiningDPS()
for drone in self.drones:
droneYield += drone.getMiningYPS()
droneDrain += drone.getMiningDPS()
self.__minerYield = minerYield
self.__minerDrain = minerDrain
self.__droneYield = droneYield
self.__droneDrain = droneDrain
def calculateWeaponDmgStats(self, spoolOptions):
weaponVolley = DmgTypes.default()
weaponDps = DmgTypes.default()
for mod in self.modules:
weaponVolley += mod.getVolley(spoolOptions=spoolOptions)
weaponDps += mod.getDps(spoolOptions=spoolOptions)
weaponVolley.profile = self.targetProfile
weaponDps.profile = self.targetProfile
self.__weaponVolleyMap[spoolOptions] = weaponVolley
self.__weaponDpsMap[spoolOptions] = weaponDps
def calculateDroneDmgStats(self):
droneVolley = DmgTypes.default()
droneDps = DmgTypes.default()
for drone in self.drones:
droneVolley += drone.getVolley()
droneDps += drone.getDps()
for fighter in self.fighters:
droneVolley += fighter.getVolley()
droneDps += fighter.getDps()
droneVolley.profile = self.targetProfile
droneDps.profile = self.targetProfile
self.__droneDps = droneDps
self.__droneVolley = droneVolley
@property
def fits(self):
for mod in self.modules:
if not mod.isEmpty and not mod.fits(self):
return False
return True
def getReleaseLimitForDrone(self, item):
if not item.isDrone:
return 0
bw = round(self.ship.getModifiedItemAttr("droneBandwidth"))
volume = round(item.attribsWithOverrides['volume'].value)
return int(bw / volume)
def getStoreLimitForDrone(self, item):
if not item.isDrone:
return 0
bayTotal = round(self.ship.getModifiedItemAttr("droneCapacity"))
bayUsed = round(self.droneBayUsed)
volume = item.attribsWithOverrides['volume'].value
return int((bayTotal - bayUsed) / volume)
def getSystemSecurity(self):
secstatus = self.systemSecurity
# Default to nullsec
if secstatus is None:
secstatus = FitSystemSecurity.NULLSEC
return secstatus
def getPilotSecurity(self, low_limit=-10, high_limit=5):
secstatus = self.pilotSecurity
# Not defined -> use character SS, with 0.0 fallback if it fails
if secstatus is None:
try:
secstatus = self.character.secStatus
except (SystemExit, KeyboardInterrupt):
raise
except:
secstatus = 0
return max(low_limit, min(high_limit, secstatus))
def activeModulesIter(self):
for mod in self.modules:
if mod.state >= FittingModuleState.ACTIVE:
yield mod
def activeDronesIter(self):
for drone in self.drones:
if drone.amountActive > 0:
yield drone
def activeFightersIter(self):
for fighter in self.fighters:
if fighter.active:
yield fighter
def activeFighterAbilityIter(self):
for fighter in self.activeFightersIter():
for ability in fighter.abilities:
if ability.active:
yield fighter, ability
def getDampMultScanRes(self):
damps = []
for mod in self.activeModulesIter():
for effectName in ('remoteSensorDampFalloff', 'structureModuleEffectRemoteSensorDampener'):
if effectName in mod.item.effects:
damps.append((mod.getModifiedItemAttr('scanResolutionBonus'), 'default'))
if 'doomsdayAOEDamp' in mod.item.effects:
damps.append((mod.getModifiedItemAttr('scanResolutionBonus'), 'default'))
for drone in self.activeDronesIter():
if 'remoteSensorDampEntity' in drone.item.effects:
damps.extend(drone.amountActive * ((drone.getModifiedItemAttr('scanResolutionBonus'), 'default'),))
mults = {}
for strength, stackingGroup in damps:
mults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
return calculateMultiplier(mults)
def _getAppliedHullRr(self):
return self.__getAppliedRr(self._hullRr)
def _getAppliedArmorRr(self):
return self.__getAppliedRr(self._armorRr)
def _getAppliedArmorPreSpoolRr(self):
return self.__getAppliedRr(self._armorRrPreSpool)
def _getAppliedArmorFullSpoolRr(self):
return self.__getAppliedRr(self._armorRrFullSpool)
def _getAppliedShieldRr(self):
return self.__getAppliedRr(self._shieldRr)
@staticmethod
def __getAppliedRr(rrList):
totalRaw = 0
for amount, cycleTime in rrList:
# That's right, for considerations of RR diminishing returns cycle time is rounded this way
totalRaw += amount / int(cycleTime)
RR_ADDITION = 7000
RR_MULTIPLIER = 20
appliedRr = 0
for amount, cycleTime in rrList:
rrps = amount / int(cycleTime)
modified_rrps = RR_ADDITION + (rrps * RR_MULTIPLIER)
rrps_mult = 1 - (((rrps + modified_rrps) / (totalRaw + modified_rrps)) - 1) ** 2
appliedRr += rrps_mult * amount / cycleTime
return appliedRr
def __deepcopy__(self, memo=None):
fitCopy = Fit()
# Character and owner are not copied
fitCopy.character = self.__character
fitCopy.owner = self.owner
fitCopy.ship = deepcopy(self.ship)
fitCopy.mode = deepcopy(self.mode)
fitCopy.name = "%s copy" % self.name
fitCopy.damagePattern = self.damagePattern
fitCopy.targetProfile = self.targetProfile
fitCopy.implantLocation = self.implantLocation
fitCopy.systemSecurity = self.systemSecurity
fitCopy.pilotSecurity = self.pilotSecurity
fitCopy.notes = self.notes
for i in self.modules:
fitCopy.modules.appendIgnoreEmpty(deepcopy(i))
toCopy = (
"drones",
"fighters",
"cargo",
"implants",
"boosters",
"projectedModules",
"projectedDrones",
"projectedFighters")
for name in toCopy:
orig = getattr(self, name)
c = getattr(fitCopy, name)
for i in orig:
c.append(deepcopy(i))
# this bit is required -- see GH issue # 83
def forceUpdateSavedata(fit):
eos.db.saveddata_session.flush()
eos.db.saveddata_session.refresh(fit)
for fit in self.commandFits:
fitCopy.commandFitDict[fit.ID] = fit
forceUpdateSavedata(fit)
copyCommandInfo = fit.getCommandInfo(fitCopy.ID)
originalCommandInfo = fit.getCommandInfo(self.ID)
copyCommandInfo.active = originalCommandInfo.active
forceUpdateSavedata(fit)
for fit in self.projectedFits:
fitCopy.projectedFitDict[fit.ID] = fit
forceUpdateSavedata(fit)
copyProjectionInfo = fit.getProjectionInfo(fitCopy.ID)
originalProjectionInfo = fit.getProjectionInfo(self.ID)
copyProjectionInfo.active = originalProjectionInfo.active
copyProjectionInfo.amount = originalProjectionInfo.amount
copyProjectionInfo.projectionRange = originalProjectionInfo.projectionRange
forceUpdateSavedata(fit)
return fitCopy
def __repr__(self):
return "Fit(ID={}, ship={}, name={}) at {}".format(
self.ID, self.ship.item.name, self.name, hex(id(self))
)
def __str__(self):
return "{} ({})".format(
self.name, self.ship.item.name
)