Files
pyfa/eos/saveddata/module.py
blitzmann 337db326fd Add Mode and separation to fitting list.
Todo: A few exceptions are thrown when trying to remove mode via double click, spawn context menu, move to a different position, etc. All simply because Mode is not a Module. Will need to add try-except blocks to cover these instances
2014-12-02 14:33:47 -05:00

674 lines
25 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 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
MODE = 6 # not a real slot, need for pyfa display rack separation
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_TYPES = ("em", "thermal", "kinetic", "explosive")
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()
def damageStats(self, targetResists):
if self.__dps == None:
self.__dps = 0
self.__volley = 0
if not self.isEmpty and self.state >= State.ACTIVE:
if self.charge:
func = self.getModifiedChargeAttr
else:
func = self.getModifiedItemAttr
volley = sum(map(lambda attr: (func("%sDamage"%attr) or 0) * (1-getattr(targetResists, "%sAmount"%attr, 0)), self.DAMAGE_TYPES))
volley *= self.getModifiedItemAttr("damageMultiplier") or 1
if volley:
cycleTime = self.cycleTime
self.__volley = volley
self.__dps = volley / (cycleTime / 1000.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(None)[0]
@property
def volley(self):
return self.damageStats(None)[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") and \
not forceProjected:
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