Merge branch 'master' into fix-syntax-warnings

This commit is contained in:
Anton Vorobyov
2024-11-27 23:37:50 +01:00
committed by GitHub
530 changed files with 104471 additions and 23536 deletions

View File

@@ -17,7 +17,7 @@ for:
# init:
# - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e -
install:
- sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y update
- sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y update --allow-releaseinfo-change
# AppImage dependencies
- sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install libfuse2
# Preparation script dependencies

View File

@@ -0,0 +1,15 @@
"""
Migration 48
- added pilot security column (CONCORD ships)
"""
import sqlalchemy
def upgrade(saveddata_engine):
try:
saveddata_engine.execute("SELECT pilotSecurity FROM fits LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE fits ADD COLUMN pilotSecurity FLOAT")

View File

@@ -0,0 +1,15 @@
"""
Migration 49
- added hp column to targetResists table
"""
import sqlalchemy
def upgrade(saveddata_engine):
try:
saveddata_engine.execute("SELECT hp FROM targetResists LIMIT 1;")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE targetResists ADD COLUMN hp FLOAT;")

View File

@@ -63,7 +63,8 @@ fits_table = Table("fits", saveddata_meta,
Column("ignoreRestrictions", Boolean, default=0),
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, default=datetime.datetime.now, onupdate=datetime.datetime.now),
Column("systemSecurity", Integer, nullable=True)
Column("systemSecurity", Integer, nullable=True),
Column("pilotSecurity", Float, nullable=True),
)
projectedFits_table = Table("projectedFits", saveddata_meta,

View File

@@ -37,6 +37,7 @@ targetProfiles_table = Table(
Column('maxVelocity', Float, nullable=True),
Column('signatureRadius', Float, nullable=True),
Column('radius', Float, nullable=True),
Column('hp', Float, nullable=True),
Column('ownerID', ForeignKey('users.ID'), nullable=True),
Column('created', DateTime, nullable=True, default=datetime.datetime.now),
Column('modified', DateTime, nullable=True, onupdate=datetime.datetime.now))
@@ -48,4 +49,5 @@ mapper(
'rawName': targetProfiles_table.c.name,
'_maxVelocity': targetProfiles_table.c.maxVelocity,
'_signatureRadius': targetProfiles_table.c.signatureRadius,
'_radius': targetProfiles_table.c.radius})
'_radius': targetProfiles_table.c.radius,
'_hp': targetProfiles_table.c.hp})

File diff suppressed because it is too large Load Diff

View File

@@ -345,6 +345,7 @@ class Item(EqBase):
500020: "serpentis",
500026: "triglavian",
500027: "upwell",
500029: "deathless",
}
@property
@@ -569,13 +570,18 @@ class DynamicItem(EqBase):
@property
def shortName(self):
name = self.item.customName
keywords = ('Decayed', 'Gravid', 'Unstable', 'Radical')
keywords = (
'Decayed', 'Glorified Decayed',
'Gravid', 'Glorified Gravid',
'Unstable', 'Glorified Unstable',
'Radical', 'Glorified Radical')
for kw in keywords:
if name.startswith(f'{kw} '):
name = kw
m = re.match(r'(?P<mutagrade>\S+) (?P<dronetype>\S+) Drone (?P<mutatype>\S+) Mutaplasmid', name)
m = re.match(r'(?P<mutagrade>(Glorified )?\S+) (?P<dronetype>\S+) Drone (?P<mutatype>\S+) Mutaplasmid', name)
if m:
name = '{} {}'.format(m.group('mutagrade'), m.group('mutatype'))
name = name.replace('Glorified ', 'Gl. ')
return name

View File

@@ -19,6 +19,7 @@
import math
from copy import deepcopy
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
@@ -161,7 +162,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
def getVolleyParameters(self, targetProfile=None):
if not self.dealsDamage or self.amountActive <= 0:
return {0: DmgTypes(0, 0, 0, 0)}
return {0: DmgTypes.default()}
if self.__baseVolley is None:
dmgGetter = self.getModifiedChargeAttr if self.hasAmmo else self.getModifiedItemAttr
dmgMult = self.amountActive * (self.getModifiedItemAttr("damageMultiplier", 1))
@@ -170,11 +171,8 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
volley = DmgTypes(
em=self.__baseVolley.em * (1 - getattr(targetProfile, "emAmount", 0)),
thermal=self.__baseVolley.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=self.__baseVolley.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=self.__baseVolley.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)))
volley = deepcopy(self.__baseVolley)
volley.profile = targetProfile
return {0: volley}
def getVolley(self, targetProfile=None):
@@ -183,16 +181,12 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
def getDps(self, targetProfile=None):
volley = self.getVolley(targetProfile=targetProfile)
if not volley:
return DmgTypes(0, 0, 0, 0)
return DmgTypes.default()
cycleParams = self.getCycleParameters()
if cycleParams is None:
return DmgTypes(0, 0, 0, 0)
return DmgTypes.default()
dpsFactor = 1 / (cycleParams.averageTime / 1000)
dps = DmgTypes(
em=volley.em * dpsFactor,
thermal=volley.thermal * dpsFactor,
kinetic=volley.kinetic * dpsFactor,
explosive=volley.explosive * dpsFactor)
dps = volley * dpsFactor
return dps
def isRemoteRepping(self, ignoreState=False):

View File

@@ -19,6 +19,7 @@
import math
from copy import deepcopy
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
@@ -198,16 +199,14 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
for ability in self.abilities:
# Not passing resists here as we want to calculate and store base volley
self.__baseVolley[ability.effectID] = {0: ability.getVolley()}
adjustedVolley = {}
adjustedVolleys = {}
for effectID, effectData in self.__baseVolley.items():
adjustedVolley[effectID] = {}
for volleyTime, volleyValue in effectData.items():
adjustedVolley[effectID][volleyTime] = DmgTypes(
em=volleyValue.em * (1 - getattr(targetProfile, "emAmount", 0)),
thermal=volleyValue.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=volleyValue.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=volleyValue.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)))
return adjustedVolley
adjustedVolleys[effectID] = {}
for volleyTime, baseVolley in effectData.items():
adjustedVolley = deepcopy(baseVolley)
adjustedVolley.profile = targetProfile
adjustedVolleys[effectID][volleyTime] = adjustedVolley
return adjustedVolleys
def getVolleyPerEffect(self, targetProfile=None):
volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile)
@@ -218,28 +217,16 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def getVolley(self, targetProfile=None):
volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile)
em = 0
therm = 0
kin = 0
exp = 0
volley = DmgTypes.default()
for volleyData in volleyParams.values():
em += volleyData[0].em
therm += volleyData[0].thermal
kin += volleyData[0].kinetic
exp += volleyData[0].explosive
return DmgTypes(em, therm, kin, exp)
volley += volleyData[0]
return volley
def getDps(self, targetProfile=None):
em = 0
thermal = 0
kinetic = 0
explosive = 0
for dps in self.getDpsPerEffect(targetProfile=targetProfile).values():
em += dps.em
thermal += dps.thermal
kinetic += dps.kinetic
explosive += dps.explosive
return DmgTypes(em=em, thermal=thermal, kinetic=kinetic, explosive=explosive)
dps = DmgTypes.default()
for subdps in self.getDpsPerEffect(targetProfile=targetProfile).values():
dps += subdps
return dps
def getDpsPerEffect(self, targetProfile=None):
if not self.active or self.amount <= 0:

