1011 lines
35 KiB
Python
1011 lines
35 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/>.
|
|
#===============================================================================
|
|
|
|
from eos.effectHandlerHelpers import *
|
|
from eos.modifiedAttributeDict import ModifiedAttributeDict
|
|
from sqlalchemy.orm import validates, reconstructor
|
|
from itertools import chain
|
|
from eos import capSim
|
|
from copy import deepcopy
|
|
from math import sqrt, log, asinh
|
|
from eos.types import Drone, Cargo, Ship, Character, State, Slot, Module, Implant, Booster, Skill
|
|
from eos.saveddata.module import State
|
|
from eos.saveddata.mode import Mode
|
|
import eos.db
|
|
import time
|
|
import copy
|
|
from utils.timer import Timer
|
|
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
from collections import OrderedDict
|
|
except ImportError:
|
|
from utils.compat import OrderedDict
|
|
|
|
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
|
|
|
|
self.__modules = HandledModuleList()
|
|
self.__drones = HandledDroneCargoList()
|
|
self.__cargo = HandledDroneCargoList()
|
|
self.__implants = HandledImplantBoosterList()
|
|
self.__boosters = HandledImplantBoosterList()
|
|
#self.__projectedFits = {}
|
|
self.__projectedModules = HandledProjectedModList()
|
|
self.__projectedDrones = 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:
|
|
logger.error("Item (id: %d) does not exist", self.shipID)
|
|
return
|
|
|
|
try:
|
|
self.__ship = Ship(item)
|
|
# @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:
|
|
logger.error("Item (id: %d) 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.factorReload = False
|
|
self.fleet = None
|
|
self.boostsFits = set()
|
|
self.gangBoosts = None
|
|
self.ecmProjectedStr = 1
|
|
|
|
@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 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 drones(self):
|
|
return self.__drones
|
|
|
|
@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]
|
|
|
|
def getProjectionInfo(self, fitID):
|
|
return self.projectedOnto.get(fitID, None)
|
|
|
|
@property
|
|
def projectedDrones(self):
|
|
return self.__projectedDrones
|
|
|
|
@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")
|
|
mass = self.ship.getModifiedItemAttr("mass")
|
|
|
|
return -log(0.25) * agility * mass / 1000000
|
|
|
|
@property
|
|
def appliedImplants(self):
|
|
implantsBySlot = {}
|
|
if self.character:
|
|
for implant in self.character.implants:
|
|
implantsBySlot[implant.slot] = implant
|
|
|
|
for implant in self.implants:
|
|
implantsBySlot[implant.slot] = implant
|
|
|
|
return implantsBySlot.values()
|
|
|
|
@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 map[key](val) == False: 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
|
|
del self.__calculatedTargets[:]
|
|
del self.__extraDrains[:]
|
|
|
|
if self.ship:
|
|
self.ship.clear()
|
|
|
|
c = chain(
|
|
self.modules,
|
|
self.drones,
|
|
self.boosters,
|
|
self.implants,
|
|
self.projectedDrones,
|
|
self.projectedModules,
|
|
(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"):
|
|
currModifier.itemModifiedAttributes.fit = self
|
|
if hasattr(currModifier, "chargeModifiedAttributes"):
|
|
currModifier.chargeModifiedAttributes.fit = self
|
|
|
|
def getModifier(self):
|
|
return self.__modifier
|
|
|
|
def getOrigin(self):
|
|
return self.__origin
|
|
|
|
def __calculateGangBoosts(self, runTime):
|
|
logger.debug("Applying gang boosts in `%s` runtime for %s", runTime, repr(self))
|
|
for name, info in self.gangBoosts.iteritems():
|
|
# Unpack all data required to run effect properly
|
|
effect, thing = info[1]
|
|
if effect.runTime == runTime:
|
|
context = ("gang", thing.__class__.__name__.lower())
|
|
if isinstance(thing, Module):
|
|
if effect.isType("offline") or (effect.isType("passive") and thing.state >= State.ONLINE) or \
|
|
(effect.isType("active") and thing.state >= State.ACTIVE):
|
|
# Run effect, and get proper bonuses applied
|
|
try:
|
|
self.register(thing)
|
|
effect.handler(self, thing, context)
|
|
except:
|
|
pass
|
|
else:
|
|
# Run effect, and get proper bonuses applied
|
|
try:
|
|
self.register(thing)
|
|
effect.handler(self, thing, context)
|
|
except:
|
|
pass
|
|
|
|
def calculateModifiedAttributes(self, targetFit=None, withBoosters=False, dirtyStorage=None):
|
|
timer = Timer('Fit: {}, {}'.format(self.ID, self.name), logger)
|
|
logger.debug("Starting fit calculation on: %s, withBoosters: %s", repr(self), withBoosters)
|
|
|
|
shadow = False
|
|
if targetFit:
|
|
logger.debug("Applying projections to target: %s", repr(targetFit))
|
|
projectionInfo = self.getProjectionInfo(targetFit.ID)
|
|
logger.debug("ProjectionInfo: %s", projectionInfo)
|
|
if self == targetFit:
|
|
copied = self # original fit
|
|
shadow = True
|
|
self = copy.deepcopy(self)
|
|
self.fleet = copied.fleet
|
|
logger.debug("Handling self projection - making shadow copy of fit. %s => %s", repr(copied), repr(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)
|
|
|
|
if self.fleet is not None and withBoosters is True:
|
|
logger.debug("Fleet is set, gathering gang boosts")
|
|
self.gangBoosts = self.fleet.recalculateLinear(withBoosters=withBoosters)
|
|
timer.checkpoint("Done calculating gang boosts for %s"%repr(self))
|
|
elif self.fleet is None:
|
|
self.gangBoosts = None
|
|
|
|
# If we're not explicitly asked to project fit onto something,
|
|
# set self as target fit
|
|
if targetFit is None:
|
|
targetFit = self
|
|
projected = False
|
|
else:
|
|
projected = True
|
|
|
|
# If fit is calculated and we have nothing to do here, get out
|
|
|
|
# A note on why projected fits don't get to return here. If we return
|
|
# here, the projection afflictions will not be run as they are
|
|
# intertwined into the regular fit calculations. So, even if the fit has
|
|
# been calculated, we need to recalculate it again just to apply the
|
|
# projections. This is in contract to gang boosts, which are only
|
|
# calculated once, and their items are then looped and accessed with
|
|
# self.gangBoosts.iteritems()
|
|
# We might be able to exit early in the fit calculations if we separate
|
|
# projections from the normal fit calculations. But we must ensure that
|
|
# projection have modifying stuff applied, such as gang boosts and other
|
|
# local modules that may help
|
|
if self.__calculated and not projected:
|
|
logger.debug("Fit has already been calculated and is not projected, returning: %s", repr(self))
|
|
return
|
|
|
|
# Mark fit as calculated
|
|
self.__calculated = True
|
|
|
|
for runTime in ("early", "normal", "late"):
|
|
c = chain(
|
|
(self.character, self.ship),
|
|
self.drones,
|
|
self.boosters,
|
|
self.appliedImplants,
|
|
self.modules
|
|
)
|
|
|
|
if not projected:
|
|
# if not a projected fit, add a couple of more things
|
|
c = chain(c, (self.mode,), self.projectedDrones, self.projectedModules)
|
|
|
|
# We calculate gang bonuses first so that projected fits get them
|
|
if self.gangBoosts is not None:
|
|
self.__calculateGangBoosts(runTime)
|
|
|
|
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:
|
|
self.register(item)
|
|
item.calculateModifiedAttributes(self, runTime, False)
|
|
if projected is True:
|
|
for _ in xrange(projectionInfo.amount):
|
|
targetFit.register(item, origin=self)
|
|
item.calculateModifiedAttributes(targetFit, runTime, True)
|
|
|
|
timer.checkpoint('Done with runtime: %s'%runTime)
|
|
|
|
# Only apply projected fits if fit it not projected itself.
|
|
if not projected:
|
|
for fit in self.projectedFits:
|
|
if fit.getProjectionInfo(self.ID).active:
|
|
fit.calculateModifiedAttributes(self, withBoosters=withBoosters, dirtyStorage=dirtyStorage)
|
|
|
|
timer.checkpoint('Done with fit calculation')
|
|
|
|
if shadow:
|
|
logger.debug("Delete shadow fit object")
|
|
del self
|
|
|
|
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):
|
|
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
|
|
|
|
def getItemAttrSum(self, dict, attr):
|
|
amount = 0
|
|
for mod in dict:
|
|
add = mod.getModifiedItemAttr(attr)
|
|
if add is not None:
|
|
amount += add
|
|
|
|
return amount
|
|
|
|
def getItemAttrOnlineSum(self, 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 self.modules:
|
|
if mod.slot is type and (not mod.isEmpty or countDummies):
|
|
amount += 1
|
|
|
|
return amount
|
|
|
|
def getSlotsFree(self, type, countDummies=False):
|
|
slots = {Slot.LOW: "lowSlots",
|
|
Slot.MED: "medSlots",
|
|
Slot.HIGH: "hiSlots",
|
|
Slot.RIG: "rigSlots",
|
|
Slot.SUBSYSTEM: "maxSubSystems"}
|
|
|
|
if type == Slot.MODE:
|
|
# Mode slot doesn't really exist, return default 0
|
|
return 0
|
|
|
|
slotsUsed = self.getSlotsUsed(type, countDummies)
|
|
totalSlots = self.ship.getModifiedItemAttr(slots[type]) or 0
|
|
return int(totalSlots - slotsUsed)
|
|
|
|
@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 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
|
|
|
|
# Expresses how difficult a target is to probe down with scan probes
|
|
# If this is <1.08, the ship is unproabeable
|
|
@property
|
|
def probeSize(self):
|
|
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:
|
|
# http://forum.eve-ru.com/index.php?showtopic=74195&view=findpost&p=1333691
|
|
# http://forum.eve-ru.com/index.php?showtopic=74195&view=findpost&p=1333763
|
|
# Tests by tester128 and several conclusions by me, prove that cap is in range
|
|
# from 1.1 to 1.12, we're picking average value
|
|
probeSize = max(probeSize, 1.11)
|
|
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")
|
|
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 = {}
|
|
sustainable["armorRepair"] = self.extraAttributes["armorRepair"]
|
|
sustainable["shieldRepair"] = self.extraAttributes["shieldRepair"]
|
|
sustainable["hullRepair"] = self.extraAttributes["hullRepair"]
|
|
else:
|
|
sustainable = {}
|
|
|
|
repairers = []
|
|
#Map a repairer type to the attribute it uses
|
|
groupAttrMap = {"Armor Repair Unit": "armorDamageAmount",
|
|
"Fueled Armor Repairer": "armorDamageAmount",
|
|
"Hull Repair Unit": "structureDamageAmount",
|
|
"Shield Booster": "shieldBonus",
|
|
"Fueled 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",
|
|
"Fueled Shield Booster": "shieldRepair",
|
|
"Remote Armor Repairer": "armorRepair",
|
|
"Remote Shield Booster": "shieldRepair",
|
|
"Fueled 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, cycleTime, capNeed, clipSize=0):
|
|
self.__extraDrains.append((cycleTime, capNeed, 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
|
|
|
|
drains.append((int(fullCycleTime), mod.getModifiedItemAttr("capacitorNeed") or 0, mod.numShots or 0))
|
|
|
|
for fullCycleTime, capNeed, clipSize in self.iterDrains():
|
|
drains.append((int(fullCycleTime), capNeed, clipSize))
|
|
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 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
|
|
|
|
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):
|
|
copy = Fit()
|
|
#Character and owner are not copied
|
|
copy.character = self.__character
|
|
copy.owner = self.owner
|
|
copy.ship = deepcopy(self.ship, memo)
|
|
copy.name = "%s copy" % self.name
|
|
copy.damagePattern = self.damagePattern
|
|
copy.targetResists = self.targetResists
|
|
|
|
toCopy = ("modules", "drones", "cargo", "implants", "boosters", "projectedModules", "projectedDrones")
|
|
for name in toCopy:
|
|
orig = getattr(self, name)
|
|
c = getattr(copy, name)
|
|
for i in orig:
|
|
c.append(deepcopy(i, memo))
|
|
|
|
for fit in self.projectedFits:
|
|
copy.__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
|
|
|
|
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
|
|
)
|