* Remove dirtyStorage, legacy of old fleet boosting system * Remove withBooster flag from the recalc() function. This was a legacy parameter dealing with the old fleet boost code, the purpose of which has changed with the addition of command boosts. We no longer have to call recalc with this particular parameter (indeed, it was forced to False in `recalc`). These were never cleaned up. We still have the withBoosters parameter for the `calculateModifiedAttributes` function, however that will be renamed and has a different meaning than it used to (used in the recursion of command fits) * Add some comments * Consolidate the tangled mess of conditional flags meant to determine if a fit is local/projected/command. This should make the logic flow a bit clearer. Also rewrite a blurb about returning on a calculated fit - need to look into this a little bit more. * fix up test to no longer pass `withBooster`
1434 lines
56 KiB
Python
1434 lines
56 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 time
|
|
from copy import deepcopy
|
|
from itertools import chain
|
|
from math import sqrt, log, asinh
|
|
|
|
from sqlalchemy.orm import validates, reconstructor
|
|
|
|
import eos.db
|
|
from eos import capSim
|
|
from eos.effectHandlerHelpers import HandledModuleList, HandledDroneCargoList, HandledImplantBoosterList, HandledProjectedDroneList, HandledProjectedModList
|
|
from eos.enum import Enum
|
|
from eos.saveddata.ship import Ship
|
|
from eos.saveddata.character import Character
|
|
from eos.saveddata.citadel import Citadel
|
|
from eos.saveddata.module import Module, State, Slot, Hardpoint
|
|
from logbook import Logger
|
|
|
|
pyfalog = Logger(__name__)
|
|
|
|
|
|
class ImplantLocation(Enum):
|
|
FIT = 0
|
|
CHARACTER = 1
|
|
|
|
|
|
class CalcType(Enum):
|
|
LOCAL = 0
|
|
PROJECTED = 1
|
|
COMMAND = 2
|
|
|
|
|
|
class Fit(object):
|
|
"""Represents a fitting, with modules, ship, implants, etc."""
|
|
|
|
PEAK_RECHARGE = 0.25
|
|
|
|
def __init__(self, ship=None, name=""):
|
|
"""Initialize a fit from the program"""
|
|
# use @mode.setter's to set __attr and IDs. This will set mode as well
|
|
self.ship = ship
|
|
if self.ship:
|
|
self.ship.parent = self
|
|
|
|
self.__modules = HandledModuleList()
|
|
self.__drones = HandledDroneCargoList()
|
|
self.__fighters = HandledDroneCargoList()
|
|
self.__cargo = HandledDroneCargoList()
|
|
self.__implants = HandledImplantBoosterList()
|
|
self.__boosters = HandledImplantBoosterList()
|
|
# 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.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)
|
|
else:
|
|
self.__mode = self.ship.validateModeItem(None)
|
|
|
|
self.build()
|
|
|
|
def build(self):
|
|
self.__extraDrains = []
|
|
self.__ehp = None
|
|
self.__weaponDPS = None
|
|
self.__minerYield = None
|
|
self.__weaponVolley = None
|
|
self.__droneDPS = None
|
|
self.__droneVolley = None
|
|
self.__droneYield = 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.__calculatedTargets = []
|
|
self.__remoteReps = {
|
|
"Armor" : None,
|
|
"Shield" : None,
|
|
"Hull" : None,
|
|
"Capacitor": None,
|
|
}
|
|
self.factorReload = False
|
|
self.boostsFits = set()
|
|
self.gangBoosts = None
|
|
self.ecmProjectedStr = 1
|
|
self.commandBonuses = {}
|
|
|
|
@property
|
|
def targetResists(self):
|
|
return self.__targetResists
|
|
|
|
@targetResists.setter
|
|
def targetResists(self, targetResists):
|
|
self.__targetResists = targetResists
|
|
self.__weaponDPS = None
|
|
self.__weaponVolley = None
|
|
self.__droneDPS = None
|
|
self.__droneVolley = None
|
|
|
|
@property
|
|
def damagePattern(self):
|
|
return self.__damagePattern
|
|
|
|
@damagePattern.setter
|
|
def damagePattern(self, damagePattern):
|
|
self.__damagePattern = damagePattern
|
|
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):
|
|
self.__mode = mode
|
|
self.modeID = mode.item.ID if mode is not None else None
|
|
|
|
@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):
|
|
self.__ship = ship
|
|
self.shipID = ship.item.ID if ship is not None else None
|
|
if ship is not None:
|
|
# set mode of new ship
|
|
self.mode = self.ship.validateModeItem(None) 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 self.__projectedFits.values() if not fit.isInvalid]
|
|
|
|
@property
|
|
def commandFits(self):
|
|
return [fit for fit in self.__commandFits.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
|
|
|
|
@property
|
|
def weaponDPS(self):
|
|
if self.__weaponDPS is None:
|
|
self.calculateWeaponStats()
|
|
|
|
return self.__weaponDPS
|
|
|
|
@property
|
|
def weaponVolley(self):
|
|
if self.__weaponVolley is None:
|
|
self.calculateWeaponStats()
|
|
|
|
return self.__weaponVolley
|
|
|
|
@property
|
|
def droneDPS(self):
|
|
if self.__droneDPS is None:
|
|
self.calculateWeaponStats()
|
|
|
|
return self.__droneDPS
|
|
|
|
@property
|
|
def droneVolley(self):
|
|
if self.__droneVolley is None:
|
|
self.calculateWeaponStats()
|
|
|
|
return self.__droneVolley
|
|
|
|
@property
|
|
def totalDPS(self):
|
|
return self.droneDPS + self.weaponDPS
|
|
|
|
@property
|
|
def totalVolley(self):
|
|
return self.droneVolley + self.weaponVolley
|
|
|
|
@property
|
|
def minerYield(self):
|
|
if self.__minerYield is None:
|
|
self.calculateMiningStats()
|
|
|
|
return self.__minerYield
|
|
|
|
@property
|
|
def droneYield(self):
|
|
if self.__droneYield is None:
|
|
self.calculateMiningStats()
|
|
|
|
return self.__droneYield
|
|
|
|
@property
|
|
def totalYield(self):
|
|
return self.droneYield + self.minerYield
|
|
|
|
@property
|
|
def maxTargets(self):
|
|
return min(self.extraAttributes["maxTargetsLockedFromSkills"],
|
|
self.ship.getModifiedItemAttr("maxLockedTargets"))
|
|
|
|
@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 ("Magnetometric", "Ladar", "Radar", "Gravimetric"):
|
|
currStr = self.ship.getModifiedItemAttr("scan%sStrength" % scanType)
|
|
if currStr > maxStr:
|
|
maxStr = currStr
|
|
type = scanType
|
|
elif currStr == maxStr:
|
|
type = "Multispectral"
|
|
|
|
return type
|
|
|
|
@property
|
|
def jamChance(self):
|
|
return (1 - self.ecmProjectedStr) * 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 clear(self, projected=False):
|
|
self.__effectiveTank = None
|
|
self.__weaponDPS = None
|
|
self.__minerYield = None
|
|
self.__weaponVolley = None
|
|
self.__effectiveSustainableTank = None
|
|
self.__sustainableTank = None
|
|
self.__droneDPS = None
|
|
self.__droneVolley = None
|
|
self.__droneYield = None
|
|
self.__ehp = None
|
|
self.__calculated = False
|
|
self.__capStable = None
|
|
self.__capState = None
|
|
self.__capUsed = None
|
|
self.__capRecharge = None
|
|
self.ecmProjectedStr = 1
|
|
self.commandBonuses = {}
|
|
|
|
for remoterep_type in self.__remoteReps:
|
|
self.__remoteReps[remoterep_type] = None
|
|
|
|
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()
|
|
|
|
# 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)
|
|
|
|
# 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.itemModifiedAttributes, "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 __runCommandBoosts(self, runTime="normal"):
|
|
pyfalog.debug("Applying gang boosts for {0}", repr(self))
|
|
for warfareBuffID in 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
|
|
# @todo: Check this
|
|
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)
|
|
|
|
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"), "capacitorNeed", value)
|
|
self.modules.filteredItemBoost(
|
|
lambda mod: mod.item.requiresSkill("Shield Operation") or mod.item.requiresSkill(
|
|
"Shield Emission Systems"), "duration", value)
|
|
|
|
if warfareBuffID == 12: # Shield Burst: Shield Extension: Shield HP
|
|
self.ship.boostItemAttr("shieldCapacity", value, stackingPenalties=True)
|
|
|
|
if warfareBuffID == 13: # Armor Burst: Armor Energizing: Armor Resistance
|
|
for damageType in ("Em", "Thermal", "Explosive", "Kinetic"):
|
|
self.ship.boostItemAttr("armor%sDamageResonance" % damageType, value)
|
|
|
|
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"),
|
|
"capacitorNeed", value)
|
|
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or
|
|
mod.item.requiresSkill("Repair Systems"),
|
|
"duration", value)
|
|
|
|
if warfareBuffID == 15: # Armor Burst: Armor Reinforcement: Armor HP
|
|
self.ship.boostItemAttr("armorHP", value, stackingPenalties=True)
|
|
|
|
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 == 26: # Information Burst: Sensor Optimization: Targeting Range
|
|
self.ship.boostItemAttr("maxTargetRange", 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/Survey 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)
|
|
|
|
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("CPU Management"),
|
|
"surveyScanRange", 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.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining"),
|
|
"crystalVolatilityChance", 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, stackingPenalties=True)
|
|
|
|
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, stackingPenalties=True)
|
|
|
|
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)
|
|
|
|
del self.commandBonuses[warfareBuffID]
|
|
|
|
def __resetDependantCalcs(self):
|
|
self.calculated = False
|
|
for value in 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.debug("Starting fit calculation on: {0}, calc: {1}", repr(self), CalcType.getName(type))
|
|
|
|
# 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.__resetDependantCalcs()
|
|
|
|
# For fits that are under local's Command, we do the same thing
|
|
for value in self.boostedOnto.values():
|
|
value.boosted_fit.__resetDependantCalcs()
|
|
|
|
# it should be noted that command bursts don't affect other command bursts
|
|
|
|
shadow = False
|
|
if targetFit and type == CalcType.PROJECTED:
|
|
pyfalog.debug("Calculating projections from {0} to target {1}", repr(self), repr(targetFit))
|
|
projectionInfo = self.getProjectionInfo(targetFit.ID)
|
|
pyfalog.debug("ProjectionInfo: {0}", projectionInfo)
|
|
if self == targetFit:
|
|
copied = self # original fit
|
|
shadow = True
|
|
# Don't inspect this, we genuinely want to reassign self
|
|
# noinspection PyMethodFirstArgAssignment
|
|
self = deepcopy(self)
|
|
pyfalog.debug("Handling self projection - making shadow copy of fit. {0} => {1}", copied, self)
|
|
# we delete the fit because when we copy a fit, flush() is
|
|
# called to properly handle projection updates. However, we do
|
|
# not want to save this fit to the database, so simply remove it
|
|
eos.db.saveddata_session.delete(self)
|
|
|
|
# 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:
|
|
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
|
|
|
|
# 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:
|
|
for item in chain.from_iterable(u):
|
|
if item is not None:
|
|
# apply effects onto target fit
|
|
for _ in xrange(projectionInfo.amount):
|
|
targetFit.register(item, origin=self)
|
|
item.calculateModifiedAttributes(targetFit, runTime, True)
|
|
|
|
# Mark fit as calculated
|
|
self.__calculated = True
|
|
|
|
# Only apply projected fits if fit it not projected itself.
|
|
if type == CalcType.LOCAL:
|
|
for fit in self.projectedFits:
|
|
if fit.getProjectionInfo(self.ID).active:
|
|
fit.calculateModifiedAttributes(self, type=CalcType.PROJECTED)
|
|
|
|
if shadow:
|
|
pyfalog.debug("Delete shadow fit object")
|
|
del self
|
|
|
|
pyfalog.debug('Done with fit calculation')
|
|
|
|
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.
|
|
"""
|
|
if self.ship is None:
|
|
return
|
|
|
|
for slotType in (Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE):
|
|
amount = self.getSlotsFree(slotType, True)
|
|
if amount > 0:
|
|
for _ in xrange(int(amount)):
|
|
self.modules.append(Module.buildEmpty(slotType))
|
|
|
|
if amount < 0:
|
|
# Look for any dummies of that type to remove
|
|
toRemove = []
|
|
for mod in self.modules:
|
|
if mod.isEmpty and mod.slot == slotType:
|
|
toRemove.append(mod)
|
|
amount += 1
|
|
if amount == 0:
|
|
break
|
|
for mod in toRemove:
|
|
self.modules.remove(mod)
|
|
|
|
def unfill(self):
|
|
for i in xrange(len(self.modules) - 1, -1, -1):
|
|
mod = self.modules[i]
|
|
if mod.isEmpty:
|
|
del self.modules[i]
|
|
|
|
@property
|
|
def modCount(self):
|
|
x = 0
|
|
for i in xrange(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 >= State.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 (Slot.F_HEAVY, Slot.F_SUPPORT, Slot.F_LIGHT) and not mod.active:
|
|
continue
|
|
amount += 1
|
|
|
|
return amount
|
|
|
|
slots = {
|
|
Slot.LOW : "lowSlots",
|
|
Slot.MED : "medSlots",
|
|
Slot.HIGH : "hiSlots",
|
|
Slot.RIG : "rigSlots",
|
|
Slot.SUBSYSTEM: "maxSubSystems",
|
|
Slot.SERVICE : "serviceSlots",
|
|
Slot.F_LIGHT : "fighterLightSlots",
|
|
Slot.F_SUPPORT: "fighterSupportSlots",
|
|
Slot.F_HEAVY : "fighterHeavySlots"
|
|
}
|
|
|
|
def getSlotsFree(self, type, countDummies=False):
|
|
if type in (Slot.MODE, Slot.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
|
|
|
|
@property
|
|
def calibrationUsed(self):
|
|
return self.getItemAttrOnlineSum(self.modules, 'upgradeCost')
|
|
|
|
@property
|
|
def pgUsed(self):
|
|
return self.getItemAttrOnlineSum(self.modules, "power")
|
|
|
|
@property
|
|
def cpuUsed(self):
|
|
return self.getItemAttrOnlineSum(self.modules, "cpu")
|
|
|
|
@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.volume * d.amount
|
|
|
|
return amount
|
|
|
|
@property
|
|
def fighterBayUsed(self):
|
|
amount = 0
|
|
for f in self.fighters:
|
|
amount += f.item.volume * f.amountActive
|
|
|
|
return amount
|
|
|
|
@property
|
|
def fighterTubesUsed(self):
|
|
amount = 0
|
|
for f in self.fighters:
|
|
if f.active:
|
|
amount += 1
|
|
|
|
return amount
|
|
|
|
@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 sustainableTank(self):
|
|
if self.__sustainableTank is None:
|
|
self.calculateSustainableTank()
|
|
|
|
return self.__sustainableTank
|
|
|
|
def calculateSustainableTank(self, effective=True):
|
|
if self.__sustainableTank is None:
|
|
if self.capStable:
|
|
sustainable = {
|
|
"armorRepair" : self.extraAttributes["armorRepair"],
|
|
"shieldRepair": self.extraAttributes["shieldRepair"],
|
|
"hullRepair" : self.extraAttributes["hullRepair"]
|
|
}
|
|
else:
|
|
sustainable = {}
|
|
|
|
repairers = []
|
|
# Map a repairer type to the attribute it uses
|
|
groupAttrMap = {
|
|
"Armor Repair Unit" : "armorDamageAmount",
|
|
"Ancillary Armor Repairer": "armorDamageAmount",
|
|
"Hull Repair Unit" : "structureDamageAmount",
|
|
"Shield Booster" : "shieldBonus",
|
|
"Ancillary Shield Booster": "shieldBonus",
|
|
"Remote Armor Repairer" : "armorDamageAmount",
|
|
"Remote Shield Booster" : "shieldBonus"
|
|
}
|
|
# Map repairer type to attribute
|
|
groupStoreMap = {
|
|
"Armor Repair Unit" : "armorRepair",
|
|
"Hull Repair Unit" : "hullRepair",
|
|
"Shield Booster" : "shieldRepair",
|
|
"Ancillary Shield Booster": "shieldRepair",
|
|
"Remote Armor Repairer" : "armorRepair",
|
|
"Remote Shield Booster" : "shieldRepair",
|
|
"Ancillary Armor Repairer": "armorRepair",
|
|
}
|
|
|
|
capUsed = self.capUsed
|
|
for attr in ("shieldRepair", "armorRepair", "hullRepair"):
|
|
sustainable[attr] = self.extraAttributes[attr]
|
|
dict = self.extraAttributes.getAfflictions(attr)
|
|
if self in dict:
|
|
for mod, _, amount, used in dict[self]:
|
|
if not used:
|
|
continue
|
|
if mod.projected is False:
|
|
usesCap = True
|
|
try:
|
|
if mod.capUse:
|
|
capUsed -= mod.capUse
|
|
else:
|
|
usesCap = False
|
|
except AttributeError:
|
|
usesCap = False
|
|
# Modules which do not use cap are not penalized based on cap use
|
|
if usesCap:
|
|
cycleTime = mod.getModifiedItemAttr("duration")
|
|
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
|
|
sustainable[attr] -= amount / (cycleTime / 1000.0)
|
|
repairers.append(mod)
|
|
|
|
# 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("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 mod in repairers:
|
|
if capUsed > totalPeakRecharge:
|
|
break
|
|
cycleTime = mod.cycleTime
|
|
capPerSec = mod.capUse
|
|
if capPerSec is not None and cycleTime is not None:
|
|
# Check how much this repper can work
|
|
sustainability = min(1, (totalPeakRecharge - capUsed) / capPerSec)
|
|
|
|
# Add the sustainable amount
|
|
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
|
|
sustainable[groupStoreMap[mod.item.group.name]] += sustainability * (amount / (cycleTime / 1000.0))
|
|
capUsed += capPerSec
|
|
|
|
sustainable["passiveShield"] = self.calculateShieldRecharge()
|
|
self.__sustainableTank = sustainable
|
|
|
|
return self.__sustainableTank
|
|
|
|
def calculateCapRecharge(self, percent=PEAK_RECHARGE):
|
|
capacity = self.ship.getModifiedItemAttr("capacitorCapacity")
|
|
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):
|
|
""" 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)
|
|
|
|
resistance = self.ship.getModifiedItemAttr("energyWarfareResistance") or 1 if capNeed > 0 else 1
|
|
self.__extraDrains.append((cycleTime, capNeed * resistance, clipSize))
|
|
|
|
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.modules:
|
|
if mod.state >= State.ACTIVE:
|
|
if (mod.getModifiedItemAttr("capacitorNeed") or 0) != 0:
|
|
cycleTime = mod.rawCycleTime or 0
|
|
reactivationTime = mod.getModifiedItemAttr("moduleReactivationDelay") or 0
|
|
fullCycleTime = cycleTime + reactivationTime
|
|
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 == Hardpoint.TURRET
|
|
|
|
drains.append((int(fullCycleTime), mod.getModifiedItemAttr("capacitorNeed") or 0,
|
|
mod.numShots or 0, disableStagger))
|
|
|
|
for fullCycleTime, capNeed, clipSize in self.iterDrains():
|
|
# Stagger incoming effects for cap simulation
|
|
drains.append((int(fullCycleTime), capNeed, clipSize, 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()
|
|
if len(drains) > 0:
|
|
sim = capSim.CapSimulator()
|
|
sim.init(drains)
|
|
sim.capacitorCapacity = self.ship.getModifiedItemAttr("capacitorCapacity")
|
|
sim.capacitorRecharge = self.ship.getModifiedItemAttr("rechargeRate")
|
|
sim.stagger = True
|
|
sim.scale = False
|
|
sim.t_max = 6 * 60 * 60 * 1000
|
|
sim.reload = self.factorReload
|
|
sim.run()
|
|
|
|
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
|
|
|
|
@property
|
|
def remoteReps(self):
|
|
force_recalc = False
|
|
for remote_type in self.__remoteReps:
|
|
if self.__remoteReps[remote_type] is None:
|
|
force_recalc = True
|
|
break
|
|
|
|
if force_recalc is False:
|
|
return self.__remoteReps
|
|
|
|
# We are rerunning the recalcs. Explicitly set to 0 to make sure we don't duplicate anything and correctly set all values to 0.
|
|
for remote_type in self.__remoteReps:
|
|
self.__remoteReps[remote_type] = 0
|
|
|
|
for module in self.modules:
|
|
# Skip empty and non-Active modules
|
|
if module.isEmpty or module.state < State.ACTIVE:
|
|
continue
|
|
|
|
# Covert cycleTime to seconds
|
|
duration = module.cycleTime / 1000
|
|
|
|
# Skip modules with no duration.
|
|
if not duration:
|
|
continue
|
|
|
|
fueledMultiplier = module.getModifiedItemAttr("chargedArmorDamageMultiplier", 1)
|
|
|
|
remote_module_groups = {
|
|
"Remote Armor Repairer" : "Armor",
|
|
"Ancillary Remote Armor Repairer": "Armor",
|
|
"Remote Hull Repairer" : "Hull",
|
|
"Remote Shield Booster" : "Shield",
|
|
"Ancillary Remote Shield Booster": "Shield",
|
|
"Remote Capacitor Transmitter" : "Capacitor",
|
|
}
|
|
|
|
module_group = module.item.group.name
|
|
|
|
if module_group in remote_module_groups:
|
|
remote_type = remote_module_groups[module_group]
|
|
else:
|
|
# Module isn't in our list of remote rep modules, bail
|
|
continue
|
|
|
|
if remote_type == "Hull":
|
|
hp = module.getModifiedItemAttr("structureDamageAmount", 0)
|
|
elif remote_type == "Armor":
|
|
hp = module.getModifiedItemAttr("armorDamageAmount", 0)
|
|
elif remote_type == "Shield":
|
|
hp = module.getModifiedItemAttr("shieldBonus", 0)
|
|
elif remote_type == "Capacitor":
|
|
hp = module.getModifiedItemAttr("powerTransferAmount", 0)
|
|
else:
|
|
hp = 0
|
|
|
|
self.__remoteReps[remote_type] += (hp * fueledMultiplier) / duration
|
|
|
|
return self.__remoteReps
|
|
|
|
@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)
|
|
self.__ehp = ehp
|
|
|
|
return self.__ehp
|
|
|
|
@property
|
|
def tank(self):
|
|
hps = {"passiveShield": self.calculateShieldRecharge()}
|
|
for type in ("shield", "armor", "hull"):
|
|
hps["%sRepair" % type] = self.extraAttributes["%sRepair" % type]
|
|
|
|
return hps
|
|
|
|
@property
|
|
def effectiveTank(self):
|
|
if self.__effectiveTank is None:
|
|
if self.damagePattern is None:
|
|
ehps = self.tank
|
|
else:
|
|
ehps = self.damagePattern.calculateEffectiveTank(self, self.extraAttributes)
|
|
|
|
self.__effectiveTank = ehps
|
|
|
|
return self.__effectiveTank
|
|
|
|
@property
|
|
def effectiveSustainableTank(self):
|
|
if self.__effectiveSustainableTank is None:
|
|
if self.damagePattern is None:
|
|
eshps = self.sustainableTank
|
|
else:
|
|
eshps = self.damagePattern.calculateEffectiveTank(self, self.sustainableTank)
|
|
|
|
self.__effectiveSustainableTank = eshps
|
|
|
|
return self.__effectiveSustainableTank
|
|
|
|
def calculateLockTime(self, radius):
|
|
scanRes = self.ship.getModifiedItemAttr("scanResolution")
|
|
if scanRes is not None and scanRes > 0:
|
|
# Yes, this function returns time in seconds, not miliseconds.
|
|
# 40,000 is indeed the correct constant here.
|
|
return min(40000 / scanRes / asinh(radius) ** 2, 30 * 60)
|
|
else:
|
|
return self.ship.getModifiedItemAttr("scanSpeed") / 1000.0
|
|
|
|
def calculateMiningStats(self):
|
|
minerYield = 0
|
|
droneYield = 0
|
|
|
|
for mod in self.modules:
|
|
minerYield += mod.miningStats
|
|
|
|
for drone in self.drones:
|
|
droneYield += drone.miningStats
|
|
|
|
self.__minerYield = minerYield
|
|
self.__droneYield = droneYield
|
|
|
|
def calculateWeaponStats(self):
|
|
weaponDPS = 0
|
|
droneDPS = 0
|
|
weaponVolley = 0
|
|
droneVolley = 0
|
|
|
|
for mod in self.modules:
|
|
dps, volley = mod.damageStats(self.targetResists)
|
|
weaponDPS += dps
|
|
weaponVolley += volley
|
|
|
|
for drone in self.drones:
|
|
dps, volley = drone.damageStats(self.targetResists)
|
|
droneDPS += dps
|
|
droneVolley += volley
|
|
|
|
for fighter in self.fighters:
|
|
dps, volley = fighter.damageStats(self.targetResists)
|
|
droneDPS += dps
|
|
droneVolley += volley
|
|
|
|
self.__weaponDPS = weaponDPS
|
|
self.__weaponVolley = weaponVolley
|
|
self.__droneDPS = droneDPS
|
|
self.__droneVolley = droneVolley
|
|
|
|
@property
|
|
def fits(self):
|
|
for mod in self.modules:
|
|
if not mod.fits(self):
|
|
return False
|
|
|
|
return True
|
|
|
|
def __deepcopy__(self, memo=None):
|
|
copy_ship = Fit()
|
|
# Character and owner are not copied
|
|
copy_ship.character = self.__character
|
|
copy_ship.owner = self.owner
|
|
copy_ship.ship = deepcopy(self.ship)
|
|
copy_ship.name = "%s copy" % self.name
|
|
copy_ship.damagePattern = self.damagePattern
|
|
copy_ship.targetResists = self.targetResists
|
|
copy_ship.notes = self.notes
|
|
|
|
toCopy = (
|
|
"modules",
|
|
"drones",
|
|
"fighters",
|
|
"cargo",
|
|
"implants",
|
|
"boosters",
|
|
"projectedModules",
|
|
"projectedDrones",
|
|
"projectedFighters")
|
|
for name in toCopy:
|
|
orig = getattr(self, name)
|
|
c = getattr(copy_ship, name)
|
|
for i in orig:
|
|
c.append(deepcopy(i))
|
|
|
|
for fit in self.projectedFits:
|
|
copy_ship.__projectedFits[fit.ID] = fit
|
|
# this bit is required -- see GH issue # 83
|
|
eos.db.saveddata_session.flush()
|
|
eos.db.saveddata_session.refresh(fit)
|
|
|
|
return copy_ship
|
|
|
|
def __repr__(self):
|
|
return u"Fit(ID={}, ship={}, name={}) at {}".format(
|
|
self.ID, self.ship.item.name, self.name, hex(id(self))
|
|
).encode('utf8')
|
|
|
|
def __str__(self):
|
|
return u"{} ({})".format(
|
|
self.name, self.ship.item.name
|
|
).encode('utf8')
|