View File

@@ -116,7 +116,7 @@ class FighterAbility:
def getVolley(self, targetProfile=None):
if not self.dealsDamage or not self.active:
return DmgTypes(0, 0, 0, 0)
return DmgTypes.default()
if self.attrPrefix == "fighterAbilityLaunchBomb":
em = self.fighter.getModifiedChargeAttr("emDamage", 0)
therm = self.fighter.getModifiedChargeAttr("thermalDamage", 0)
@@ -128,24 +128,17 @@ class FighterAbility:
kin = self.fighter.getModifiedItemAttr("{}DamageKin".format(self.attrPrefix), 0)
exp = self.fighter.getModifiedItemAttr("{}DamageExp".format(self.attrPrefix), 0)
dmgMult = self.fighter.amount * self.fighter.getModifiedItemAttr("{}DamageMultiplier".format(self.attrPrefix), 1)
volley = DmgTypes(
em=em * dmgMult * (1 - getattr(targetProfile, "emAmount", 0)),
thermal=therm * dmgMult * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=kin * dmgMult * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=exp * dmgMult * (1 - getattr(targetProfile, "explosiveAmount", 0)))
volley = DmgTypes(em=em * dmgMult, thermal=therm * dmgMult, kinetic=kin * dmgMult, explosive=exp * dmgMult)
volley.profile = targetProfile
return volley
def getDps(self, targetProfile=None, cycleTimeOverride=None):
volley = self.getVolley(targetProfile=targetProfile)
if not volley:
return DmgTypes(0, 0, 0, 0)
return DmgTypes.default()
cycleTime = cycleTimeOverride if cycleTimeOverride is not None else self.cycleTime
dpsFactor = 1 / (cycleTime / 1000)
dps = DmgTypes(
em=volley.em * dpsFactor,
thermal=volley.thermal * dpsFactor,
kinetic=volley.kinetic * dpsFactor,
explosive=volley.explosive * dpsFactor)
dps = volley * dpsFactor
return dps
def clear(self):

View File

@@ -1688,27 +1688,33 @@ class Fit:
self.__droneWaste = droneWaste
def calculateWeaponDmgStats(self, spoolOptions):
weaponVolley = DmgTypes(0, 0, 0, 0)
weaponDps = DmgTypes(0, 0, 0, 0)
weaponVolley = DmgTypes.default()
weaponDps = DmgTypes.default()
for mod in self.modules:
weaponVolley += mod.getVolley(spoolOptions=spoolOptions, targetProfile=self.targetProfile)
weaponDps += mod.getDps(spoolOptions=spoolOptions, targetProfile=self.targetProfile)
weaponVolley += mod.getVolley(spoolOptions=spoolOptions)
weaponDps += mod.getDps(spoolOptions=spoolOptions)
weaponVolley.profile = self.targetProfile
weaponDps.profile = self.targetProfile
self.__weaponVolleyMap[spoolOptions] = weaponVolley
self.__weaponDpsMap[spoolOptions] = weaponDps
def calculateDroneDmgStats(self):
droneVolley = DmgTypes(0, 0, 0, 0)
droneDps = DmgTypes(0, 0, 0, 0)
droneVolley = DmgTypes.default()
droneDps = DmgTypes.default()
for drone in self.drones:
droneVolley += drone.getVolley(targetProfile=self.targetProfile)
droneDps += drone.getDps(targetProfile=self.targetProfile)
droneVolley += drone.getVolley()
droneDps += drone.getDps()
for fighter in self.fighters:
droneVolley += fighter.getVolley(targetProfile=self.targetProfile)
droneDps += fighter.getDps(targetProfile=self.targetProfile)
droneVolley += fighter.getVolley()
droneDps += fighter.getDps()
droneVolley.profile = self.targetProfile
droneDps.profile = self.targetProfile
self.__droneDps = droneDps
self.__droneVolley = droneVolley
@@ -1743,6 +1749,18 @@ class Fit:
secstatus = FitSystemSecurity.NULLSEC
return secstatus
def getPilotSecurity(self, low_limit=-10, high_limit=5):
secstatus = self.pilotSecurity
# Not defined -> use character SS, with 0.0 fallback if it fails
if secstatus is None:
try:
secstatus = self.character.secStatus
except (SystemExit, KeyboardInterrupt):
raise
except:
secstatus = 0
return max(low_limit, min(high_limit, secstatus))
def activeModulesIter(self):
for mod in self.modules:
if mod.state >= FittingModuleState.ACTIVE:
@@ -1824,6 +1842,7 @@ class Fit:
fitCopy.targetProfile = self.targetProfile
fitCopy.implantLocation = self.implantLocation
fitCopy.systemSecurity = self.systemSecurity
fitCopy.pilotSecurity = self.pilotSecurity
fitCopy.notes = self.notes
for i in self.modules:

View File

