diff --git a/eos/db/gamedata/attribute.py b/eos/db/gamedata/attribute.py index e16463e1d..e6bcc80c8 100644 --- a/eos/db/gamedata/attribute.py +++ b/eos/db/gamedata/attribute.py @@ -18,7 +18,7 @@ #=============================================================================== from sqlalchemy import Table, Column, Integer, Float, Unicode, ForeignKey, String, Boolean -from sqlalchemy.orm import relation, mapper, synonym, deferred +from sqlalchemy.orm import relation, mapper, synonym, deferred, remote from sqlalchemy.ext.associationproxy import association_proxy from eos.types import Attribute, Icon, AttributeInfo, Unit from eos.db import gamedata_meta @@ -31,6 +31,7 @@ attributes_table = Table("dgmattribs", gamedata_meta, Column("attributeID", Integer, primary_key = True), Column("attributeName", String), Column("defaultValue", Float), + Column("maxAttributeID", Integer, ForeignKey("dgmattribs.attributeID")), Column("description", Unicode), Column("published", Boolean), Column("displayName", String), diff --git a/eos/modifiedAttributeDict.py b/eos/modifiedAttributeDict.py index ca3615281..79d871ce5 100644 --- a/eos/modifiedAttributeDict.py +++ b/eos/modifiedAttributeDict.py @@ -21,6 +21,7 @@ from math import exp import collections defaultValuesCache = {} +cappingAttrKeyCache = {} class ItemAttrShortcut(object): def getModifiedItemAttr(self, key): @@ -126,10 +127,29 @@ class ModifiedAttributeDict(collections.MutableMapping): return len(keys) def __calculateValue(self, key): + # It's possible that various attributes are capped by other attributes, + # it's defined by reference maxAttributeID + try: + cappingKey = cappingAttrKeyCache[key] + except KeyError: + from eos.db.gamedata.queries import getAttributeInfo + attrInfo = getAttributeInfo(key) + if attrInfo is None: + cappingId = cappingAttrKeyCache[key] = None + else: + cappingId = cappingAttrKeyCache[key] = attrInfo.maxAttributeID + if cappingId is None: + cappingKey = None + else: + cappingAttrInfo = getAttributeInfo(cappingId) + cappingKey = None if cappingAttrInfo is None else cappingAttrInfo.name + cappingValue = self.__calculateValue(cappingKey) if cappingKey is not None else None # If value is forced, we don't have to calculate anything, # just return forced value instead force = self.__forced[key] if key in self.__forced else None if force is not None: + if cappingValue is not None: + force = min(force, cappingValue) return force # Grab our values if they're there, otherwise we'll take default values preIncrease = self.__preIncreases[key] if key in self.__preIncreases else 0 @@ -176,6 +196,10 @@ class ModifiedAttributeDict(collections.MutableMapping): val *= 1 + (bonus - 1) * exp(- i ** 2 / 7.1289) val += postIncrease + # Cap value if we have cap defined + if cappingValue is not None: + val = min(val, cappingValue) + return val def getAfflictions(self, key): diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 7eb67921a..ac66da6f1 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -1,865 +1,865 @@ -#=============================================================================== -# 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 . -#=============================================================================== - -from eos.effectHandlerHelpers import HandledList, HandledModuleList, HandledDroneList, HandledImplantBoosterList, \ -HandledProjectedFitList, HandledProjectedModList, HandledProjectedDroneList, HandledCargoList -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 -import time - -try: - from collections import OrderedDict -except ImportError: - from gui.utils.compat import OrderedDict - -class Fit(object): - """Represents a fitting, with modules, ship, implants, etc.""" - EXTRA_ATTRIBUTES = {"armorRepair": 0, - "hullRepair": 0, - "shieldRepair": 0, - "maxActiveDrones": 0, - "maxTargetsLockedFromSkills": 2, - "droneControlRange": 20000, - "cloaked": False, - "siege": False} - - PEAK_RECHARGE = 0.25 - - def __init__(self): - self.__modules = HandledModuleList() - self.__drones = HandledDroneList() - self.__cargo = HandledCargoList() - self.__implants = HandledImplantBoosterList() - self.__boosters = HandledImplantBoosterList() - self.__projectedFits = HandledProjectedFitList() - self.__projectedModules = HandledProjectedModList() - self.__projectedDrones = HandledProjectedDroneList() - self.__character = None - self.__owner = None - self.shipID = None - self.projected = False - self.name = "" - self.fleet = None - self.boostsFits = set() - self.gangBoosts = None - self.timestamp = time.time() - self.ecmProjectedStr = 1 - self.build() - - @reconstructor - def init(self): - self.build() - - def build(self): - from eos import db - self.__extraDrains = [] - self.__ehp = None - self.__weaponDPS = None - self.__minerYield = None - self.__weaponVolley = None - self.__droneDPS = 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 - self.extraAttributes = ModifiedAttributeDict(self) - self.extraAttributes.original = self.EXTRA_ATTRIBUTES - self.ship = Ship(db.getItem(self.shipID)) if self.shipID is not None else None - - @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 - - @property - def damagePattern(self): - return self.__damagePattern - - @damagePattern.setter - def damagePattern(self, damagePattern): - self.__damagePattern = damagePattern - self.__ehp = None - self.__effectiveTank = 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 - - @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): - return self.__projectedFits - - @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 totalDPS(self): - return self.droneDPS + self.weaponDPS - - @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 min(self.ship.getModifiedItemAttr("maxTargetRange"), 250000) - - @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 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), - "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): - self.__effectiveTank = None - self.__weaponDPS = None - self.__minerYield = None - self.__weaponVolley = None - self.__effectiveSustainableTank = None - self.__sustainableTank = None - self.__droneDPS = 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 is not None: self.ship.clear() - c = chain(self.modules, self.drones, self.boosters, self.implants, self.projectedDrones, self.projectedModules, self.projectedFits, (self.character, self.extraAttributes)) - for stuff in c: - if stuff is not None and stuff != self: stuff.clear() - - #Methods to register and get the thing currently affecting the fit, - #so we can correctly map "Affected By" - def register(self, currModifier): - self.__modifier = currModifier - if hasattr(currModifier, "itemModifiedAttributes"): - currModifier.itemModifiedAttributes.fit = self - if hasattr(currModifier, "chargeModifiedAttributes"): - currModifier.chargeModifiedAttributes.fit = self - - def getModifier(self): - return self.__modifier - - def calculateModifiedAttributes(self, targetFit=None, withBoosters=False, dirtyStorage=None): - refreshBoosts = False - if withBoosters is True: - refreshBoosts = True - if dirtyStorage is not None and self.ID in dirtyStorage: - refreshBoosts = True - if dirtyStorage is not None: - dirtyStorage.update(self.boostsFits) - if self.fleet is not None and refreshBoosts is True: - self.gangBoosts = self.fleet.recalculateLinear(withBoosters=withBoosters, dirtyStorage=dirtyStorage) - elif self.fleet is None: - self.gangBoosts = None - if dirtyStorage is not None: - try: - dirtyStorage.remove(self.ID) - except KeyError: - pass - # If we're not explicitly asked to project fit onto something, - # set self as target fit - if targetFit is None: - targetFit = self - forceProjected = False - # Else, we're checking all target projectee fits - elif targetFit not in self.__calculatedTargets: - self.__calculatedTargets.append(targetFit) - targetFit.calculateModifiedAttributes(dirtyStorage=dirtyStorage) - forceProjected = True - # Or do nothing if target fit is calculated - else: - return - - # If fit is calculated and we have nothing to do here, get out - if self.__calculated == True and forceProjected == False: - return - - # Mark fit as calculated - self.__calculated = True - - # There's a few things to keep in mind here - # 1: Early effects first, then regular ones, then late ones, regardless of anything else - # 2: Some effects aren't implemented - # 3: Some effects are implemented poorly and will just explode on us - # 4: Errors should be handled gracefully and preferably without crashing unless serious - for runTime in ("early", "normal", "late"): - # Build a little chain of stuff - # Avoid adding projected drones and modules when fit is projected onto self - # TODO: remove this workaround when proper self-projection using virtual duplicate fits is implemented - if forceProjected is True: - c = chain((self.character, self.ship), self.drones, self.boosters, self.appliedImplants, self.modules) - else: - c = chain((self.character, self.ship), self.drones, self.boosters, self.appliedImplants, self.modules, - self.projectedDrones, self.projectedModules) - - if self.gangBoosts is not None: - contextMap = {Skill: "skill", - Ship: "ship", - Module: "module", - Implant: "implant"} - 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", contextMap[type(thing)]) - 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: - effect.handler(self, thing, context) - except: - pass - else: - # Run effect, and get proper bonuses applied - try: - effect.handler(self, thing, context) - except: - pass - - 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 forceProjected is True: - targetFit.register(item) - item.calculateModifiedAttributes(targetFit, runTime, True) - - for fit in self.projectedFits: - fit.calculateModifiedAttributes(self, withBoosters=withBoosters, dirtyStorage=dirtyStorage) - - 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"} - - slotsUsed = self.getSlotsUsed(type, countDummies) - totalSlots = self.ship.getModifiedItemAttr(slots[type]) or 0 - return int(totalSlots - slotsUsed) - - @property - def calibrationUsed(self): - return self.getItemAttrSum(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") != 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 - - for mod in self.modules: - dps, volley = mod.damageStats(self.targetResists) - weaponDPS += dps - weaponVolley += volley - - for drone in self.drones: - droneDPS += drone.damageStats(self.targetResists) - - self.__weaponDPS = weaponDPS - self.__weaponVolley = weaponVolley - self.__droneDPS = droneDPS - - @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.append(fit) - - return copy +#=============================================================================== +# 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 . +#=============================================================================== + +from eos.effectHandlerHelpers import HandledList, HandledModuleList, HandledDroneList, HandledImplantBoosterList, \ +HandledProjectedFitList, HandledProjectedModList, HandledProjectedDroneList, HandledCargoList +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 +import time + +try: + from collections import OrderedDict +except ImportError: + from gui.utils.compat import OrderedDict + +class Fit(object): + """Represents a fitting, with modules, ship, implants, etc.""" + EXTRA_ATTRIBUTES = {"armorRepair": 0, + "hullRepair": 0, + "shieldRepair": 0, + "maxActiveDrones": 0, + "maxTargetsLockedFromSkills": 2, + "droneControlRange": 20000, + "cloaked": False, + "siege": False} + + PEAK_RECHARGE = 0.25 + + def __init__(self): + self.__modules = HandledModuleList() + self.__drones = HandledDroneList() + self.__cargo = HandledCargoList() + self.__implants = HandledImplantBoosterList() + self.__boosters = HandledImplantBoosterList() + self.__projectedFits = HandledProjectedFitList() + self.__projectedModules = HandledProjectedModList() + self.__projectedDrones = HandledProjectedDroneList() + self.__character = None + self.__owner = None + self.shipID = None + self.projected = False + self.name = "" + self.fleet = None + self.boostsFits = set() + self.gangBoosts = None + self.timestamp = time.time() + self.ecmProjectedStr = 1 + self.build() + + @reconstructor + def init(self): + self.build() + + def build(self): + from eos import db + self.__extraDrains = [] + self.__ehp = None + self.__weaponDPS = None + self.__minerYield = None + self.__weaponVolley = None + self.__droneDPS = 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 + self.extraAttributes = ModifiedAttributeDict(self) + self.extraAttributes.original = self.EXTRA_ATTRIBUTES + self.ship = Ship(db.getItem(self.shipID)) if self.shipID is not None else None + + @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 + + @property + def damagePattern(self): + return self.__damagePattern + + @damagePattern.setter + def damagePattern(self, damagePattern): + self.__damagePattern = damagePattern + self.__ehp = None + self.__effectiveTank = 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 + + @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): + return self.__projectedFits + + @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 totalDPS(self): + return self.droneDPS + self.weaponDPS + + @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 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), + "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): + self.__effectiveTank = None + self.__weaponDPS = None + self.__minerYield = None + self.__weaponVolley = None + self.__effectiveSustainableTank = None + self.__sustainableTank = None + self.__droneDPS = 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 is not None: self.ship.clear() + c = chain(self.modules, self.drones, self.boosters, self.implants, self.projectedDrones, self.projectedModules, self.projectedFits, (self.character, self.extraAttributes)) + for stuff in c: + if stuff is not None and stuff != self: stuff.clear() + + #Methods to register and get the thing currently affecting the fit, + #so we can correctly map "Affected By" + def register(self, currModifier): + self.__modifier = currModifier + if hasattr(currModifier, "itemModifiedAttributes"): + currModifier.itemModifiedAttributes.fit = self + if hasattr(currModifier, "chargeModifiedAttributes"): + currModifier.chargeModifiedAttributes.fit = self + + def getModifier(self): + return self.__modifier + + def calculateModifiedAttributes(self, targetFit=None, withBoosters=False, dirtyStorage=None): + refreshBoosts = False + if withBoosters is True: + refreshBoosts = True + if dirtyStorage is not None and self.ID in dirtyStorage: + refreshBoosts = True + if dirtyStorage is not None: + dirtyStorage.update(self.boostsFits) + if self.fleet is not None and refreshBoosts is True: + self.gangBoosts = self.fleet.recalculateLinear(withBoosters=withBoosters, dirtyStorage=dirtyStorage) + elif self.fleet is None: + self.gangBoosts = None + if dirtyStorage is not None: + try: + dirtyStorage.remove(self.ID) + except KeyError: + pass + # If we're not explicitly asked to project fit onto something, + # set self as target fit + if targetFit is None: + targetFit = self + forceProjected = False + # Else, we're checking all target projectee fits + elif targetFit not in self.__calculatedTargets: + self.__calculatedTargets.append(targetFit) + targetFit.calculateModifiedAttributes(dirtyStorage=dirtyStorage) + forceProjected = True + # Or do nothing if target fit is calculated + else: + return + + # If fit is calculated and we have nothing to do here, get out + if self.__calculated == True and forceProjected == False: + return + + # Mark fit as calculated + self.__calculated = True + + # There's a few things to keep in mind here + # 1: Early effects first, then regular ones, then late ones, regardless of anything else + # 2: Some effects aren't implemented + # 3: Some effects are implemented poorly and will just explode on us + # 4: Errors should be handled gracefully and preferably without crashing unless serious + for runTime in ("early", "normal", "late"): + # Build a little chain of stuff + # Avoid adding projected drones and modules when fit is projected onto self + # TODO: remove this workaround when proper self-projection using virtual duplicate fits is implemented + if forceProjected is True: + c = chain((self.character, self.ship), self.drones, self.boosters, self.appliedImplants, self.modules) + else: + c = chain((self.character, self.ship), self.drones, self.boosters, self.appliedImplants, self.modules, + self.projectedDrones, self.projectedModules) + + if self.gangBoosts is not None: + contextMap = {Skill: "skill", + Ship: "ship", + Module: "module", + Implant: "implant"} + 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", contextMap[type(thing)]) + 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: + effect.handler(self, thing, context) + except: + pass + else: + # Run effect, and get proper bonuses applied + try: + effect.handler(self, thing, context) + except: + pass + + 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 forceProjected is True: + targetFit.register(item) + item.calculateModifiedAttributes(targetFit, runTime, True) + + for fit in self.projectedFits: + fit.calculateModifiedAttributes(self, withBoosters=withBoosters, dirtyStorage=dirtyStorage) + + 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"} + + slotsUsed = self.getSlotsUsed(type, countDummies) + totalSlots = self.ship.getModifiedItemAttr(slots[type]) or 0 + return int(totalSlots - slotsUsed) + + @property + def calibrationUsed(self): + return self.getItemAttrSum(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") != 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 + + for mod in self.modules: + dps, volley = mod.damageStats(self.targetResists) + weaponDPS += dps + weaponVolley += volley + + for drone in self.drones: + droneDPS += drone.damageStats(self.targetResists) + + self.__weaponDPS = weaponDPS + self.__weaponVolley = weaponVolley + self.__droneDPS = droneDPS + + @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.append(fit) + + return copy diff --git a/staticdata/eve.db b/staticdata/eve.db index 01c0a0058..c4c2a5283 100644 Binary files a/staticdata/eve.db and b/staticdata/eve.db differ