773 lines
41 KiB
Python
Executable File
773 lines
41 KiB
Python
Executable File
import json
|
|
from math import log
|
|
from numbers import Number
|
|
|
|
from logbook import Logger
|
|
|
|
import eos.db
|
|
from config import getVersion
|
|
from service.fit import Fit
|
|
from service.market import Market
|
|
from eos.const import FittingModuleState, FittingHardpoint, FittingSlot
|
|
from service.const import PortEftRigSize
|
|
from eos.saveddata.module import Module
|
|
from eos.saveddata.drone import Drone
|
|
from eos.effectHandlerHelpers import HandledList
|
|
from eos.db import gamedata_session, getCategory, getAttributeInfo, getGroup
|
|
from eos.gamedata import Attribute, Effect, Group, Item, ItemEffect
|
|
from eos.utils.spoolSupport import SpoolType, SpoolOptions
|
|
from gui.fitCommands.calc.module.localAdd import CalcAddLocalModuleCommand
|
|
from gui.fitCommands.calc.module.localRemove import CalcRemoveLocalModulesCommand
|
|
from gui.fitCommands.calc.module.changeCharges import CalcChangeModuleChargesCommand
|
|
from gui.fitCommands.helpers import ModuleInfo
|
|
|
|
|
|
pyfalog = Logger(__name__)
|
|
pyfaVersion = getVersion()
|
|
|
|
|
|
class EfsPort:
|
|
wepTestSet = {}
|
|
version = 0.06
|
|
|
|
@staticmethod
|
|
def attrDirectMap(values, target, source):
|
|
for val in values:
|
|
target[val] = source.getModifiedItemAttr(val)
|
|
|
|
@staticmethod
|
|
def getT2MwdSpeed(fit, sFit):
|
|
fitID = fit.ID
|
|
propID = None
|
|
shipHasMedSlots = fit.ship.getModifiedItemAttr("medSlots") > 0
|
|
shipPower = fit.ship.getModifiedItemAttr("powerOutput")
|
|
# Monitors have a 99% reduction to prop mod power requirements
|
|
if fit.ship.typeName == "Monitor":
|
|
shipPower *= 100
|
|
rigSize = fit.ship.getModifiedItemAttr("rigSize")
|
|
if not shipHasMedSlots:
|
|
return None
|
|
|
|
filterVal = Item.groupID == getGroup("Propulsion Module").ID
|
|
propMods = gamedata_session.query(Item).options().filter(filterVal).all()
|
|
mapPropData = lambda propName: \
|
|
next(map(lambda propMod: {"id": propMod.typeID, "powerReq": propMod.attributes["power"].value},
|
|
(filter(lambda mod: mod.typeName == propName, propMods))))
|
|
mwd5mn = mapPropData("5MN Microwarpdrive II")
|
|
mwd50mn = mapPropData("50MN Microwarpdrive II")
|
|
mwd500mn = mapPropData("500MN Microwarpdrive II")
|
|
mwd50000mn = mapPropData("50000MN Microwarpdrive II")
|
|
if rigSize == PortEftRigSize.SMALL or rigSize is None:
|
|
propID = mwd5mn["id"] if shipPower > mwd5mn["powerReq"] else None
|
|
elif rigSize == PortEftRigSize.MEDIUM:
|
|
propID = mwd50mn["id"] if shipPower > mwd50mn["powerReq"] else mwd5mn["id"]
|
|
elif rigSize == PortEftRigSize.LARGE:
|
|
propID = mwd500mn["id"] if shipPower > mwd500mn["powerReq"] else mwd50mn["id"]
|
|
elif rigSize == PortEftRigSize.CAPITAL:
|
|
propID = mwd50000mn["id"] if shipPower > mwd50000mn["powerReq"] else mwd500mn["id"]
|
|
|
|
if propID is None:
|
|
return None
|
|
cmd = CalcAddLocalModuleCommand(fitID, ModuleInfo(itemID=propID))
|
|
cmd.Do()
|
|
if cmd.needsGuiRecalc:
|
|
sFit.recalc(fit)
|
|
fit = eos.db.getFit(fitID)
|
|
mwdPropSpeed = fit.maxSpeed
|
|
mwdPosition = list(filter(lambda mod: mod.item and mod.item.ID == propID, fit.modules))[0].position
|
|
cmd = CalcRemoveLocalModulesCommand(fitID, [mwdPosition])
|
|
cmd.Do()
|
|
if cmd.needsGuiRecalc:
|
|
sFit.recalc(fit)
|
|
fit = eos.db.getFit(fitID)
|
|
return mwdPropSpeed
|
|
|
|
@staticmethod
|
|
def getPropData(fit, sFit):
|
|
propMods = filter(lambda mod: mod.item and mod.item.group.name == "Propulsion Module", fit.modules)
|
|
activePropWBloomFilter = lambda mod: mod.state > 0 and "signatureRadiusBonus" in mod.item.attributes
|
|
propWithBloom = next(filter(activePropWBloomFilter, propMods), None)
|
|
if propWithBloom is not None:
|
|
oldPropState = propWithBloom.state
|
|
propWithBloom.state = FittingModuleState.ONLINE
|
|
sFit.recalc(fit)
|
|
sp = fit.maxSpeed
|
|
sig = fit.ship.getModifiedItemAttr("signatureRadius")
|
|
propWithBloom.state = oldPropState
|
|
sFit.recalc(fit)
|
|
return {"usingMWD": True, "unpropedSpeed": sp, "unpropedSig": sig}
|
|
return {
|
|
"usingMWD": False,
|
|
"unpropedSpeed": fit.maxSpeed,
|
|
"unpropedSig": fit.ship.getModifiedItemAttr("signatureRadius")
|
|
}
|
|
|
|
@staticmethod
|
|
def getModsInGroups(fit, modGroupNames):
|
|
matchingMods = list(filter(lambda mod: mod.item and mod.item.group.name in modGroupNames, fit.modules))
|
|
# Sort mods to prevent the order needlessly changing as pyfa updates.
|
|
matchingMods.sort(key=lambda mod: mod.item.ID)
|
|
matchingMods.sort(key=lambda mod: mod.item.group.ID)
|
|
return matchingMods
|
|
|
|
# Note this also includes data for any cap boosters as they "repair" cap.
|
|
@staticmethod
|
|
def getRepairData(fit, sFit):
|
|
modGroupNames = [
|
|
"Shield Booster", "Armor Repair Unit",
|
|
"Ancillary Shield Booster", "Ancillary Armor Repairer",
|
|
"Hull Repair Unit", "Capacitor Booster",
|
|
]
|
|
repairMods = EfsPort.getModsInGroups(fit, modGroupNames)
|
|
repairs = [];
|
|
for mod in repairMods:
|
|
stats = {}
|
|
EfsPort.attrDirectMap(["duration", "capacitorNeed"], stats, mod)
|
|
if mod.item.group.name in ["Armor Repair Unit", "Ancillary Armor Repairer"]:
|
|
stats["type"] = "Armor Repairer"
|
|
EfsPort.attrDirectMap(["armorDamageAmount"], stats, mod)
|
|
if mod.item.group.name == "Ancillary Armor Repairer":
|
|
stats["numShots"] = mod.numShots
|
|
EfsPort.attrDirectMap(["reloadTime", "chargedArmorDamageMultiplier"], stats, mod)
|
|
elif mod.item.group.name in ["Shield Booster", "Ancillary Shield Booster"]:
|
|
stats["type"] = "Shield Booster"
|
|
EfsPort.attrDirectMap(["shieldBonus"], stats, mod)
|
|
if mod.item.group.name == "Ancillary Shield Booster":
|
|
stats["numShots"] = mod.numShots
|
|
EfsPort.attrDirectMap(["reloadTime"], stats, mod)
|
|
c = mod.charge
|
|
if c:
|
|
sFit.recalc(fit)
|
|
CalcChangeModuleChargesCommand(
|
|
fit.ID,
|
|
projected=False,
|
|
chargeMap={mod.position: None},
|
|
recalc=False).Do()
|
|
sFit.recalc(fit)
|
|
stats["unloadedCapacitorNeed"] = mod.getModifiedItemAttr("capacitorNeed")
|
|
CalcChangeModuleChargesCommand(
|
|
fit.ID,
|
|
projected=False,
|
|
chargeMap={mod.position: c.typeID},
|
|
recalc=False).Do()
|
|
sFit.recalc(fit)
|
|
elif mod.item.group.name == "Capacitor Booster":
|
|
# The capacitorNeed is negative, which provides the boost.
|
|
stats["type"] = "Capacitor Booster"
|
|
stats["numShots"] = mod.numShots
|
|
EfsPort.attrDirectMap(["reloadTime"], stats, mod)
|
|
repairs.append(stats)
|
|
return repairs
|
|
|
|
@staticmethod
|
|
def getOutgoingProjectionData(fit):
|
|
# This is a subset of module groups capable of projection and a superset of those currently used by efs
|
|
modGroupNames = [
|
|
"Remote Shield Booster", "Warp Scrambler", "Stasis Web", "Remote Capacitor Transmitter",
|
|
"Energy Nosferatu", "Energy Neutralizer", "Burst Jammer", "ECM", "Sensor Dampener",
|
|
"Weapon Disruptor", "Remote Armor Repairer", "Target Painter", "Remote Hull Repairer",
|
|
"Burst Projectors", "Warp Disrupt Field Generator", "Armor Resistance Shift Hardener",
|
|
"Micro Jump Drive", "Ship Modifiers", "Stasis Grappler", "Ancillary Remote Shield Booster",
|
|
"Ancillary Remote Armor Repairer", "Titan Phenomena Generator", "Non-Repeating Hardeners",
|
|
"Mutadaptive Remote Armor Repairer"
|
|
]
|
|
projectedMods = EfsPort.getModsInGroups(fit, modGroupNames)
|
|
projections = []
|
|
for mod in projectedMods:
|
|
maxRangeDefault = 0
|
|
falloffDefault = 0
|
|
stats = {}
|
|
if mod.item.group.name in ["Stasis Web", "Stasis Grappler"]:
|
|
stats["type"] = "Stasis Web"
|
|
stats["optimal"] = mod.getModifiedItemAttr("maxRange")
|
|
EfsPort.attrDirectMap(["duration", "speedFactor"], stats, mod)
|
|
elif mod.item.group.name == "Weapon Disruptor":
|
|
stats["type"] = "Weapon Disruptor"
|
|
stats["optimal"] = mod.getModifiedItemAttr("maxRange")
|
|
stats["falloff"] = mod.getModifiedItemAttr("falloffEffectiveness")
|
|
EfsPort.attrDirectMap([
|
|
"trackingSpeedBonus", "maxRangeBonus", "falloffBonus", "aoeCloudSizeBonus",
|
|
"aoeVelocityBonus", "missileVelocityBonus", "explosionDelayBonus"
|
|
], stats, mod)
|
|
elif mod.item.group.name == "Energy Nosferatu":
|
|
stats["type"] = "Energy Nosferatu"
|
|
EfsPort.attrDirectMap(["powerTransferAmount", "energyNeutralizerSignatureResolution"], stats, mod)
|
|
elif mod.item.group.name == "Energy Neutralizer":
|
|
stats["type"] = "Energy Neutralizer"
|
|
EfsPort.attrDirectMap([
|
|
"energyNeutralizerSignatureResolution", "entityCapacitorLevelModifierSmall",
|
|
"entityCapacitorLevelModifierMedium", "entityCapacitorLevelModifierLarge",
|
|
"energyNeutralizerAmount"
|
|
], stats, mod)
|
|
elif mod.item.group.name in ["Remote Shield Booster", "Ancillary Remote Shield Booster"]:
|
|
stats["type"] = "Remote Shield Booster"
|
|
EfsPort.attrDirectMap(["shieldBonus"], stats, mod)
|
|
elif mod.item.group.name in [
|
|
"Remote Armor Repairer", "Ancillary Remote Armor Repairer", "Mutadaptive Remote Armor Repairer"
|
|
]:
|
|
stats["type"] = "Remote Armor Repairer"
|
|
EfsPort.attrDirectMap(["armorDamageAmount"], stats, mod)
|
|
elif mod.item.group.name in ["Remote Capacitor Transmitter"]:
|
|
stats["type"] = "Remote Capacitor Transmitter"
|
|
EfsPort.attrDirectMap(["powerTransferAmount"], stats, mod)
|
|
elif mod.item.group.name == "Warp Scrambler":
|
|
stats["type"] = "Warp Scrambler"
|
|
EfsPort.attrDirectMap(["activationBlockedStrenght", "warpScrambleStrength"], stats, mod)
|
|
elif mod.item.group.name == "Warp Disrupt Field Generator":
|
|
maxRangeDefault = mod.getModifiedItemAttr("warpScrambleRange")
|
|
stats["type"] = "Warp Scrambler"
|
|
EfsPort.attrDirectMap(["activationBlockedStrenght", "warpScrambleStrength"], stats, mod)
|
|
if maxRangeDefault >= 30000:
|
|
# We want this to be 0 for disruption scripts as we have no other way to tell scrams from points.
|
|
stats["activationBlockedStrenght"] = 0
|
|
elif mod.item.group.name == "Target Painter":
|
|
stats["type"] = "Target Painter"
|
|
EfsPort.attrDirectMap(["signatureRadiusBonus"], stats, mod)
|
|
elif mod.item.group.name == "Sensor Dampener":
|
|
stats["type"] = "Sensor Dampener"
|
|
EfsPort.attrDirectMap(["maxTargetRangeBonus", "scanResolutionBonus"], stats, mod)
|
|
elif mod.item.group.name == "ECM":
|
|
stats["type"] = "ECM"
|
|
EfsPort.attrDirectMap([
|
|
"scanGravimetricStrengthBonus", "scanMagnetometricStrengthBonus",
|
|
"scanRadarStrengthBonus", "scanLadarStrengthBonus",
|
|
], stats, mod)
|
|
elif mod.item.group.name == "Burst Jammer":
|
|
stats["type"] = "Burst Jammer"
|
|
maxRangeDefault = mod.getModifiedItemAttr("ecmBurstRange")
|
|
EfsPort.attrDirectMap([
|
|
"scanGravimetricStrengthBonus", "scanMagnetometricStrengthBonus",
|
|
"scanRadarStrengthBonus", "scanLadarStrengthBonus",
|
|
], stats, mod)
|
|
elif mod.item.group.name == "Micro Jump Drive":
|
|
stats["type"] = "Micro Jump Drive"
|
|
EfsPort.attrDirectMap(["moduleReactivationDelay"], stats, mod)
|
|
else:
|
|
pyfalog.error("Projected module {0} lacks efs export implementation".format(mod.item.typeName))
|
|
if mod.getModifiedItemAttr("maxRange", None) is None:
|
|
pyfalog.error("Projected module {0} has no maxRange".format(mod.item.typeName))
|
|
|
|
# Burst jammer maxRange is 0 if the value is retrieved using mod.getModifiedItemAttr("maxRange")
|
|
# Despite it is correct, it still pulls 0.0.
|
|
stats["optimal"] = mod.getModifiedItemAttr("maxRange", maxRangeDefault) if mod.item.group.name != "Burst Jammer" else mod.maxRange
|
|
stats["falloff"] = mod.getModifiedItemAttr("falloffEffectiveness", falloffDefault)
|
|
EfsPort.attrDirectMap(["duration", "capacitorNeed"], stats, mod)
|
|
projections.append(stats)
|
|
return projections
|
|
|
|
# Note that unless padTypeIDs is True all 0s will be removed from modTypeIDs in the return.
|
|
# They always are added initially for the sake of brevity, as this option may not be retained long term.
|
|
@staticmethod
|
|
def getModuleInfo(fit, padTypeIDs=False):
|
|
moduleNames = []
|
|
modTypeIDs = []
|
|
moduleNameSets = {FittingSlot.LOW: [], FittingSlot.MED: [], FittingSlot.HIGH: [], FittingSlot.RIG: [], FittingSlot.SUBSYSTEM: []}
|
|
modTypeIDSets = {FittingSlot.LOW: [], FittingSlot.MED: [], FittingSlot.HIGH: [], FittingSlot.RIG: [], FittingSlot.SUBSYSTEM: []}
|
|
for mod in fit.modules:
|
|
try:
|
|
if mod.item is not None:
|
|
if mod.charge is not None:
|
|
modTypeIDSets[mod.slot].append([mod.item.typeID, mod.charge.typeID])
|
|
moduleNameSets[mod.slot].append(mod.item.typeName + ": " + mod.charge.typeName)
|
|
else:
|
|
modTypeIDSets[mod.slot].append(mod.item.typeID)
|
|
moduleNameSets[mod.slot].append(mod.item.typeName)
|
|
else:
|
|
modTypeIDSets[mod.slot].append(0)
|
|
moduleNameSets[mod.slot].append("Empty Slot")
|
|
except (KeyboardInterrupt, SystemExit):
|
|
raise
|
|
except:
|
|
pyfalog.error("Could not find name for module {0}".format(vars(mod)))
|
|
|
|
for modInfo in [
|
|
["High Slots:"], moduleNameSets[FittingSlot.HIGH], ["", "Med Slots:"], moduleNameSets[FittingSlot.MED],
|
|
["", "Low Slots:"], moduleNameSets[FittingSlot.LOW], ["", "Rig Slots:"], moduleNameSets[FittingSlot.RIG]
|
|
]:
|
|
moduleNames.extend(modInfo)
|
|
if len(moduleNameSets[FittingSlot.SUBSYSTEM]) > 0:
|
|
moduleNames.extend(["", "Subsystems:"])
|
|
moduleNames.extend(moduleNameSets[FittingSlot.SUBSYSTEM])
|
|
|
|
for slotType in [FittingSlot.HIGH, FittingSlot.MED, FittingSlot.LOW, FittingSlot.RIG, FittingSlot.SUBSYSTEM]:
|
|
if slotType is not FittingSlot.SUBSYSTEM or len(modTypeIDSets[slotType]) > 0:
|
|
modTypeIDs.extend([0, 0] if slotType is not FittingSlot.HIGH else [0])
|
|
modTypeIDs.extend(modTypeIDSets[slotType])
|
|
|
|
droneNames = []
|
|
droneIDs = []
|
|
fighterNames = []
|
|
fighterIDs = []
|
|
for drone in fit.drones:
|
|
if drone.amountActive > 0:
|
|
droneIDs.append(drone.item.typeID)
|
|
droneNames.append("%s x%s" % (drone.item.typeName, drone.amount))
|
|
for fighter in fit.fighters:
|
|
if fighter.amount > 0:
|
|
fighterIDs.append(fighter.item.typeID)
|
|
fighterNames.append("%s x%s" % (fighter.item.typeName, fighter.amount))
|
|
if len(droneNames) > 0:
|
|
modTypeIDs.extend([0, 0])
|
|
modTypeIDs.extend(droneIDs)
|
|
moduleNames.extend(["", "Drones:"])
|
|
moduleNames.extend(droneNames)
|
|
if len(fighterNames) > 0:
|
|
modTypeIDs.extend([0, 0])
|
|
modTypeIDs.extend(fighterIDs)
|
|
moduleNames.extend(["", "Fighters:"])
|
|
moduleNames.extend(fighterNames)
|
|
if len(fit.implants) > 0:
|
|
modTypeIDs.extend([0, 0])
|
|
moduleNames.extend(["", "Implants:"])
|
|
for implant in fit.implants:
|
|
modTypeIDs.append(implant.item.typeID)
|
|
moduleNames.append(implant.item.typeName)
|
|
if len(fit.boosters) > 0:
|
|
modTypeIDs.extend([0, 0])
|
|
moduleNames.extend(["", "Boosters:"])
|
|
for booster in fit.boosters:
|
|
modTypeIDs.append(booster.item.typeID)
|
|
moduleNames.append(booster.item.typeName)
|
|
if len(fit.commandFits) > 0:
|
|
modTypeIDs.extend([0, 0])
|
|
moduleNames.extend(["", "Command Fits:"])
|
|
for commandFit in fit.commandFits:
|
|
modTypeIDs.append(commandFit.ship.item.typeID)
|
|
moduleNames.append(commandFit.name)
|
|
if len(fit.projectedModules) > 0:
|
|
modTypeIDs.extend([0, 0])
|
|
moduleNames.extend(["", "Projected Modules:"])
|
|
for mod in fit.projectedModules:
|
|
modTypeIDs.append(mod.item.typeID)
|
|
moduleNames.append(mod.item.typeName)
|
|
|
|
if fit.character.name != "All 5":
|
|
modTypeIDs.extend([0, 0, 0])
|
|
moduleNames.extend(["", "Character:"])
|
|
moduleNames.append(fit.character.name)
|
|
if padTypeIDs is not True:
|
|
modTypeIDsUnpadded = [mod for mod in modTypeIDs if mod != 0]
|
|
modTypeIDs = modTypeIDsUnpadded
|
|
return {"moduleNames": moduleNames, "modTypeIDs": modTypeIDs}
|
|
|
|
@staticmethod
|
|
def getFighterAbilityData(fighterAttr, fighter, baseRef):
|
|
baseRefDam = baseRef + "Damage"
|
|
abilityName = "RegularAttack" if baseRef == "fighterAbilityAttackMissile" else "MissileAttack"
|
|
rangeSuffix = "RangeOptimal" if baseRef == "fighterAbilityAttackMissile" else "Range"
|
|
reductionRef = baseRef if baseRef == "fighterAbilityAttackMissile" else baseRefDam
|
|
damageReductionFactor = log(fighterAttr(reductionRef + "ReductionFactor")) / log(fighterAttr(reductionRef + "ReductionSensitivity"))
|
|
damTypes = ["EM", "Therm", "Exp", "Kin"]
|
|
abBaseDamage = sum(map(lambda damType: fighterAttr(baseRefDam + damType), damTypes))
|
|
abDamage = abBaseDamage * fighterAttr(baseRefDam + "Multiplier")
|
|
return {
|
|
"name": abilityName, "volley": abDamage * fighter.amount, "explosionRadius": fighterAttr(baseRef + "ExplosionRadius"),
|
|
"explosionVelocity": fighterAttr(baseRef + "ExplosionVelocity"), "optimal": fighterAttr(baseRef + rangeSuffix),
|
|
"damageReductionFactor": damageReductionFactor, "rof": fighterAttr(baseRef + "Duration"),
|
|
}
|
|
|
|
@staticmethod
|
|
def getWeaponSystemData(fit):
|
|
weaponSystems = []
|
|
groups = {}
|
|
# Export at maximum spool for consistency, spoolup data is exported anyway.
|
|
defaultSpoolValue = 1
|
|
spoolOptions = SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, True)
|
|
for mod in fit.modules:
|
|
if mod.getDps(spoolOptions=spoolOptions).total > 0:
|
|
# Group weapon + ammo combinations that occur more than once
|
|
keystr = str(mod.itemID) + "-" + str(mod.chargeID)
|
|
if keystr in groups:
|
|
groups[keystr][1] += 1
|
|
else:
|
|
groups[keystr] = [mod, 1]
|
|
for wepGroup in groups.values():
|
|
stats = wepGroup[0]
|
|
n = wepGroup[1]
|
|
tracking = 0
|
|
optimalSigRadius = 0
|
|
maxVelocity = 0
|
|
explosionDelay = 0
|
|
damageReductionFactor = 0
|
|
explosionRadius = 0
|
|
explosionVelocity = 0
|
|
aoeFieldRange = 0
|
|
typeing = 'None'
|
|
if stats.charge:
|
|
name = stats.item.typeName + ", " + stats.charge.typeName
|
|
else:
|
|
name = stats.item.typeName
|
|
if stats.hardpoint == FittingHardpoint.TURRET:
|
|
tracking = stats.getModifiedItemAttr("trackingSpeed")
|
|
optimalSigRadius = stats.getModifiedItemAttr('optimalSigRadius')
|
|
typeing = "Turret"
|
|
# Bombs share most attributes with missiles despite not needing the hardpoint
|
|
elif stats.hardpoint == FittingHardpoint.MISSILE or "Bomb Launcher" in stats.item.typeName:
|
|
maxVelocity = stats.getModifiedChargeAttr("maxVelocity")
|
|
explosionDelay = stats.getModifiedChargeAttr("explosionDelay")
|
|
damageReductionFactor = stats.getModifiedChargeAttr("aoeDamageReductionFactor")
|
|
explosionRadius = stats.getModifiedChargeAttr("aoeCloudSize")
|
|
explosionVelocity = stats.getModifiedChargeAttr("aoeVelocity")
|
|
typeing = "Missile"
|
|
# AoE DDs can be treated like missiles with a damageReductionFactor of 0
|
|
elif stats.item.group.name == 'Super Weapon' and stats.maxRange:
|
|
explosionRadius = stats.getModifiedItemAttr("signatureRadius")
|
|
typeing = "Missile"
|
|
elif stats.hardpoint == FittingHardpoint.NONE:
|
|
aoeFieldRange = stats.getModifiedItemAttr("empFieldRange")
|
|
# This also covers non-bomb weapons with dps values and no hardpoints, most notably targeted doomsdays.
|
|
typeing = "SmartBomb"
|
|
# Targeted DDs are the only non drone/fighter weapon without an explicit max range
|
|
if stats.item.group.name == 'Super Weapon' and stats.maxRange is None:
|
|
maxRange = 300000
|
|
else:
|
|
maxRange = stats.maxRange
|
|
|
|
dps_spread_dict = stats.getDps(spoolOptions=spoolOptions, getSpreadDPS=True)
|
|
dps_spread_dict.update((x, y*n) for x, y in dps_spread_dict.items())
|
|
|
|
statDict = {
|
|
"dps": stats.getDps(spoolOptions=spoolOptions).total * n, "capUse": stats.capUse * n, "falloff": stats.falloff,
|
|
"type": typeing, "name": name, "optimal": maxRange,
|
|
"numCharges": stats.numCharges, "numShots": stats.numShots, "reloadTime": stats.reloadTime,
|
|
"cycleTime": stats.getCycleParameters().averageTime, "volley": stats.getVolley(spoolOptions=spoolOptions).total * n, "tracking": tracking,
|
|
"maxVelocity": maxVelocity, "explosionDelay": explosionDelay, "damageReductionFactor": damageReductionFactor,
|
|
"explosionRadius": explosionRadius, "explosionVelocity": explosionVelocity, "aoeFieldRange": aoeFieldRange,
|
|
"damageMultiplierBonusMax": stats.getModifiedItemAttr("damageMultiplierBonusMax"),
|
|
"damageMultiplierBonusPerCycle": stats.getModifiedItemAttr("damageMultiplierBonusPerCycle"),
|
|
"dps_spread": dps_spread_dict, "optimalSigRadius": optimalSigRadius
|
|
}
|
|
weaponSystems.append(statDict)
|
|
for drone in fit.drones:
|
|
if drone.getDps().total > 0 and drone.amountActive > 0:
|
|
droneAttr = drone.getModifiedItemAttr
|
|
# Drones are using the old tracking formula for trackingSpeed. This updates it to match turrets.
|
|
newTracking = droneAttr("trackingSpeed") / (droneAttr("optimalSigRadius") / 40000)
|
|
statDict = {
|
|
"dps": drone.getDps().total, "cycleTime": drone.getCycleParameters().averageTime, "type": "Drone",
|
|
"optimal": drone.maxRange, "name": drone.item.typeName, "falloff": drone.falloff,
|
|
"maxSpeed": droneAttr("maxVelocity"), "tracking": newTracking,
|
|
"volley": drone.getVolley().total
|
|
}
|
|
weaponSystems.append(statDict)
|
|
for fighter in fit.fighters:
|
|
if fighter.getDps().total > 0 and fighter.amount > 0:
|
|
fighterAttr = fighter.getModifiedItemAttr
|
|
abilities = []
|
|
if "fighterAbilityAttackMissileDamageEM" in fighter.item.attributes.keys():
|
|
baseRef = "fighterAbilityAttackMissile"
|
|
ability = EfsPort.getFighterAbilityData(fighterAttr, fighter, baseRef)
|
|
abilities.append(ability)
|
|
if "fighterAbilityMissilesDamageEM" in fighter.item.attributes.keys():
|
|
baseRef = "fighterAbilityMissiles"
|
|
ability = EfsPort.getFighterAbilityData(fighterAttr, fighter, baseRef)
|
|
abilities.append(ability)
|
|
statDict = {
|
|
"dps": fighter.getDps().total, "type": "Fighter", "name": fighter.item.typeName,
|
|
"maxSpeed": fighterAttr("maxVelocity"), "abilities": abilities,
|
|
"ehp": fighterAttr("shieldCapacity") / 0.8875 * fighter.amount,
|
|
"volley": fighter.getVolley().total, "signatureRadius": fighterAttr("signatureRadius")
|
|
}
|
|
weaponSystems.append(statDict)
|
|
return weaponSystems
|
|
|
|
@staticmethod
|
|
def getTestSet(setType):
|
|
def getT2ItemsWhere(additionalFilter, mustBeOffensive=False, category="Module"):
|
|
# Used to obtain a smaller subset of items while still containing examples of each group.
|
|
T2_META_LEVEL = 5
|
|
categoryID = getCategory(category).categoryID
|
|
result = gamedata_session.query(Item).join(ItemEffect, Group, Attribute).\
|
|
filter(
|
|
additionalFilter,
|
|
Item.metaLevel == T2_META_LEVEL,
|
|
Group.categoryID == categoryID,
|
|
).all()
|
|
if mustBeOffensive:
|
|
result = filter(lambda t: t.offensive is True, result)
|
|
return list(result)
|
|
|
|
def getChargeType(item, setType):
|
|
if setType == "turret":
|
|
return str(item.attributes["chargeGroup1"].value) + "-" + str(item.attributes["chargeSize"].value)
|
|
return str(item.attributes["chargeGroup1"].value)
|
|
|
|
if setType in EfsPort.wepTestSet.keys():
|
|
return EfsPort.wepTestSet[setType]
|
|
else:
|
|
EfsPort.wepTestSet[setType] = []
|
|
modSet = EfsPort.wepTestSet[setType]
|
|
|
|
if setType == "drone":
|
|
ilist = getT2ItemsWhere(True, True, "Drone")
|
|
for item in ilist:
|
|
drone = Drone(item)
|
|
drone.amount = 1
|
|
drone.amountActive = 1
|
|
drone.itemModifiedAttributes.parent = drone
|
|
modSet.append(drone)
|
|
return modSet
|
|
|
|
turretFittedEffectID = gamedata_session.query(Effect).filter(Effect.name == "turretFitted").first().effectID
|
|
launcherFittedEffectID = gamedata_session.query(Effect).filter(Effect.name == "launcherFitted").first().effectID
|
|
if setType == "launcher":
|
|
effectFilter = ItemEffect.effectID == launcherFittedEffectID
|
|
reqOff = False
|
|
else:
|
|
effectFilter = ItemEffect.effectID == turretFittedEffectID
|
|
reqOff = True
|
|
ilist = getT2ItemsWhere(effectFilter, reqOff)
|
|
previousChargeTypes = []
|
|
# Get modules from item list
|
|
for item in ilist:
|
|
chargeType = getChargeType(item, setType)
|
|
# Only add turrets if we don"t already have one with the same size and ammo type.
|
|
if setType == "launcher" or chargeType not in previousChargeTypes:
|
|
previousChargeTypes.append(chargeType)
|
|
mod = Module(item)
|
|
modSet.append(mod)
|
|
|
|
sMkt = Market.getInstance()
|
|
# Due to typed missile damage bonuses we"ll need to add extra launchers to cover all four types.
|
|
additionalLaunchers = []
|
|
for mod in modSet:
|
|
clist = list(gamedata_session.query(Item).options().
|
|
filter(Item.groupID == mod.getModifiedItemAttr("chargeGroup1")).all())
|
|
mods = [mod]
|
|
charges = [clist[0]]
|
|
if setType == "launcher":
|
|
# We don"t want variations of missiles we already have
|
|
prevCharges = list(sMkt.getVariationsByItems(charges))
|
|
testCharges = []
|
|
for charge in clist:
|
|
if charge not in prevCharges:
|
|
testCharges.append(charge)
|
|
prevCharges += sMkt.getVariationsByItems([charge])
|
|
for c in testCharges:
|
|
charges.append(c)
|
|
additionalLauncher = Module(mod.item)
|
|
mods.append(additionalLauncher)
|
|
for i in range(len(mods)):
|
|
mods[i].charge = charges[i]
|
|
mods[i].reloadForce = True
|
|
mods[i].state = 2
|
|
if setType == "launcher" and i > 0:
|
|
additionalLaunchers.append(mods[i])
|
|
modSet += additionalLaunchers
|
|
return modSet
|
|
|
|
@staticmethod
|
|
def getWeaponBonusMultipliers(fit):
|
|
def sumDamage(attr):
|
|
totalDamage = 0
|
|
for damageType in ["emDamage", "thermalDamage", "kineticDamage", "explosiveDamage"]:
|
|
if attr(damageType) is not None:
|
|
totalDamage += attr(damageType)
|
|
return totalDamage
|
|
|
|
def getCurrentMultipliers(tf):
|
|
fitMultipliers = {}
|
|
getDroneMulti = lambda d: sumDamage(d.getModifiedItemAttr) * d.getModifiedItemAttr("damageMultiplier")
|
|
fitMultipliers["drones"] = list(map(getDroneMulti, tf.drones))
|
|
|
|
getFitTurrets = lambda f: filter(lambda mod: mod.hardpoint == FittingHardpoint.TURRET, f.modules)
|
|
getTurretMulti = lambda mod: mod.getModifiedItemAttr("damageMultiplier") / mod.getCycleParameters().averageTime
|
|
fitMultipliers["turrets"] = list(map(getTurretMulti, getFitTurrets(tf)))
|
|
|
|
getFitLaunchers = lambda f: filter(lambda mod: mod.hardpoint == FittingHardpoint.MISSILE, f.modules)
|
|
getLauncherMulti = lambda mod: sumDamage(mod.getModifiedChargeAttr) / mod.getCycleParameters().averageTime
|
|
fitMultipliers["launchers"] = list(map(getLauncherMulti, getFitLaunchers(tf)))
|
|
return fitMultipliers
|
|
|
|
multipliers = {"turret": 1, "launcher": 1, "droneBandwidth": 1}
|
|
drones = EfsPort.getTestSet("drone")
|
|
launchers = EfsPort.getTestSet("launcher")
|
|
turrets = EfsPort.getTestSet("turret")
|
|
for weaponTypeSet in [turrets, launchers, drones]:
|
|
for mod in weaponTypeSet:
|
|
mod.owner = fit
|
|
turrets = list(filter(lambda mod: mod.getModifiedItemAttr("damageMultiplier"), turrets))
|
|
launchers = list(filter(lambda mod: sumDamage(mod.getModifiedChargeAttr), launchers))
|
|
|
|
# Since the effect modules are fairly opaque a mock test fit is used to test the impact of traits.
|
|
# standin class used to prevent . notation causing issues when used as an arg
|
|
class standin:
|
|
pass
|
|
tf = standin()
|
|
tf.modules = HandledList(turrets + launchers)
|
|
tf.character = fit.character
|
|
tf.ship = fit.ship
|
|
tf.drones = HandledList(drones)
|
|
tf.fighters = HandledList([])
|
|
tf.boosters = HandledList([])
|
|
tf.extraAttributes = fit.extraAttributes
|
|
tf.mode = fit.mode
|
|
preTraitMultipliers = getCurrentMultipliers(tf)
|
|
for effect in fit.ship.item.effects.values():
|
|
if effect.isImplemented:
|
|
effect.handler(tf, tf.ship, [], None, effect=effect)
|
|
# Factor in mode effects for T3 Destroyers
|
|
if fit.mode is not None:
|
|
for effect in fit.mode.item.effects.values():
|
|
if effect.isImplemented:
|
|
effect.handler(tf, fit.mode, [], None, effect=effect)
|
|
if fit.ship.item.groupID == getGroup("Strategic Cruiser").ID:
|
|
subSystems = list(filter(lambda mod: mod.slot == FittingSlot.SUBSYSTEM and mod.item, fit.modules))
|
|
for sub in subSystems:
|
|
for effect in sub.item.effects.values():
|
|
if effect.isImplemented:
|
|
effect.handler(tf, sub, [], None, effect=effect)
|
|
postTraitMultipliers = getCurrentMultipliers(tf)
|
|
getMaxRatio = lambda dictA, dictB, key: max(map(lambda a, b: b / a, dictA[key], dictB[key]))
|
|
multipliers["turret"] = round(getMaxRatio(preTraitMultipliers, postTraitMultipliers, "turrets"), 6)
|
|
multipliers["launcher"] = round(getMaxRatio(preTraitMultipliers, postTraitMultipliers, "launchers"), 6)
|
|
multipliers["droneBandwidth"] = round(getMaxRatio(preTraitMultipliers, postTraitMultipliers, "drones"), 6)
|
|
Fit.getInstance().recalc(fit)
|
|
return multipliers
|
|
|
|
@staticmethod
|
|
def getShipSize(groupID):
|
|
# Size groupings are somewhat arbitrary but allow for a more manageable number of top level groupings in a tree structure.
|
|
frigateGroupNames = ["Frigate", "Shuttle", "Corvette", "Assault Frigate", "Covert Ops", "Interceptor",
|
|
"Stealth Bomber", "Electronic Attack Ship", "Expedition Frigate", "Logistics Frigate"]
|
|
destroyerGroupNames = ["Destroyer", "Interdictor", "Tactical Destroyer", "Command Destroyer"]
|
|
cruiserGroupNames = ["Cruiser", "Heavy Assault Cruiser", "Logistics", "Force Recon Ship",
|
|
"Heavy Interdiction Cruiser", "Combat Recon Ship", "Strategic Cruiser"]
|
|
bcGroupNames = ["Combat Battlecruiser", "Command Ship", "Attack Battlecruiser"]
|
|
bsGroupNames = ["Battleship", "Elite Battleship", "Black Ops", "Marauder"]
|
|
capitalGroupNames = ["Titan", "Dreadnought", "Freighter", "Carrier", "Supercarrier",
|
|
"Capital Industrial Ship", "Jump Freighter", "Force Auxiliary"]
|
|
indyGroupNames = ["Hauler", "Deep Space Transport", "Blockade Runner",
|
|
"Mining Barge", "Exhumer", "Industrial Command Ship"]
|
|
miscGroupNames = ["Capsule", "Prototype Exploration Ship"]
|
|
shipSizes = [
|
|
{"name": "Frigate", "groupIDs": map(lambda s: getGroup(s).ID, frigateGroupNames)},
|
|
{"name": "Destroyer", "groupIDs": map(lambda s: getGroup(s).ID, destroyerGroupNames)},
|
|
{"name": "Cruiser", "groupIDs": map(lambda s: getGroup(s).ID, cruiserGroupNames)},
|
|
{"name": "Battlecruiser", "groupIDs": map(lambda s: getGroup(s).ID, bcGroupNames)},
|
|
{"name": "Battleship", "groupIDs": map(lambda s: getGroup(s).ID, bsGroupNames)},
|
|
{"name": "Capital", "groupIDs": map(lambda s: getGroup(s).ID, capitalGroupNames)},
|
|
{"name": "Industrial", "groupIDs": map(lambda s: getGroup(s).ID, indyGroupNames)},
|
|
{"name": "Misc", "groupIDs": map(lambda s: getGroup(s).ID, miscGroupNames)}
|
|
]
|
|
for size in shipSizes:
|
|
if groupID in size["groupIDs"]:
|
|
return size["name"]
|
|
sizeNotFoundMsg = "ShipSize not found for groupID: " + str(groupID)
|
|
return sizeNotFoundMsg
|
|
|
|
@staticmethod
|
|
def exportEfs(fit, typeNotFitFlag, callback):
|
|
sFit = Fit.getInstance()
|
|
includeShipTypeData = typeNotFitFlag > 0
|
|
if includeShipTypeData:
|
|
fitName = fit.name
|
|
else:
|
|
fitName = fit.ship.name + ": " + fit.name
|
|
pyfalog.info("Creating Eve Fleet Simulator data for: " + fit.name)
|
|
fitModAttr = fit.ship.getModifiedItemAttr
|
|
propData = EfsPort.getPropData(fit, sFit)
|
|
mwdPropSpeed = fit.maxSpeed
|
|
if includeShipTypeData:
|
|
mwdPropSpeed = EfsPort.getT2MwdSpeed(fit, sFit)
|
|
projections = EfsPort.getOutgoingProjectionData(fit)
|
|
modInfo = EfsPort.getModuleInfo(fit)
|
|
moduleNames = modInfo["moduleNames"]
|
|
modTypeIDs = modInfo["modTypeIDs"]
|
|
weaponSystems = EfsPort.getWeaponSystemData(fit)
|
|
|
|
|
|
turretSlots = fitModAttr("turretSlotsLeft") if fitModAttr("turretSlotsLeft") is not None else 0
|
|
launcherSlots = fitModAttr("launcherSlotsLeft") if fitModAttr("launcherSlotsLeft") is not None else 0
|
|
droneBandwidth = fitModAttr("droneBandwidth") if fitModAttr("droneBandwidth") is not None else 0
|
|
weaponBonusMultipliers = EfsPort.getWeaponBonusMultipliers(fit)
|
|
effectiveTurretSlots = round(turretSlots * weaponBonusMultipliers["turret"], 2)
|
|
effectiveLauncherSlots = round(launcherSlots * weaponBonusMultipliers["launcher"], 2)
|
|
effectiveDroneBandwidth = round(droneBandwidth * weaponBonusMultipliers["droneBandwidth"], 2)
|
|
# Assume a T2 siege module for dreads
|
|
if fit.ship.item.group.name == "Dreadnought":
|
|
effectiveTurretSlots *= 9.4
|
|
effectiveLauncherSlots *= 15
|
|
hullResonance = {
|
|
"exp": fitModAttr("explosiveDamageResonance"), "kin": fitModAttr("kineticDamageResonance"),
|
|
"therm": fitModAttr("thermalDamageResonance"), "em": fitModAttr("emDamageResonance")
|
|
}
|
|
armorResonance = {
|
|
"exp": fitModAttr("armorExplosiveDamageResonance"), "kin": fitModAttr("armorKineticDamageResonance"),
|
|
"therm": fitModAttr("armorThermalDamageResonance"), "em": fitModAttr("armorEmDamageResonance")
|
|
}
|
|
shieldResonance = {
|
|
"exp": fitModAttr("shieldExplosiveDamageResonance"), "kin": fitModAttr("shieldKineticDamageResonance"),
|
|
"therm": fitModAttr("shieldThermalDamageResonance"), "em": fitModAttr("shieldEmDamageResonance")
|
|
}
|
|
|
|
resonance = {"hull": hullResonance, "armor": armorResonance, "shield": shieldResonance}
|
|
shipSize = EfsPort.getShipSize(fit.ship.item.groupID)
|
|
# Export at maximum spool for consistency, spoolup data is exported anyway.
|
|
defaultSpoolValue = 1
|
|
spoolOptions = SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, True)
|
|
|
|
cargoIDs = []
|
|
for cargo in fit.cargo:
|
|
cargoIDs.append(cargo.itemID)
|
|
|
|
repairs = EfsPort.getRepairData(fit, sFit)
|
|
|
|
def roundNumbers(data, digits):
|
|
if isinstance(data, str):
|
|
return
|
|
if isinstance(data, dict):
|
|
for key in data:
|
|
if isinstance(data[key], Number):
|
|
data[key] = round(data[key], digits)
|
|
else:
|
|
roundNumbers(data[key], digits)
|
|
if isinstance(data, list) or isinstance(data, tuple):
|
|
for val in data:
|
|
roundNumbers(val, digits)
|
|
if isinstance(data, Number):
|
|
rounded = round(data, digits)
|
|
if data != rounded:
|
|
pyfalog.error("Error rounding numbers for EFS export, export may be inconsistent."
|
|
"This suggests the format has been broken somewhere.")
|
|
return
|
|
|
|
try:
|
|
dataDict = {
|
|
"name": fitName, "ehp": fit.ehp, "droneDPS": fit.getDroneDps().total,
|
|
"droneVolley": fit.getDroneVolley().total, "hp": fit.hp, "maxTargets": fit.maxTargets,
|
|
"maxSpeed": fit.maxSpeed, "weaponVolley": fit.getWeaponVolley(spoolOptions=spoolOptions).total,
|
|
"totalVolley": fit.getTotalVolley(spoolOptions=spoolOptions).total, "maxTargetRange": fit.maxTargetRange,
|
|
"scanStrength": fit.scanStrength, "weaponDPS": fit.getWeaponDps(spoolOptions=spoolOptions).total,
|
|
"alignTime": fit.alignTime, "signatureRadius": fitModAttr("signatureRadius"), "weapons": weaponSystems,
|
|
"scanRes": fitModAttr("scanResolution"), "capUsed": fit.capUsed, "capRecharge": fit.capRecharge,
|
|
"capacitorCapacity": fitModAttr("capacitorCapacity"), "rechargeRate": fitModAttr("rechargeRate"),
|
|
"rigSlots": fitModAttr("rigSlots"), "lowSlots": fitModAttr("lowSlots"),
|
|
"midSlots": fitModAttr("medSlots"), "highSlots": fitModAttr("hiSlots"),
|
|
"turretSlots": fitModAttr("turretSlotsLeft"), "launcherSlots": fitModAttr("launcherSlotsLeft"),
|
|
"powerOutput": fitModAttr("powerOutput"), "cpuOutput": fitModAttr("cpuOutput"),
|
|
"rigSize": fitModAttr("rigSize"), "effectiveTurrets": effectiveTurretSlots,
|
|
"effectiveLaunchers": effectiveLauncherSlots, "effectiveDroneBandwidth": effectiveDroneBandwidth,
|
|
"resonance": resonance, "typeID": fit.shipID, "groupID": fit.ship.item.groupID, "shipSize": shipSize,
|
|
"droneControlRange": fitModAttr("droneControlRange"), "mass": fitModAttr("mass"),
|
|
"shieldrechargetime": fitModAttr("shieldRechargeRate"), 'shipinertia': fitModAttr("agility"),
|
|
"energyWarfareResistance": fitModAttr("energyWarfareResistance"),
|
|
"unpropedSpeed": propData["unpropedSpeed"], "unpropedSig": propData["unpropedSig"],
|
|
"usingMWD": propData["usingMWD"], "mwdPropSpeed": mwdPropSpeed, "projections": projections,
|
|
"repairs": repairs, "modTypeIDs": modTypeIDs, "moduleNames": moduleNames, "cargoItemIDs": cargoIDs,
|
|
"pyfaVersion": pyfaVersion, "efsExportVersion": EfsPort.version
|
|
}
|
|
# Recursively round any numbers in dicts to 6 decimal places.
|
|
# This prevents meaningless rounding errors from changing the output whenever pyfa changes.
|
|
roundNumbers(dataDict, 6)
|
|
except TypeError as e:
|
|
pyfalog.error("Error parsing fit:" + str(fit))
|
|
pyfalog.error(e)
|
|
dataDict = {"name": fitName + "Fit could not be correctly parsed"}
|
|
export = json.dumps(dataDict, skipkeys=True)
|
|
|
|
if callback:
|
|
callback(export)
|
|
else:
|
|
return export
|