@@ -33,7 +33,7 @@ from eos.utils.cycles import CycleInfo, CycleSequence
from eos.utils.default import DEFAULT
from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import calculateSpoolup, resolveSpoolOptions
from eos.utils.stats import DmgTypes, RRTypes
from eos.utils.stats import BreacherInfo, DmgTypes, RRTypes
pyfalog = Logger(__name__)
@@ -453,6 +453,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
return True
return False
@property
def isBreacher(self):
return self.charge and 'dotMissileLaunching' in self.charge.effects
def canDealDamage(self, ignoreState=False):
if self.isEmpty:
return False
@@ -469,75 +473,77 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
def getVolleyParameters(self, spoolOptions=None, targetProfile=None, ignoreState=False):
if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState):
return {0: DmgTypes(0, 0, 0, 0)}
return {0: DmgTypes.default()}
if self.__baseVolley is None:
self.__baseVolley = {}
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
dmgMult = self.getModifiedItemAttr("damageMultiplier", 1)
# Some delay attributes have non-0 default value, so we have to pick according to effects
if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(self.item.effects):
dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0)
elif {'doomsdayBeamDOT', 'doomsdaySlash', 'doomsdayConeDOT', 'debuffLance'}.intersection(self.item.effects):
dmgDelay = self.getModifiedItemAttr("doomsdayWarningDuration", 0)
if self.isBreacher:
dmgDelay = 1
subcycles = math.floor(self.getModifiedChargeAttr("dotDuration", 0) / 1000)
breacher_info = BreacherInfo(
absolute=self.getModifiedChargeAttr("dotMaxDamagePerTick", 0),
relative=self.getModifiedChargeAttr("dotMaxHPPercentagePerTick", 0) / 100)
for i in range(subcycles):
volley = DmgTypes.default()
volley.add_breacher(dmgDelay + i, breacher_info)
self.__baseVolley[dmgDelay + i] = volley
else:
dmgDelay = 0
dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0)
dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0)
# Reaper DD can damage each target only once
if dmgDuration != 0 and dmgSubcycle != 0 and 'doomsdaySlash' not in self.item.effects:
subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle))
else:
subcycles = 1
for i in range(subcycles):
self.__baseVolley[dmgDelay + dmgSubcycle * i] = DmgTypes(
em=(dmgGetter("emDamage", 0)) * dmgMult,
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
dmgMult = self.getModifiedItemAttr("damageMultiplier", 1)
# Some delay attributes have non-0 default value, so we have to pick according to effects
if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(self.item.effects):
dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0)
elif {'doomsdayBeamDOT', 'doomsdaySlash', 'doomsdayConeDOT', 'debuffLance'}.intersection(self.item.effects):
dmgDelay = self.getModifiedItemAttr("doomsdayWarningDuration", 0)
else:
dmgDelay = 0
dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0)
dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0)
# Reaper DD can damage each target only once
if dmgDuration != 0 and dmgSubcycle != 0 and 'doomsdaySlash' not in self.item.effects:
subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle))
else:
subcycles = 1
for i in range(subcycles):
self.__baseVolley[dmgDelay + dmgSubcycle * i] = DmgTypes(
em=(dmgGetter("emDamage", 0)) * dmgMult,
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self)
spoolBoost = calculateSpoolup(
self.getModifiedItemAttr("damageMultiplierBonusMax", 0),
self.getModifiedItemAttr("damageMultiplierBonusPerCycle", 0),
self.rawCycleTime / 1000, spoolType, spoolAmount)[0]
spoolMultiplier = 1 + spoolBoost
adjustedVolley = {}
for volleyTime, volleyValue in self.__baseVolley.items():
adjustedVolley[volleyTime] = DmgTypes(
em=volleyValue.em * spoolMultiplier * (1 - getattr(targetProfile, "emAmount", 0)),
thermal=volleyValue.thermal * spoolMultiplier * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=volleyValue.kinetic * spoolMultiplier * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=volleyValue.explosive * spoolMultiplier * (1 - getattr(targetProfile, "explosiveAmount", 0)))
return adjustedVolley
adjustedVolleys = {}
for volleyTime, baseVolley in self.__baseVolley.items():
adjustedVolley = baseVolley * spoolMultiplier
adjustedVolley.profile = targetProfile
adjustedVolleys[volleyTime] = adjustedVolley
return adjustedVolleys
def getVolley(self, spoolOptions=None, targetProfile=None, ignoreState=False):
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
if len(volleyParams) == 0:
return DmgTypes(0, 0, 0, 0)
return DmgTypes.default()
return volleyParams[min(volleyParams)]
def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False, getSpreadDPS=False):
dmgDuringCycle = DmgTypes(0, 0, 0, 0)
def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False):
dps = DmgTypes.default()
cycleParams = self.getCycleParameters()
if cycleParams is None:
return dmgDuringCycle
return dps
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
avgCycleTime = cycleParams.averageTime
if len(volleyParams) == 0 or avgCycleTime == 0:
return dmgDuringCycle
for volleyValue in volleyParams.values():
dmgDuringCycle += volleyValue
dpsFactor = 1 / (avgCycleTime / 1000)
dps = DmgTypes(
em=dmgDuringCycle.em * dpsFactor,
thermal=dmgDuringCycle.thermal * dpsFactor,
kinetic=dmgDuringCycle.kinetic * dpsFactor,
explosive=dmgDuringCycle.explosive * dpsFactor)
if not getSpreadDPS:
return dps
return {'em':dmgDuringCycle.em * dpsFactor,
'therm': dmgDuringCycle.thermal * dpsFactor,
'kin': dmgDuringCycle.kinetic * dpsFactor,
'exp': dmgDuringCycle.explosive * dpsFactor}
if self.isBreacher:
return volleyParams[min(volleyParams)]
for volleyValue in volleyParams.values():
dps += volleyValue
dpsFactor = 1 / (avgCycleTime / 1000)
dps *= dpsFactor
return dps
def isRemoteRepping(self, ignoreState=False):
repParams = self.getRepAmountParameters(ignoreState=ignoreState)
@@ -949,6 +955,13 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
and ((gang and effect.isType("gang")) or not gang):
effect.handler(fit, self, context, projectionRange, effect=effect)
def getCycleParametersForDps(self, reloadOverride=None):
# Special hack for breachers, since those are DoT and work independently of gun cycle
if self.isBreacher:
return CycleInfo(activeTime=1000, inactiveTime=0, quantity=math.inf, isInactivityReload=False)
else:
return self.getCycleParameters(reloadOverride=reloadOverride)
def getCycleParameters(self, reloadOverride=None):
"""Copied from new eos as well"""
# Determine if we'll take into account reload time or not

View File

@@ -254,7 +254,7 @@ class TargetProfile:
def init(self):
self.builtin = False
def update(self, emAmount=0, thermalAmount=0, kineticAmount=0, explosiveAmount=0, maxVelocity=None, signatureRadius=None, radius=None):
def update(self, emAmount=0, thermalAmount=0, kineticAmount=0, explosiveAmount=0, maxVelocity=None, signatureRadius=None, radius=None, hp=None):
self.emAmount = emAmount
self.thermalAmount = thermalAmount
self.kineticAmount = kineticAmount
@@ -262,6 +262,7 @@ class TargetProfile:
self._maxVelocity = maxVelocity
self._signatureRadius = signatureRadius
self._radius = radius
self._hp = hp
@classmethod
def getBuiltinList(cls):
@@ -331,6 +332,18 @@ class TargetProfile:
def radius(self, val):
self._radius = val
@property
def hp(self):
if self._hp is None or self._hp == -1:
return math.inf
return self._hp
@hp.setter
def hp(self, val):
if val is not None and math.isinf(val):
val = None
self._hp = val
@classmethod
def importPatterns(cls, text):
lines = re.split('[\n\r]+', text)

View File

