677 lines
25 KiB
Python
Executable File
677 lines
25 KiB
Python
Executable File
#===============================================================================
|
|
# 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 sqlalchemy.orm import validates, reconstructor
|
|
|
|
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut
|
|
from eos.effectHandlerHelpers import HandledItem, HandledCharge
|
|
from eos.enum import Enum
|
|
from eos.mathUtils import floorFloat
|
|
|
|
class State(Enum):
|
|
OFFLINE = -1
|
|
ONLINE = 0
|
|
ACTIVE = 1
|
|
OVERHEATED = 2
|
|
|
|
class Slot(Enum):
|
|
LOW = 1
|
|
MED = 2
|
|
HIGH = 3
|
|
RIG = 4
|
|
SUBSYSTEM = 5
|
|
|
|
class Hardpoint(Enum):
|
|
NONE = 0
|
|
MISSILE = 1
|
|
TURRET = 2
|
|
|
|
class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
|
"""An instance of this class represents a module together with its charge and modified attributes"""
|
|
DAMAGE_ATTRIBUTES = ("emDamage", "kineticDamage", "explosiveDamage", "thermalDamage")
|
|
MINING_ATTRIBUTES = ("miningAmount", )
|
|
|
|
def __init__(self, item):
|
|
self.__item = item if item != None else 0
|
|
self.itemID = item.ID if item is not None else None
|
|
self.__charge = 0
|
|
self.projected = False
|
|
self.state = State.ONLINE
|
|
self.__dps = None
|
|
self.__miningyield = None
|
|
self.__volley = None
|
|
self.__reloadTime = None
|
|
self.__reloadForce = None
|
|
self.__chargeCycles = None
|
|
self.__itemModifiedAttributes = ModifiedAttributeDict()
|
|
self.__slot = None
|
|
|
|
if item != None:
|
|
self.__itemModifiedAttributes.original = item.attributes
|
|
self.__hardpoint = self.__calculateHardpoint(item)
|
|
self.__slot = self.__calculateSlot(item)
|
|
|
|
self.__chargeModifiedAttributes = ModifiedAttributeDict()
|
|
|
|
@reconstructor
|
|
def init(self):
|
|
if self.dummySlot is None:
|
|
self.__item = None
|
|
self.__charge = None
|
|
self.__volley = None
|
|
self.__dps = None
|
|
self.__miningyield = None
|
|
self.__reloadTime = None
|
|
self.__reloadForce = None
|
|
self.__chargeCycles = None
|
|
else:
|
|
self.__slot = self.dummySlot
|
|
self.__item = 0
|
|
self.__charge = 0
|
|
self.__dps = 0
|
|
self.__miningyield = 0
|
|
self.__volley = 0
|
|
self.__reloadTime = 0
|
|
self.__reloadForce = None
|
|
self.__chargeCycles = 0
|
|
self.__hardpoint = Hardpoint.NONE
|
|
self.__itemModifiedAttributes = ModifiedAttributeDict()
|
|
self.__chargeModifiedAttributes = ModifiedAttributeDict()
|
|
|
|
def __fetchItemInfo(self):
|
|
import eos.db
|
|
item = eos.db.getItem(self.itemID)
|
|
self.__item = item
|
|
self.__itemModifiedAttributes = ModifiedAttributeDict()
|
|
self.__itemModifiedAttributes.original = item.attributes
|
|
self.__hardpoint = self.__calculateHardpoint(item)
|
|
self.__slot = self.__calculateSlot(item)
|
|
|
|
def __fetchChargeInfo(self):
|
|
self.__chargeModifiedAttributes = ModifiedAttributeDict()
|
|
if self.chargeID is not None:
|
|
import eos.db
|
|
charge = eos.db.getItem(self.chargeID)
|
|
self.__charge = charge
|
|
self.__chargeModifiedAttributes.original = charge.attributes
|
|
else:
|
|
self.__charge = 0
|
|
|
|
@classmethod
|
|
def buildEmpty(cls, slot):
|
|
empty = Module(None)
|
|
empty.__slot = slot
|
|
empty.__hardpoint = Hardpoint.NONE
|
|
empty.__item = 0
|
|
empty.__charge = 0
|
|
empty.dummySlot = slot
|
|
empty.__itemModifiedAttributes = ModifiedAttributeDict()
|
|
empty.__chargeModifiedAttributes = ModifiedAttributeDict()
|
|
|
|
return empty
|
|
|
|
@classmethod
|
|
def buildRack(cls, slot):
|
|
empty = Rack(None)
|
|
empty.__slot = slot
|
|
empty.__hardpoint = Hardpoint.NONE
|
|
empty.__item = 0
|
|
empty.__charge = 0
|
|
empty.dummySlot = slot
|
|
empty.__itemModifiedAttributes = ModifiedAttributeDict()
|
|
empty.__chargeModifiedAttributes = ModifiedAttributeDict()
|
|
|
|
return empty
|
|
|
|
@property
|
|
def isEmpty(self):
|
|
return self.dummySlot is not None
|
|
|
|
@property
|
|
def hardpoint(self):
|
|
if self.__item is None:
|
|
self.__fetchItemInfo()
|
|
|
|
return self.__hardpoint
|
|
|
|
@property
|
|
def numCharges(self):
|
|
if self.charge is None:
|
|
charges = 0
|
|
else:
|
|
chargeVolume = self.charge.volume
|
|
containerCapacity = self.item.capacity
|
|
if chargeVolume is None or containerCapacity is None:
|
|
charges = 0
|
|
else:
|
|
charges = floorFloat(float(containerCapacity) / chargeVolume)
|
|
return charges
|
|
|
|
@property
|
|
def numShots(self):
|
|
if self.charge is None:
|
|
return None
|
|
if self.__chargeCycles is None and self.charge:
|
|
numCharges = self.numCharges
|
|
# Usual ammo like projectiles and missiles
|
|
if numCharges > 0 and "chargeRate" in self.itemModifiedAttributes:
|
|
self.__chargeCycles = self.__calculateAmmoShots()
|
|
# Frequency crystals (combat and mining lasers)
|
|
elif numCharges > 0 and "crystalsGetDamaged" in self.chargeModifiedAttributes:
|
|
self.__chargeCycles = self.__calculateCrystalShots()
|
|
# Scripts and stuff
|
|
else:
|
|
self.__chargeCycles = 0
|
|
return self.__chargeCycles
|
|
else:
|
|
return self.__chargeCycles
|
|
|
|
@property
|
|
def hpBeforeReload(self):
|
|
"""
|
|
If item is some kind of repairer with charges, calculate
|
|
HP it reps before going into reload.
|
|
"""
|
|
cycles = self.numShots
|
|
armorRep = self.getModifiedItemAttr("armorDamageAmount") or 0
|
|
shieldRep = self.getModifiedItemAttr("shieldBonus") or 0
|
|
if not cycles or (not armorRep and not shieldRep):
|
|
return None
|
|
hp = round((armorRep + shieldRep) * cycles)
|
|
return hp
|
|
|
|
def __calculateAmmoShots(self):
|
|
if self.charge is not None:
|
|
# Set number of cycles before reload is needed
|
|
chargeRate = self.getModifiedItemAttr("chargeRate")
|
|
numCharges = self.numCharges
|
|
numShots = floorFloat(float(numCharges) / chargeRate)
|
|
else:
|
|
numShots = None
|
|
return numShots
|
|
|
|
def __calculateCrystalShots(self):
|
|
if self.charge is not None:
|
|
if self.getModifiedChargeAttr("crystalsGetDamaged") == 1:
|
|
# For depletable crystals, calculate average amount of shots before it's destroyed
|
|
hp = self.getModifiedChargeAttr("hp")
|
|
chance = self.getModifiedChargeAttr("crystalVolatilityChance")
|
|
damage = self.getModifiedChargeAttr("crystalVolatilityDamage")
|
|
crystals = self.numCharges
|
|
numShots = floorFloat(float(crystals * hp) / (damage * chance))
|
|
else:
|
|
# Set 0 (infinite) for permanent crystals like t1 laser crystals
|
|
numShots = 0
|
|
else:
|
|
numShots = None
|
|
return numShots
|
|
|
|
@property
|
|
def maxRange(self):
|
|
attrs = ("maxRange", "shieldTransferRange", "powerTransferRange",
|
|
"energyDestabilizationRange", "empFieldRange",
|
|
"ecmBurstRange", "warpScrambleRange", "cargoScanRange",
|
|
"shipScanRange", "surveyScanRange")
|
|
for attr in attrs:
|
|
maxRange = self.getModifiedItemAttr(attr)
|
|
if maxRange is not None: return maxRange
|
|
if self.charge is not None:
|
|
try:
|
|
chargeName = self.charge.group.name
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
if chargeName in ("Scanner Probe", "Survey Probe"):
|
|
return None
|
|
# Source: http://www.eveonline.com/ingameboard.asp?a=topic&threadID=1307419&page=1#15
|
|
# D_m = V_m * (T_m + T_0*[exp(- T_m/T_0)-1])
|
|
maxVelocity = self.getModifiedChargeAttr("maxVelocity")
|
|
flightTime = self.getModifiedChargeAttr("explosionDelay") / 1000.0
|
|
mass = self.getModifiedChargeAttr("mass")
|
|
agility = self.getModifiedChargeAttr("agility")
|
|
if maxVelocity and flightTime and mass and agility:
|
|
accelTime = min(flightTime, mass*agility/1000000)
|
|
# Average distance done during acceleration
|
|
duringAcceleration = maxVelocity / 2 * accelTime
|
|
# Distance done after being at full speed
|
|
fullSpeed = maxVelocity * (flightTime - accelTime)
|
|
return duringAcceleration + fullSpeed
|
|
|
|
@property
|
|
def falloff(self):
|
|
attrs = ("falloff", "shipScanFalloff")
|
|
for attr in attrs:
|
|
falloff = self.getModifiedItemAttr(attr)
|
|
if falloff is not None: return falloff
|
|
|
|
@property
|
|
def slot(self):
|
|
if self.__item is None:
|
|
self.__fetchItemInfo()
|
|
|
|
return self.__slot
|
|
|
|
|
|
@property
|
|
def itemModifiedAttributes(self):
|
|
if self.__item is None:
|
|
self.__fetchItemInfo()
|
|
|
|
return self.__itemModifiedAttributes
|
|
|
|
@property
|
|
def chargeModifiedAttributes(self):
|
|
if self.__charge is None:
|
|
self.__fetchChargeInfo()
|
|
|
|
return self.__chargeModifiedAttributes
|
|
|
|
@property
|
|
def item(self):
|
|
if self.__item is None:
|
|
self.__fetchItemInfo()
|
|
|
|
return self.__item if self.__item != 0 else None
|
|
|
|
@property
|
|
def charge(self):
|
|
if self.__charge is None:
|
|
self.__fetchChargeInfo()
|
|
|
|
return self.__charge if self.__charge != 0 else None
|
|
|
|
@charge.setter
|
|
def charge(self, charge):
|
|
self.__charge = charge
|
|
if charge is not None:
|
|
self.chargeID = charge.ID
|
|
self.__chargeModifiedAttributes.original = charge.attributes
|
|
else:
|
|
self.chargeID = None
|
|
self.__chargeModifiedAttributes.original = None
|
|
|
|
self.__itemModifiedAttributes.clear()
|
|
|
|
@property
|
|
def damageStats(self):
|
|
if self.__dps == None:
|
|
if self.isEmpty:
|
|
self.__dps = 0
|
|
self.__volley = 0
|
|
else:
|
|
if self.state >= State.ACTIVE:
|
|
if self.charge:
|
|
volley = sum(map(lambda attr: self.getModifiedChargeAttr(attr) or 0, self.DAMAGE_ATTRIBUTES))
|
|
else:
|
|
volley = sum(map(lambda attr: self.getModifiedItemAttr(attr) or 0, self.DAMAGE_ATTRIBUTES))
|
|
volley *= self.getModifiedItemAttr("damageMultiplier") or 1
|
|
if volley:
|
|
cycleTime = self.cycleTime
|
|
self.__volley = volley
|
|
self.__dps = volley / (cycleTime / 1000.0)
|
|
else:
|
|
self.__volley = 0
|
|
self.__dps = 0
|
|
else:
|
|
self.__volley = 0
|
|
self.__dps = 0
|
|
|
|
return self.__dps, self.__volley
|
|
|
|
@property
|
|
def miningStats(self):
|
|
if self.__miningyield == None:
|
|
if self.isEmpty:
|
|
self.__miningyield = 0
|
|
else:
|
|
if self.state >= State.ACTIVE:
|
|
volley = sum(map(lambda attr: self.getModifiedItemAttr(attr) or 0, self.MINING_ATTRIBUTES))
|
|
if volley:
|
|
cycleTime = self.cycleTime
|
|
self.__miningyield = volley / (cycleTime / 1000.0)
|
|
else:
|
|
self.__miningyield = 0
|
|
else:
|
|
self.__miningyield = 0
|
|
|
|
return self.__miningyield
|
|
|
|
@property
|
|
def dps(self):
|
|
return self.damageStats[0]
|
|
|
|
@property
|
|
def volley(self):
|
|
return self.damageStats[1]
|
|
|
|
@property
|
|
def reloadTime(self):
|
|
# Get reload time from attrs first, then use
|
|
# custom value specified otherwise (e.g. in effects)
|
|
moduleReloadTime = self.getModifiedItemAttr("reloadTime")
|
|
if moduleReloadTime is None:
|
|
moduleReloadTime = self.__reloadTime
|
|
return moduleReloadTime
|
|
|
|
@reloadTime.setter
|
|
def reloadTime(self, milliseconds):
|
|
self.__reloadTime = milliseconds
|
|
|
|
@property
|
|
def forceReload(self):
|
|
return self.__reloadForce
|
|
|
|
@forceReload.setter
|
|
def forceReload(self, type):
|
|
self.__reloadForce = type
|
|
|
|
def fits(self, fit, hardpointLimit=True):
|
|
slot = self.slot
|
|
if fit.getSlotsFree(slot) <= (0 if self.owner != fit else -1):
|
|
return False
|
|
|
|
# Check ship type restrictions
|
|
fitsOnType = set()
|
|
fitsOnGroup = set()
|
|
|
|
shipType = self.getModifiedItemAttr("fitsToShipType")
|
|
if shipType is not None:
|
|
fitsOnType.add(shipType)
|
|
|
|
for i in xrange(1, 6):
|
|
shipType = self.getModifiedItemAttr("canFitShipType%d" % i)
|
|
if shipType is not None:
|
|
fitsOnType.add(shipType)
|
|
|
|
|
|
# Check ship group restrictions
|
|
for i in xrange(1, 10):
|
|
shipGroup = self.getModifiedItemAttr("canFitShipGroup%d" % i)
|
|
if shipGroup is not None:
|
|
fitsOnGroup.add(shipGroup)
|
|
|
|
if (len(fitsOnGroup) > 0 or len(fitsOnType) > 0) and fit.ship.item.group.ID not in fitsOnGroup and fit.ship.item.ID not in fitsOnType:
|
|
return False
|
|
|
|
|
|
# If the mod is a subsystem, don't let two subs in the same slot fit
|
|
if self.slot == Slot.SUBSYSTEM:
|
|
subSlot = self.getModifiedItemAttr("subSystemSlot")
|
|
for mod in fit.modules:
|
|
if mod.getModifiedItemAttr("subSystemSlot") == subSlot:
|
|
return False
|
|
|
|
# Check rig sizes
|
|
if self.slot == Slot.RIG:
|
|
if self.getModifiedItemAttr("rigSize") != fit.ship.getModifiedItemAttr("rigSize"):
|
|
return False
|
|
|
|
# Check max group fitted
|
|
max = self.getModifiedItemAttr("maxGroupFitted")
|
|
if max is not None:
|
|
current = 0 if self.owner != fit else -1
|
|
for mod in fit.modules:
|
|
if mod.item and mod.item.groupID == self.item.groupID:
|
|
current += 1
|
|
|
|
if current >= max:
|
|
return False
|
|
|
|
# Check this only if we're told to do so
|
|
if hardpointLimit:
|
|
if self.hardpoint == Hardpoint.TURRET:
|
|
if fit.ship.getModifiedItemAttr('turretSlotsLeft') - fit.getHardpointsUsed(Hardpoint.TURRET) < 1:
|
|
return False
|
|
elif self.hardpoint == Hardpoint.MISSILE:
|
|
if fit.ship.getModifiedItemAttr('launcherSlotsLeft') - fit.getHardpointsUsed(Hardpoint.MISSILE) < 1:
|
|
return False
|
|
|
|
return True
|
|
|
|
def isValidState(self, state):
|
|
"""
|
|
Check if the state is valid for this module, without considering other modules at all
|
|
"""
|
|
#Check if we're within bounds
|
|
if state < -1 or state > 2:
|
|
return False
|
|
elif state >= State.ACTIVE and not self.item.isType("active"):
|
|
return False
|
|
elif state == State.OVERHEATED and not self.item.isType("overheat"):
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def canHaveState(self, state=None, projectedOnto=None):
|
|
"""
|
|
Check with other modules if there are restrictions that might not allow this module to be activated
|
|
"""
|
|
# If we're going to set module to offline or online for local modules or offline for projected,
|
|
# it should be fine for all cases
|
|
item = self.item
|
|
if (state <= State.ONLINE and projectedOnto is None) or (state <= State.OFFLINE):
|
|
return True
|
|
|
|
# Check if the local module is over it's max limit; if it's not, we're fine
|
|
maxGroupActive = self.getModifiedItemAttr("maxGroupActive")
|
|
if maxGroupActive is None and projectedOnto is None:
|
|
return True
|
|
|
|
# Following is applicable only to local modules, we do not want to limit projected
|
|
if projectedOnto is None:
|
|
currActive = 0
|
|
group = item.group.name
|
|
for mod in self.owner.modules:
|
|
currItem = getattr(mod, "item", None)
|
|
if mod.state >= State.ACTIVE and currItem is not None and currItem.group.name == group:
|
|
currActive += 1
|
|
if currActive > maxGroupActive:
|
|
break
|
|
return currActive <= maxGroupActive
|
|
# For projected, we're checking if ship is vulnerable to given item
|
|
else:
|
|
# Do not allow to apply offensive modules on ship with offensive module immunite, with few exceptions
|
|
# (all effects which apply instant modification are exception, generally speaking)
|
|
if item.offensive and projectedOnto.ship.getModifiedItemAttr("disallowOffensiveModifiers") == 1:
|
|
offensiveNonModifiers = set(("energyDestabilizationNew", "leech"))
|
|
if not offensiveNonModifiers.intersection(set(item.effects)):
|
|
return False
|
|
# If assistive modules are not allowed, do not let to apply these altogether
|
|
if item.assistive and projectedOnto.ship.getModifiedItemAttr("disallowAssistance") == 1:
|
|
return False
|
|
return True
|
|
|
|
def isValidCharge(self, charge):
|
|
#Check sizes, if 'charge size > module volume' it won't fit
|
|
if charge is None: return True
|
|
chargeVolume = charge.volume
|
|
moduleCapacity = self.item.capacity
|
|
if chargeVolume is not None and moduleCapacity is not None and chargeVolume > moduleCapacity:
|
|
return False
|
|
|
|
itemChargeSize = self.getModifiedItemAttr("chargeSize")
|
|
if itemChargeSize is not None:
|
|
chargeSize = charge.getAttribute('chargeSize')
|
|
if itemChargeSize != chargeSize:
|
|
return False
|
|
|
|
chargeGroup = charge.groupID
|
|
for i in range(5):
|
|
itemChargeGroup = self.getModifiedItemAttr('chargeGroup' + str(i))
|
|
if itemChargeGroup is None: continue
|
|
if itemChargeGroup == chargeGroup: return True
|
|
|
|
return False
|
|
|
|
def getValidCharges(self):
|
|
validCharges = set()
|
|
import eos.db
|
|
for i in range(5):
|
|
itemChargeGroup = self.getModifiedItemAttr('chargeGroup' + str(i))
|
|
if itemChargeGroup is not None:
|
|
g = eos.db.getGroup(int(itemChargeGroup), eager=("items.icon", "items.attributes"))
|
|
if g is None:
|
|
continue
|
|
for i in g.items:
|
|
if i.published and self.isValidCharge(i):
|
|
validCharges.add(i)
|
|
|
|
return validCharges
|
|
|
|
def __calculateHardpoint(self, item):
|
|
effectHardpointMap = {"turretFitted" : Hardpoint.TURRET,
|
|
"launcherFitted": Hardpoint.MISSILE}
|
|
|
|
if item is None:
|
|
return Hardpoint.NONE
|
|
|
|
for effectName, slot in effectHardpointMap.iteritems():
|
|
if effectName in item.effects:
|
|
return slot
|
|
|
|
return Hardpoint.NONE
|
|
|
|
def __calculateSlot(self, item):
|
|
effectSlotMap = {"rigSlot" : Slot.RIG,
|
|
"loPower" : Slot.LOW,
|
|
"medPower" : Slot.MED,
|
|
"hiPower" : Slot.HIGH,
|
|
"subSystem" : Slot.SUBSYSTEM}
|
|
if item is None:
|
|
return None
|
|
for effectName, slot in effectSlotMap.iteritems():
|
|
if effectName in item.effects:
|
|
return slot
|
|
if item.group.name == "Effect Beacon":
|
|
return Slot.RIG
|
|
|
|
raise ValueError("Passed item does not fit in any known slot")
|
|
|
|
@validates("ID", "itemID", "ammoID")
|
|
def validator(self, key, val):
|
|
map = {"ID": lambda val: isinstance(val, int),
|
|
"itemID" : lambda val: val is None or isinstance(val, int),
|
|
"ammoID" : lambda val: isinstance(val, int)}
|
|
|
|
if map[key](val) == False: raise ValueError(str(val) + " is not a valid value for " + key)
|
|
else: return val
|
|
|
|
def clear(self):
|
|
self.__dps = None
|
|
self.__miningyield = None
|
|
self.__volley = None
|
|
self.__reloadTime = None
|
|
self.__reloadForce = None
|
|
self.__chargeCycles = None
|
|
self.itemModifiedAttributes.clear()
|
|
self.chargeModifiedAttributes.clear()
|
|
|
|
def calculateModifiedAttributes(self, fit, runTime, forceProjected = False):
|
|
#We will run the effect when two conditions are met:
|
|
#1: It makes sense to run the effect
|
|
# The effect is either offline
|
|
# or the effect is passive and the module is in the online state (or higher)
|
|
|
|
# or the effect is active and the module is in the active state (or higher)
|
|
# or the effect is overheat and the module is in the overheated state (or higher)
|
|
#2: the runtimes match
|
|
|
|
if self.projected or forceProjected:
|
|
context = "projected", "module"
|
|
projected = True
|
|
else:
|
|
context = ("module",)
|
|
projected = False
|
|
|
|
if self.charge is not None:
|
|
# fix for #82 and it's regression #106
|
|
if not projected or (self.projected and not forceProjected):
|
|
for effect in self.charge.effects.itervalues():
|
|
if effect.runTime == runTime:
|
|
effect.handler(fit, self, ("moduleCharge",))
|
|
|
|
if self.item:
|
|
if self.state >= State.OVERHEATED:
|
|
for effect in self.item.effects.itervalues():
|
|
if effect.runTime == runTime and effect.isType("overheat"):
|
|
effect.handler(fit, self, context)
|
|
|
|
for effect in self.item.effects.itervalues():
|
|
if effect.runTime == runTime and \
|
|
(effect.isType("offline") or
|
|
(effect.isType("passive") and self.state >= State.ONLINE) or \
|
|
(effect.isType("active") and self.state >= State.ACTIVE)) and \
|
|
((projected and effect.isType("projected")) or not projected):
|
|
effect.handler(fit, self, context)
|
|
|
|
@property
|
|
def cycleTime(self):
|
|
reactivation = (self.getModifiedItemAttr("moduleReactivationDelay") or 0)
|
|
# Reactivation time starts counting after end of module cycle
|
|
speed = self.rawCycleTime + reactivation
|
|
if self.charge:
|
|
reload = self.reloadTime
|
|
else:
|
|
reload = 0.0
|
|
# Determine if we'll take into account reload time or not
|
|
factorReload = self.owner.factorReload if self.forceReload is None else self.forceReload
|
|
# If reactivation is longer than 10 seconds then module can be reloaded
|
|
# during reactivation time, thus we may ignore reload
|
|
if factorReload and reactivation < reload:
|
|
numShots = self.numShots
|
|
# Time it takes to reload module after end of reactivation time,
|
|
# given that we started when module cycle has just over
|
|
additionalReloadTime = (reload - reactivation)
|
|
# Speed here already takes into consideration reactivation time
|
|
speed = (speed * numShots + additionalReloadTime) / numShots if numShots > 0 else speed
|
|
|
|
return speed
|
|
|
|
@property
|
|
def rawCycleTime(self):
|
|
speed = self.getModifiedItemAttr("speed") or self.getModifiedItemAttr("duration")
|
|
return speed
|
|
|
|
@property
|
|
def capUse(self):
|
|
capNeed = self.getModifiedItemAttr("capacitorNeed")
|
|
if capNeed and self.state >= State.ACTIVE:
|
|
cycleTime = self.cycleTime
|
|
capUsed = capNeed / (cycleTime / 1000.0)
|
|
return capUsed
|
|
else:
|
|
return 0
|
|
|
|
def __deepcopy__(self, memo):
|
|
item = self.item
|
|
if item is None:
|
|
copy = Module.buildEmpty(self.slot)
|
|
else:
|
|
copy = Module(self.item)
|
|
copy.charge = self.charge
|
|
copy.state = self.state
|
|
return copy
|
|
|
|
class Rack(Module):
|
|
'''
|
|
This is simply the Module class named something else to differentiate
|
|
it for app logic. This class does not do anything special
|
|
'''
|
|
pass
|