@@ -18,6 +18,9 @@
# ===============================================================================
import math
from collections import defaultdict
from eos.utils.float import floatUnerr
from utils.repr import makeReprStr
@@ -26,15 +29,133 @@ def _t(x):
return x
class BreacherInfo:
def __init__(self, absolute, relative):
self.absolute = absolute
self.relative = relative
def __mul__(self, mul):
return type(self)(absolute=self.absolute * mul, relative=self.relative * mul)
def __imul__(self, mul):
if mul == 1:
return self
self.absolute *= mul
self.relative *= mul
return self
def __truediv__(self, div):
return type(self)(absolute=self.absolute / div, relative=self.relative / div)
class DmgTypes:
"""Container for damage data stats."""
"""
Container for volley stats, which stores breacher pod data
in raw form, before application of it to target profile.
"""
def __init__(self, em, thermal, kinetic, explosive):
self.em = em
self.thermal = thermal
self.kinetic = kinetic
self.explosive = explosive
self._calcTotal()
self._em = em
self._thermal = thermal
self._kinetic = kinetic
self._explosive = explosive
self._breachers = defaultdict(lambda: [])
self.__profile = None
# Cached data
self.__cached_em = None
self.__cached_thermal = None
self.__cached_kinetic = None
self.__cached_explosive = None
self.__cached_pure = None
self.__cached_total = None
@classmethod
def default(cls):
return cls(0, 0, 0, 0)
def _clear_cached(self):
self.__cached_em = None
self.__cached_thermal = None
self.__cached_kinetic = None
self.__cached_explosive = None
self.__cached_pure = None
self.__cached_total = None
def add_breacher(self, key, data):
self._breachers[key].append(data)
@property
def profile(self):
return self.__profile
@profile.setter
def profile(self, profile):
self.__profile = profile
self._clear_cached()
@property
def em(self):
if self.__cached_em is not None:
return self.__cached_em
dmg = self._em
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "emAmount", 0)
self.__cached_em = dmg
return dmg
@property
def thermal(self):
if self.__cached_thermal is not None:
return self.__cached_thermal
dmg = self._thermal
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "thermalAmount", 0)
self.__cached_thermal = dmg
return dmg
@property
def kinetic(self):
if self.__cached_kinetic is not None:
return self.__cached_kinetic
dmg = self._kinetic
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "kineticAmount", 0)
self.__cached_kinetic = dmg
return dmg
@property
def explosive(self):
if self.__cached_explosive is not None:
return self.__cached_explosive
dmg = self._explosive
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "explosiveAmount", 0)
self.__cached_explosive = dmg
return dmg
@property
def pure(self):
if self.__cached_pure is not None:
return self.__cached_pure
if self.profile is None:
dmg = sum(
max((b.absolute for b in bs), default=0)
for bs in self._breachers.values())
else:
dmg = sum(
max((min(b.absolute, b.relative * getattr(self.profile, "hp", math.inf)) for b in bs), default=0)
for bs in self._breachers.values())
self.__cached_pure = dmg
return dmg
@property
def total(self):
if self.__cached_total is not None:
return self.__cached_total
dmg = self.em + self.thermal + self.kinetic + self.explosive + self.pure
self.__cached_total = dmg
return dmg
# Iterator is needed to support tuple-style unpacking
def __iter__(self):
@@ -42,6 +163,7 @@ class DmgTypes:
yield self.thermal
yield self.kinetic
yield self.explosive
yield self.pure
yield self.total
def __eq__(self, other):
@@ -50,77 +172,87 @@ class DmgTypes:
# Round for comparison's sake because often damage profiles are
# generated from data which includes float errors
return (
floatUnerr(self.em) == floatUnerr(other.em) and
floatUnerr(self.thermal) == floatUnerr(other.thermal) and
floatUnerr(self.kinetic) == floatUnerr(other.kinetic) and
floatUnerr(self.explosive) == floatUnerr(other.explosive) and
floatUnerr(self.total) == floatUnerr(other.total))
def __bool__(self):
return any((
self.em, self.thermal, self.kinetic,
self.explosive, self.total))
def _calcTotal(self):
self.total = self.em + self.thermal + self.kinetic + self.explosive
floatUnerr(self._em) == floatUnerr(other._em) and
floatUnerr(self._thermal) == floatUnerr(other._thermal) and
floatUnerr(self._kinetic) == floatUnerr(other._kinetic) and
floatUnerr(self._explosive) == floatUnerr(other._explosive) and
sorted(self._breachers) == sorted(other._breachers) and
self.profile == other.profile)
def __add__(self, other):
return type(self)(
em=self.em + other.em,
thermal=self.thermal + other.thermal,
kinetic=self.kinetic + other.kinetic,
explosive=self.explosive + other.explosive)
new = type(self)(
em=self._em + other._em,
thermal=self._thermal + other._thermal,
kinetic=self._kinetic + other._kinetic,
explosive=self._explosive + other._explosive)
new.profile = self.profile
for k, v in self._breachers.items():
new._breachers[k].extend(v)
for k, v in other._breachers.items():
new._breachers[k].extend(v)
return new
def __iadd__(self, other):
self.em += other.em
self.thermal += other.thermal
self.kinetic += other.kinetic
self.explosive += other.explosive
self._calcTotal()
self._em += other._em
self._thermal += other._thermal
self._kinetic += other._kinetic
self._explosive += other._explosive
for k, v in other._breachers.items():
self._breachers[k].extend(v)
self._clear_cached()
return self
def __mul__(self, mul):
return type(self)(
em=self.em * mul,
thermal=self.thermal * mul,
kinetic=self.kinetic * mul,
explosive=self.explosive * mul)
new = type(self)(
em=self._em * mul,
thermal=self._thermal * mul,
kinetic=self._kinetic * mul,
explosive=self._explosive * mul)
new.profile = self.profile
for k, v in self._breachers.items():
new._breachers[k] = [b * mul for b in v]
return new
def __imul__(self, mul):
if mul == 1:
return
self.em *= mul
self.thermal *= mul
self.kinetic *= mul
self.explosive *= mul
self._calcTotal()
return self
self._em *= mul
self._thermal *= mul
self._kinetic *= mul
self._explosive *= mul
for v in self._breachers.values():
for b in v:
b *= mul
self._clear_cached()
return self
def __truediv__(self, div):
return type(self)(
em=self.em / div,
thermal=self.thermal / div,
kinetic=self.kinetic / div,
explosive=self.explosive / div)
new = type(self)(
em=self._em / div,
thermal=self._thermal / div,
kinetic=self._kinetic / div,
explosive=self._explosive / div)
new.profile = self.profile
for k, v in self._breachers.items():
new._breachers[k] = [b / div for b in v]
return new
def __itruediv__(self, div):
if div == 1:
return
self.em /= div
self.thermal /= div
self.kinetic /= div
self.explosive /= div
self._calcTotal()
return self
def __bool__(self):
return any((
self._em, self._thermal, self._kinetic, self._explosive,
any(b.absolute or b.relative for b in self._breachers)))
def __repr__(self):
spec = DmgTypes.names()
spec.append('total')
return makeReprStr(self, spec)
class_name = type(self).__name__
return (f'<{class_name}(em={self._em}, thermal={self._thermal}, kinetic={self._kinetic}, '
f'explosive={self._explosive}, breachers={len(self._breachers)})>')
@staticmethod
def names(short=None, postProcessor=None):
def names(short=None, postProcessor=None, includePure=False):
value = [_t('em'), _t('th'), _t('kin'), _t('exp')] if short else [_t('em'), _t('thermal'), _t('kinetic'), _t('explosive')]
if includePure:
value += [_t('pure')]
if postProcessor:
value = [postProcessor(x) for x in value]

View File

@@ -117,7 +117,7 @@ class TimeCache(FitDataCache):
pointData[timeStart] = (dps, volley)
# Gap between items
elif floatUnerr(prevTimeEnd) < floatUnerr(timeStart):
pointData[prevTimeEnd] = (DmgTypes(0, 0, 0, 0), DmgTypes(0, 0, 0, 0))
pointData[prevTimeEnd] = (DmgTypes.default(), DmgTypes.default())
pointData[timeStart] = (dps, volley)
# Changed value
elif dps != prevDps or volley != prevVolley:
@@ -157,7 +157,7 @@ class TimeCache(FitDataCache):
def addDpsVolley(ddKey, addedTimeStart, addedTimeFinish, addedVolleys):
if not addedVolleys:
return
volleySum = sum(addedVolleys, DmgTypes(0, 0, 0, 0))
volleySum = sum(addedVolleys, DmgTypes.default())
if volleySum.total > 0:
addedDps = volleySum / (addedTimeFinish - addedTimeStart)
# We can take "just best" volley, no matter target resistances, because all
@@ -170,24 +170,38 @@ class TimeCache(FitDataCache):
def addDmg(ddKey, addedTime, addedDmg):
if addedDmg.total == 0:
return
addedDmg._breachers = {addedTime + k: v for k, v in addedDmg._breachers.items()}
addedDmg._clear_cached()
intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg
# Modules
for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
cycleParams = mod.getCycleParameters(reloadOverride=True)
cycleParams = mod.getCycleParametersForDps(reloadOverride=True)
if cycleParams is None:
continue
currentTime = 0
nonstopCycles = 0
isBreacher = mod.isBreacher
for cycleTimeMs, inactiveTimeMs, isInactivityReload in cycleParams.iterCycles():
cycleVolleys = []
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
for volleyTimeMs, volley in volleyParams.items():
cycleVolleys.append(volley)
addDmg(mod, currentTime + volleyTimeMs / 1000, volley)
addDpsVolley(mod, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
time = currentTime + volleyTimeMs / 1000
if isBreacher:
time += 1
addDmg(mod, time, volley)
if isBreacher:
break
timeStart = currentTime
timeFinish = currentTime + cycleTimeMs / 1000
if isBreacher:
timeStart += 1
timeFinish += 1
addDpsVolley(mod, timeStart, timeFinish, cycleVolleys)
if inactiveTimeMs > 0:
nonstopCycles = 0
else:

View File

@@ -98,6 +98,8 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
tgt=tgt,
distance=distance,
tgtSigRadius=tgtSigRadius)
elif mod.isBreacher:
applicationMap[mod] = getBreacherMult(mod=mod, distance=distance) if inLockRange else 0
for drone in src.item.activeDronesIter():
if not drone.isDealingDamage():
continue
@@ -192,6 +194,21 @@ def getLauncherMult(mod, distance, tgtSpeed, tgtSigRadius):
return distanceFactor * applicationFactor
def getBreacherMult(mod, distance):
missileMaxRangeData = mod.missileMaxRangeData
if missileMaxRangeData is None:
return 0
# The ranges already consider ship radius
lowerRange, higherRange, higherChance = missileMaxRangeData
if distance is None or distance <= lowerRange:
distanceFactor = 1
elif lowerRange < distance <= higherRange:
distanceFactor = higherChance
else:
distanceFactor = 0
return distanceFactor
def getSmartbombMult(mod, distance):
modRange = mod.maxRange
if modRange is None:

View File

@@ -19,6 +19,7 @@
import eos.config
from eos.saveddata.targetProfile import TargetProfile
from eos.utils.spoolSupport import SpoolOptions, SpoolType
from eos.utils.stats import DmgTypes
from graphs.data.base import PointGetter, SmoothPointGetter
@@ -27,17 +28,16 @@ from .calc.application import getApplicationPerKey
from .calc.projected import getScramRange, getScrammables, getTackledSpeed, getSigRadiusMult
def applyDamage(dmgMap, applicationMap, tgtResists):
total = DmgTypes(em=0, thermal=0, kinetic=0, explosive=0)
def applyDamage(dmgMap, applicationMap, tgtResists, tgtFullHp):
total = DmgTypes.default()
for key, dmg in dmgMap.items():
total += dmg * applicationMap.get(key, 0)
if not GraphSettings.getInstance().get('ignoreResists'):
emRes, thermRes, kinRes, exploRes = tgtResists
total = DmgTypes(
em=total.em * (1 - emRes),
thermal=total.thermal * (1 - thermRes),
kinetic=total.kinetic * (1 - kinRes),
explosive=total.explosive * (1 - exploRes))
else:
emRes = thermRes = kinRes = exploRes = 0
total.profile = TargetProfile(
emAmount=emRes, thermalAmount=thermRes, kineticAmount=kinRes, explosiveAmount=exploRes, hp=tgtFullHp)
return total
@@ -144,7 +144,8 @@ class XDistanceMixin(SmoothPointGetter):
'srcScramRange': getScramRange(src=src) if applyProjected else None,
'tgtScrammables': getScrammables(tgt=tgt) if applyProjected else (),
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
'tgtResists': tgt.getResists()}
'tgtResists': tgt.getResists(),
'tgtFullHp': tgt.getFullHp()}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
@@ -186,7 +187,8 @@ class XDistanceMixin(SmoothPointGetter):
y = applyDamage(
dmgMap=commonData['dmgMap'],
applicationMap=applicationMap,
tgtResists=commonData['tgtResists']).total
tgtResists=commonData['tgtResists'],
tgtFullHp=commonData['tgtFullHp']).total
return y
@@ -241,14 +243,17 @@ class XTimeMixin(PointGetter):
self._prepareTimeCache(src=src, maxTime=maxTime)
timeCache = self._getTimeCacheData(src=src)
applicationMap = self._prepareApplicationMap(miscParams=miscParams, src=src, tgt=tgt)
tgtResists = tgt.getResists()
# Custom iteration for time graph to show all data points
currentDmg = None
currentTime = None
for currentTime in sorted(timeCache):
prevDmg = currentDmg
currentDmgData = timeCache[currentTime]
currentDmg = applyDamage(dmgMap=currentDmgData, applicationMap=applicationMap, tgtResists=tgtResists).total
currentDmg = applyDamage(
dmgMap=currentDmgData,
applicationMap=applicationMap,
tgtResists=tgt.getResists(),
tgtFullHp=tgt.getFullHp()).total
if currentTime < minTime:
continue
# First set of data points
@@ -294,7 +299,11 @@ class XTimeMixin(PointGetter):
self._prepareTimeCache(src=src, maxTime=time)
dmgData = self._getTimeCacheDataPoint(src=src, time=time)
applicationMap = self._prepareApplicationMap(miscParams=miscParams, src=src, tgt=tgt)
y = applyDamage(dmgMap=dmgData, applicationMap=applicationMap, tgtResists=tgt.getResists()).total
y = applyDamage(
dmgMap=dmgData,
applicationMap=applicationMap,
tgtResists=tgt.getResists(),
tgtFullHp=tgt.getFullHp()).total
return y
@@ -310,7 +319,8 @@ class XTgtSpeedMixin(SmoothPointGetter):
return {
'applyProjected': GraphSettings.getInstance().get('applyProjected'),
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
'tgtResists': tgt.getResists()}
'tgtResists': tgt.getResists(),
'tgtFullHp': tgt.getFullHp()}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
tgtSpeed = x
@@ -353,7 +363,8 @@ class XTgtSpeedMixin(SmoothPointGetter):
y = applyDamage(
dmgMap=commonData['dmgMap'],
applicationMap=applicationMap,
tgtResists=commonData['tgtResists']).total
tgtResists=commonData['tgtResists'],
tgtFullHp=commonData['tgtFullHp']).total
return y
@@ -398,7 +409,8 @@ class XTgtSigRadiusMixin(SmoothPointGetter):
'tgtSpeed': tgtSpeed,
'tgtSigMult': tgtSigMult,
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
'tgtResists': tgt.getResists()}
'tgtResists': tgt.getResists(),
'tgtFullHp': tgt.getFullHp()}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
tgtSigRadius = x
@@ -414,7 +426,8 @@ class XTgtSigRadiusMixin(SmoothPointGetter):
y = applyDamage(
dmgMap=commonData['dmgMap'],
applicationMap=applicationMap,
tgtResists=commonData['tgtResists']).total
tgtResists=commonData['tgtResists'],
tgtFullHp=commonData['tgtFullHp']).total
return y

View File

@@ -89,7 +89,7 @@ class FitDamageStatsGraph(FitGraph):
cols = []
if not GraphSettings.getInstance().get('ignoreResists'):
cols.append('Target Resists')
cols.extend(('Speed', 'SigRadius', 'Radius'))
cols.extend(('Speed', 'SigRadius', 'Radius', 'FullHP'))
return cols
# Calculation stuff

View File

@@ -145,6 +145,11 @@ class TargetWrapper(BaseWrapper):
else:
return em, therm, kin, explo
def getFullHp(self):
if self.isProfile:
return self.item.hp
if self.isFit:
return self.item.hp.get('shield', 0) + self.item.hp.get('armor', 0) + self.item.hp.get('hull', 0)
def _getShieldResists(ship):

View File

@@ -43,6 +43,10 @@ class BoosterViewDrop(wx.DropTarget):
if self.GetData():
dragged_data = DragDropHelper.data
data = dragged_data.split(':')
if dragged_data is None:
return t
self.dropFn(x, y, data)
return t

View File

@@ -41,6 +41,10 @@ class CargoViewDrop(wx.DropTarget):
def OnData(self, x, y, t):
if self.GetData():
dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':')
self.dropFn(x, y, data)
return t

View File

@@ -56,6 +56,10 @@ class CommandViewDrop(wx.DropTarget):
def OnData(self, x, y, t):
if self.GetData():
dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':')
self.dropFn(x, y, data)
return t

View File

@@ -52,6 +52,10 @@ class DroneViewDrop(wx.DropTarget):
def OnData(self, x, y, t):
if self.GetData():
dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':')
self.dropFn(x, y, data)
return t
@@ -195,7 +199,11 @@ class DroneView(Display):
@staticmethod
def droneKey(drone):
groupName = Market.getInstance().getMarketGroupByItem(drone.item).marketGroupName
if drone.isMutated:
item = drone.baseItem
else:
item = drone.item
groupName = Market.getInstance().getMarketGroupByItem(item).marketGroupName
return (DRONE_ORDER.index(groupName), drone.isMutated, drone.fullName)
def fitChanged(self, event):

View File

@@ -34,7 +34,10 @@ from service.fit import Fit
from service.market import Market
FIGHTER_ORDER = ('Light Fighter', 'Heavy Fighter', 'Support Fighter')
FIGHTER_ORDER = (
'Light Fighter', 'Structure Light Fighter',
'Heavy Fighter', 'Structure Heavy Fighter',
'Support Fighter', 'Structure Support Fighter')
_t = wx.GetTranslation
@@ -49,6 +52,10 @@ class FighterViewDrop(wx.DropTarget):
def OnData(self, x, y, t):
if self.GetData():
dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':')
self.dropFn(x, y, data)
return t

View File

@@ -46,6 +46,10 @@ class ImplantViewDrop(wx.DropTarget):
def OnData(self, x, y, t):
if self.GetData():
dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':')
self.dropFn(x, y, data)
return t

View File

@@ -65,6 +65,10 @@ class ProjectedViewDrop(wx.DropTarget):
def OnData(self, x, y, t):
if self.GetData():
dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':')
self.dropFn(x, y, data)
return t

View File

@@ -20,6 +20,7 @@ from gui.builtinContextMenus.targetProfile import editor
from gui.builtinContextMenus import itemStats
from gui.builtinContextMenus import itemMarketJump
from gui.builtinContextMenus import fitSystemSecurity # Not really an item info but want to keep it here
from gui.builtinContextMenus import fitPilotSecurity # Not really an item info but want to keep it here
from gui.builtinContextMenus import shipJump
# Generic item manipulations
from gui.builtinContextMenus import itemRemove

View File

@@ -0,0 +1,157 @@
import re
import wx
import gui.fitCommands as cmd
import gui.mainFrame
from gui.contextMenu import ContextMenuUnconditional
from service.fit import Fit
_t = wx.GetTranslation
class FitPilotSecurityMenu(ContextMenuUnconditional):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext):
if srcContext != "fittingShip":
return False
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
if fit.ship.name not in ('Pacifier', 'Enforcer', 'Marshal', 'Sidewinder', 'Cobra', 'Python'):
return
return True
def getText(self, callingWindow, itmContext):
return _t("Pilot Security Status")
def addOption(self, menu, optionLabel, optionValue):
id = ContextMenuUnconditional.nextID()
self.optionIds[id] = optionValue
menuItem = wx.MenuItem(menu, id, optionLabel, kind=wx.ITEM_CHECK)
menu.Bind(wx.EVT_MENU, self.handleMode, menuItem)
return menuItem
def addOptionCustom(self, menu, optionLabel):
id = ContextMenuUnconditional.nextID()
menuItem = wx.MenuItem(menu, id, optionLabel, kind=wx.ITEM_CHECK)
menu.Bind(wx.EVT_MENU, self.handleModeCustom, menuItem)
return menuItem
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
msw = True if "wxMSW" in wx.PlatformInfo else False
self.optionIds = {}
sub = wx.Menu()
presets = (-10, -8, -6, -4, -2, 0, 1, 2, 3, 4, 5)
# Inherit
char_sec_status = round(fit.character.secStatus, 2)
menuItem = self.addOption(rootMenu if msw else sub, _t('Character') + f' ({char_sec_status})', None)
sub.Append(menuItem)
menuItem.Check(fit.pilotSecurity is None)
# Custom
label = _t('Custom')
is_checked = False
if fit.pilotSecurity is not None and fit.pilotSecurity not in presets:
sec_status = round(fit.getPilotSecurity(), 2)
label += f' ({sec_status})'
is_checked = True
menuItem = self.addOptionCustom(rootMenu if msw else sub, label)
sub.Append(menuItem)
menuItem.Check(is_checked)
sub.AppendSeparator()
# Predefined options
for sec_status in presets:
menuItem = self.addOption(rootMenu if msw else sub, str(sec_status), sec_status)
sub.Append(menuItem)
menuItem.Check(fit.pilotSecurity == sec_status)
return sub
def handleMode(self, event):
optionValue = self.optionIds[event.Id]
self.mainFrame.command.Submit(cmd.GuiChangeFitPilotSecurityCommand(
fitID=self.mainFrame.getActiveFit(),
secStatus=optionValue))
def handleModeCustom(self, event):
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
sec_status = fit.getPilotSecurity()
with SecStatusChanger(self.mainFrame, value=sec_status) as dlg:
if dlg.ShowModal() == wx.ID_OK:
cleanInput = re.sub(r'[^0-9.\-+]', '', dlg.input.GetLineText(0).strip())
if cleanInput:
try:
cleanInputFloat = float(cleanInput)
except ValueError:
return
else:
return
self.mainFrame.command.Submit(cmd.GuiChangeFitPilotSecurityCommand(
fitID=fitID, secStatus=max(-10.0, min(5.0, cleanInputFloat))))
FitPilotSecurityMenu.register()
class SecStatusChanger(wx.Dialog):
def __init__(self, parent, value):
super().__init__(parent, title=_t('Change Security Status'), style=wx.DEFAULT_DIALOG_STYLE)
self.SetMinSize((346, 156))
bSizer1 = wx.BoxSizer(wx.VERTICAL)
bSizer2 = wx.BoxSizer(wx.VERTICAL)
text = wx.StaticText(self, wx.ID_ANY, _t('Security Status (min -10.0, max 5.0):'))
bSizer2.Add(text, 0)
bSizer1.Add(bSizer2, 0, wx.ALL, 10)
self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER)
if value is None:
value = '0.0'
else:
if value == int(value):
value = int(value)
value = str(value)
self.input.SetValue(value)
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
bSizer3 = wx.BoxSizer(wx.VERTICAL)
bSizer3.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.BOTTOM | wx.EXPAND, 15)
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
self.input.Bind(wx.EVT_CHAR, self.onChar)
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
self.SetSizer(bSizer1)
self.Fit()
self.CenterOnParent()
self.input.SetFocus()
self.input.SelectAll()
def processEnter(self, evt):
self.EndModal(wx.ID_OK)
# checks to make sure it's valid number
@staticmethod
def onChar(event):
key = event.GetKeyCode()
acceptable_characters = '1234567890.-+'
acceptable_keycode = [3, 22, 13, 8, 127] # modifiers like delete, copy, paste
if key in acceptable_keycode or key >= 255 or (key < 255 and chr(key) in acceptable_characters):
event.Skip()
return
else:
return False

View File

@@ -13,6 +13,16 @@ from service.fit import Fit
_t = wx.GetTranslation
GLORIFIED_PREFIX = 'Gl. '
def nameSorter(mutaplasmid):
name = mutaplasmid.shortName
if name.startswith(GLORIFIED_PREFIX):
return name[len(GLORIFIED_PREFIX):], True
return name, False
class ChangeItemMutation(ContextMenuSingle):
def __init__(self):
@@ -45,7 +55,7 @@ class ChangeItemMutation(ContextMenuSingle):
menu = rootMenu if msw else sub
for mutaplasmid in mainItem.item.mutaplasmids:
for mutaplasmid in sorted(mainItem.item.mutaplasmids, key=nameSorter):
id = ContextMenuSingle.nextID()
self.eventIDs[id] = (mutaplasmid, mainItem)
mItem = wx.MenuItem(menu, id, mutaplasmid.shortName)

View File

@@ -173,7 +173,7 @@ class FirepowerViewFull(StatsView):
if hasSpool:
lines.append("")
lines.append(_t("Current") + ": {}".format(formatAmount(normal.total, prec, lowest, highest)))
for dmgType in normal.names():
for dmgType in normal.names(includePure=True):
val = getattr(normal, dmgType, None)
if val:
lines.append("{}{}: {}%".format(
@@ -215,13 +215,13 @@ class FirepowerViewFull(StatsView):
val = val() if fit is not None else None
preSpoolVal = preSpoolVal() if fit is not None else None
fullSpoolVal = fullSpoolVal() if fit is not None else None
if self._cachedValues[counter] != val:
if self._cachedValues[counter] != getattr(val, 'total', None):
tooltipText = dpsToolTip(val, preSpoolVal, fullSpoolVal, prec, lowest, highest)
label.SetLabel(valueFormat.format(
formatAmount(0 if val is None else val.total, prec, lowest, highest),
"\u02e2" if hasSpoolUp(preSpoolVal, fullSpoolVal) else ""))
label.SetToolTip(wx.ToolTip(tooltipText))
self._cachedValues[counter] = val
self._cachedValues[counter] = getattr(val, 'total', None)
counter += 1
self.panel.Layout()

View File

@@ -197,6 +197,30 @@ class SignatureRadiusColumn(GraphColumn):
SignatureRadiusColumn.register()
class FullHpColumn(GraphColumn):
name = 'FullHP'
stickPrefixToValue = True
def __init__(self, fittingView, params):
super().__init__(fittingView, 68)
def _getValue(self, stuff):
if isinstance(stuff, Fit):
full_hp = stuff.hp.get('shield', 0) + stuff.hp.get('armor', 0) + stuff.hp.get('hull', 0)
elif isinstance(stuff, TargetProfile):
full_hp = stuff.hp
else:
full_hp = 0
return full_hp, 'hp'
def _getFitTooltip(self):
return 'Total raw HP'
FullHpColumn.register()
class ShieldAmountColumn(GraphColumn):
name = 'ShieldAmount'

View File

@@ -93,8 +93,6 @@ class Miscellanea(ViewColumn):
text = "{} dmg".format(formatAmount(dmg, 3, 0, 6))
tooltip = "Raw damage done"
return text, tooltip
pass
elif itemGroup in ("Energy Weapon", "Hybrid Weapon", "Projectile Weapon", "Combat Drone", "Fighter Drone"):
trackingSpeed = stuff.getModifiedItemAttr("trackingSpeed")
optimalSig = stuff.getModifiedItemAttr("optimalSigRadius")
@@ -810,6 +808,19 @@ class Miscellanea(ViewColumn):
text = "{}".format(formatAmount(scanStr, 4, 0, 3))
tooltip = "Scan strength at {} AU scan range".format(formatAmount(baseRange, 3, 0, 0))
return text, tooltip
elif chargeGroup in ("SCARAB Breacher Pods",):
duration = stuff.getModifiedChargeAttr("dotDuration") / 1000
dmgAbs = stuff.getModifiedChargeAttr("dotMaxDamagePerTick")
dmgRel = stuff.getModifiedChargeAttr("dotMaxHPPercentagePerTick")
text = "{}/{}% over {}s".format(
formatAmount(dmgAbs * duration, 3, 0, 6),
formatAmount(dmgRel * duration, 3, 0, 6),
formatAmount(duration, 0, 0, 0))
fullDmgHp = dmgAbs / (dmgRel / 100)
tooltip = (
'Pure damage inflicted over time, minimum of absolute / relative\n'
'Full DPS from {} target HP').format(formatAmount(fullDmgHp, 3, 0, 6))
return text, tooltip
else:
return "", None
else:

View File

@@ -127,6 +127,10 @@ class FittingViewDrop(wx.DropTarget):
if self.GetData():
dragged_data = DragDropHelper.data
# pyfalog.debug("fittingView: recieved drag: " + self.dropData.GetText())
if dragged_data is None:
return t
data = dragged_data.split(':')
self.dropFn(x, y, data)
return t

View File

@@ -918,7 +918,7 @@ class SecStatusDialog(wx.Dialog):
self.m_staticText1.Wrap(-1)
bSizer1.Add(self.m_staticText1, 1, wx.ALL | wx.EXPAND, 5)
self.floatSpin = FloatSpin(self, value=sec, min_val=-5.0, max_val=5.0, increment=0.1, digits=2, size=(-1, -1))
self.floatSpin = FloatSpin(self, value=sec, min_val=-10.0, max_val=5.0, increment=0.1, digits=2, size=(-1, -1))
bSizer1.Add(self.floatSpin, 0, wx.ALIGN_CENTER | wx.ALL, 5)
btnOk = wx.Button(self, wx.ID_OK)

View File

@@ -12,6 +12,7 @@ from .gui.cargo.remove import GuiRemoveCargosCommand
from .gui.commandFit.add import GuiAddCommandFitsCommand
from .gui.commandFit.remove import GuiRemoveCommandFitsCommand
from .gui.commandFit.toggleStates import GuiToggleCommandFitStatesCommand
from .gui.fitPilotSecurity import GuiChangeFitPilotSecurityCommand
from .gui.fitRename import GuiRenameFitCommand
from .gui.fitRestrictionToggle import GuiToggleFittingRestrictionsCommand
from .gui.fitSystemSecurity import GuiChangeFitSystemSecurityCommand

View File

@@ -0,0 +1,32 @@
import wx
from logbook import Logger
from service.fit import Fit
pyfalog = Logger(__name__)
class CalcChangeFitPilotSecurityCommand(wx.Command):
def __init__(self, fitID, secStatus):
wx.Command.__init__(self, True, 'Change Fit Pilot Security')
self.fitID = fitID
self.secStatus = secStatus
self.savedSecStatus = None
def Do(self):
pyfalog.debug('Doing changing pilot security status of fit {} to {}'.format(self.fitID, self.secStatus))
fit = Fit.getInstance().getFit(self.fitID, basic=True)
# Fetching status via getter and then saving 'raw' security status
# is intentional, to restore pre-change state properly
if fit.pilotSecurity == self.secStatus:
return False
self.savedSecStatus = fit.pilotSecurity
fit.pilotSecurity = self.secStatus
return True
def Undo(self):
pyfalog.debug('Undoing changing pilot security status of fit {} to {}'.format(self.fitID, self.secStatus))
cmd = CalcChangeFitPilotSecurityCommand(fitID=self.fitID, secStatus=self.savedSecStatus)
return cmd.Do()

View File

@@ -0,0 +1,36 @@
import wx
from service.fit import Fit
import eos.db
import gui.mainFrame
from gui import globalEvents as GE
from gui.fitCommands.helpers import InternalCommandHistory
from gui.fitCommands.calc.fitPilotSecurity import CalcChangeFitPilotSecurityCommand
class GuiChangeFitPilotSecurityCommand(wx.Command):
def __init__(self, fitID, secStatus):
wx.Command.__init__(self, True, 'Change Fit Pilot Security')
self.internalHistory = InternalCommandHistory()
self.fitID = fitID
self.secStatus = secStatus
def Do(self):
cmd = CalcChangeFitPilotSecurityCommand(fitID=self.fitID, secStatus=self.secStatus)
success = self.internalHistory.submit(cmd)
eos.db.flush()
sFit = Fit.getInstance()
sFit.recalc(self.fitID)
eos.db.commit()
wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,)))
return success
def Undo(self):
success = self.internalHistory.undoAll()
eos.db.flush()
sFit = Fit.getInstance()
sFit.recalc(self.fitID)
eos.db.commit()
wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,)))
return success

View File

@@ -191,7 +191,7 @@ class ShipBrowser(wx.Panel):
"amarr", "caldari", "gallente", "minmatar",
"sisters", "ore", "concord",
"serpentis", "angel", "blood", "sansha", "guristas", "mordu",
"jove", "triglavian", "upwell", None
"deathless", "jove", "triglavian", "upwell", None
]
def raceNameKey(self, ship):

View File

@@ -123,13 +123,14 @@ class TargetProfileEditor(AuxiliaryFrame):
ATTRIBUTES = OrderedDict([
('maxVelocity', (_t('Maximum speed'), 'm/s')),
('signatureRadius', (_t('Signature radius\nLeave blank for infinitely big value'), 'm')),
('radius', (_t('Radius'), 'm'))])
('radius', (_t('Radius\nThe radius of the sphere that represents a ship/drone in space. Affects range calculations.'), 'm')),
('hp', (_t('Total HP\nAffects how much damage breacher pods can do. Leave blank for infinitely big value'), 'hp'))])
def __init__(self, parent):
super().__init__(
parent, id=wx.ID_ANY, title=_t("Target Profile Editor"), resizeable=True,
# Dropdown list widget is scaled to its longest content line on GTK, adapt to that
size=wx.Size(500, 240) if "wxGTK" in wx.PlatformInfo else wx.Size(350, 240))
size=wx.Size(630, 240) if "wxGTK" in wx.PlatformInfo else wx.Size(450, 240))
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.block = False
@@ -145,36 +146,29 @@ class TargetProfileEditor(AuxiliaryFrame):
contentSizer = wx.BoxSizer(wx.VERTICAL)
resistEditSizer = wx.FlexGridSizer(2, 6, 0, 2)
resistEditSizer.AddGrowableCol(0)
resistEditSizer.AddGrowableCol(5)
resistEditSizer.SetFlexibleDirection(wx.BOTH)
resistEditSizer.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED)
resistEditSizer = wx.BoxSizer(wx.HORIZONTAL)
resistEditSizer.AddStretchSpacer()
defSize = wx.Size(50, -1)
defSize = wx.Size(70, -1)
for i, type_ in enumerate(self.DAMAGE_TYPES):
if i % 2:
style = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.LEFT
border = 25
else:
style = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT
border = 5
for type_ in self.DAMAGE_TYPES:
leftPad = 25 if type_ != list(self.DAMAGE_TYPES)[0] else 0
ttText = self.DAMAGE_TYPES[type_]
bmp = wx.StaticBitmap(self, wx.ID_ANY, BitmapLoader.getBitmap("%s_big" % type_, "gui"))
bmp.SetToolTip(wx.ToolTip(ttText))
resistEditSizer.Add(bmp, 0, style, border)
resistEditSizer.Add(bmp, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, leftPad)
# set text edit
editBox = FloatBox(parent=self, id=wx.ID_ANY, value=None, pos=wx.DefaultPosition, size=defSize, validator=ResistValidator())
editBox = FloatBox(parent=self, id=wx.ID_ANY, value=None, pos=wx.DefaultPosition, size=defSize)
editBox.SetToolTip(wx.ToolTip(ttText))
self.Bind(event=wx.EVT_TEXT, handler=self.OnFieldChanged, source=editBox)
setattr(self, '{}Edit'.format(type_), editBox)
resistEditSizer.Add(editBox, 0, wx.BOTTOM | wx.TOP | wx.ALIGN_CENTER_VERTICAL, 5)
resistEditSizer.Add(editBox, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
unit = wx.StaticText(self, wx.ID_ANY, "%", wx.DefaultPosition, wx.DefaultSize, 0)
unit.SetToolTip(wx.ToolTip(ttText))
resistEditSizer.Add(unit, 0, wx.BOTTOM | wx.TOP | wx.ALIGN_CENTER_VERTICAL, 5)
contentSizer.Add(resistEditSizer, 0, wx.EXPAND | wx.ALL, 5)
resistEditSizer.AddStretchSpacer()
contentSizer.Add(resistEditSizer, 1, wx.EXPAND | wx.ALL, 5)
miscAttrSizer = wx.BoxSizer(wx.HORIZONTAL)
miscAttrSizer.AddStretchSpacer()

BIN
imgs/gui/hp_big.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 B

After

Width:  |  Height:  |  Size: 645 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 604 B

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 B

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 634 B

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

BIN
imgs/icons/25240@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

BIN
imgs/icons/25240@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25241@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 B

BIN
imgs/icons/25241@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25242@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

BIN
imgs/icons/25242@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25243@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

BIN
imgs/icons/25243@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25250@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

BIN
imgs/icons/25250@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
imgs/icons/25251@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

BIN
imgs/icons/25251@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25252@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 823 B

BIN
imgs/icons/25252@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25253@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

BIN
imgs/icons/25253@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 697 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 818 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 768 B

Some files were not shown because too many files have changed in this diff Show More