Compare commits

...

38 Commits

Author SHA1 Message Date
DarkPhoenix
87fd358c4e Bump version 2024-12-05 15:08:54 +01:00
Anton Vorobyov
0f4b082b4b Merge pull request #2629 from cryonox/dev/issue2121
Fix wxWidget crash caused by GDI object limit
2024-12-05 13:20:53 +01:00
DarkPhoenix
59847c3756 Update for today's patch 2024-12-05 13:16:56 +01:00
DarkPhoenix
b5d3dc6d56 Bump version 2024-11-28 17:28:21 +01:00
DarkPhoenix
3500867336 Revert wxpython version bump 2024-11-27 23:41:29 +01:00
Anton Vorobyov
ea26d57566 Merge pull request #2635 from wereii/fix-syntax-warnings
Fix invalid escape syntax warnings
2024-11-27 23:38:15 +01:00
Anton Vorobyov
45f0743c57 Merge branch 'master' into fix-syntax-warnings 2024-11-27 23:37:50 +01:00
DarkPhoenix
08d4e852d3 Fix comparison issue which broke graphs in some cases 2024-11-27 23:25:49 +01:00
Anton Vorobyov
7edf1f93cb Merge pull request #2631 from ugurilter/master
fix arbitrary content being dragged and dropped causing errors
2024-11-26 19:59:08 +01:00
DarkPhoenix
31cfa7c93a Bump version 2024-11-26 19:17:50 +01:00
DarkPhoenix
da1d42b578 Fix drone sorter for EFT export 2024-11-26 19:16:44 +01:00
DarkPhoenix
502e5a8d8e Update static data 2024-11-26 19:03:00 +01:00
DarkPhoenix
087867bdca Update static data and icons 2024-11-14 14:12:20 +01:00
DarkPhoenix
86ad17b075 Expand breacher tooltip 2024-11-13 15:37:40 +01:00
DarkPhoenix
b8ad4772b8 Bump version 2024-11-13 12:32:24 +01:00
DarkPhoenix
e0e5be5870 Change tooltip 2024-11-13 12:32:07 +01:00
DarkPhoenix
8b2f7c56db Update effects 2024-11-13 12:30:51 +01:00
DarkPhoenix
0ca1a675c2 Update static data 2024-11-13 12:30:28 +01:00
DarkPhoenix
6a0bfce262 Expand radius tooltip 2024-11-13 11:02:02 +01:00
DarkPhoenix
362aebe658 Increase size of inputs 2024-11-13 10:35:09 +01:00
DarkPhoenix
bbe3e931f7 Bump wxpython version 2024-11-13 05:58:11 +01:00
DarkPhoenix
f5743af6a4 Cache calculated values 2024-11-13 05:57:05 +01:00
DarkPhoenix
dbc2993fb9 Support inflicted damage for breachers 2024-11-13 05:35:41 +01:00
DarkPhoenix
219173c43e A few fixes for breachers on graphs 2024-11-13 05:13:37 +01:00
DarkPhoenix
6074cfe15a Rework damage stats calculation to always expose full breacher info 2024-11-13 04:37:15 +01:00
DarkPhoenix
f0a72f4307 Add full HP parameter to graphs (for now, unused) 2024-11-12 17:14:52 +01:00
DarkPhoenix
ecc3f9fa7e Add base support for breacher pods to graphs 2024-11-12 16:58:18 +01:00
DarkPhoenix
c660e4058c Fix graphs 2024-11-12 14:49:52 +01:00
DarkPhoenix
7664b00b59 Minor fixes 2024-11-12 14:45:41 +01:00
DarkPhoenix
5721beacf5 Expose breacher DPS and volley to stats 2024-11-12 14:20:20 +01:00
DarkPhoenix
13f3793515 Implement breacher logic on module level 2024-11-11 21:17:32 +01:00
DarkPhoenix
f8e0520344 Add HP value to target profile editor 2024-11-11 15:07:40 +01:00
DarkPhoenix
2c8e306dc2 Add HP field to target profile 2024-11-11 14:34:38 +01:00
DarkPhoenix
7c37d8fd74 Add a few bits to breacher volley calculation 2024-11-11 14:12:06 +01:00
DarkPhoenix
88129c0df4 Do not choke on mutated drones in drone panel, since they lost market group 2024-11-10 12:10:10 +01:00
Ugur Ilter
3892dfc78c fix arbitrary content being dragged and dropped causing errors
Fixes #2630
2024-09-26 15:52:26 +03:00
wereii
954a164922 Fix invalid escape syntax warnings
Additionally use rawstrings with all regex calls
2024-09-11 17:25:38 +02:00
cryonox
32fd38ad7c Fix wxWidget crash caused by GDI id limit 2024-08-15 17:50:41 +08:00
154 changed files with 22279 additions and 2672 deletions

View File

@@ -141,14 +141,14 @@ def update_db():
(row['typeName_en-us'].startswith('Civilian') and "Shuttle" not in row['typeName_en-us'])
or row['typeName_en-us'] == 'Capsule'
or row['groupID'] == 4033 # destructible effect beacons
or re.match('AIR .+Booster.*', row['typeName_en-us'])
or re.match(r'AIR .+Booster.*', row['typeName_en-us'])
):
row['published'] = True
# Nearly useless and clutter search results too much
elif (
row['typeName_en-us'].startswith('Limited Synth ')
or row['typeName_en-us'].startswith('Expired ')
or re.match('Mining Blitz .+ Booster Dose .+', row['typeName_en-us'])
or re.match(r'Mining Blitz .+ Booster Dose .+', row['typeName_en-us'])
or row['typeName_en-us'].endswith(' Filament') and (
"'Needlejack'" not in row['typeName_en-us'] and
"'Devana'" not in row['typeName_en-us'] and

View File

@@ -14,7 +14,7 @@ with open("version.yml", 'r') as file:
os.environ["PYFA_DIST_DIR"] = os.path.join(os.getcwd(), 'dist')
os.environ["PYFA_VERSION"] = version
iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
iscc = r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
source = os.path.join(os.environ["PYFA_DIST_DIR"], "pyfa")

View File

@@ -21,7 +21,7 @@ for modName in iterNamespace(__name__, __path__):
# loop through python files, extracting update number and function, and
# adding it to a list
modname_tail = modName.rsplit('.', 1)[-1]
m = re.match("^upgrade(?P<index>\d+)$", modname_tail)
m = re.match(r"^upgrade(?P<index>\d+)$", modname_tail)
if not m:
continue
index = int(m.group("index"))

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

@@ -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})

View File

@@ -1306,6 +1306,7 @@ class Effect446(BaseEffect):
Implants named like: Festival only 'Rock' SH Dose (4 of 4)
Implants named like: Halcyon G Booster (5 of 5)
Implants named like: Serenity Limited 'Hardshell' Dose (3 of 3)
Implants named like: Wightstorm Nirvana Booster (4 of 4)
Implants named like: Zainou 'Gnome' Shield Management SM (6 of 6)
Modules named like: Core Defense Field Extender (8 of 8)
Implant: AIR Nirvana Booster II
@@ -1330,6 +1331,7 @@ class Effect485(BaseEffect):
Implants named like: Halcyon G Booster (5 of 5)
Implants named like: Halcyon R Booster (5 of 5)
Implants named like: Inherent Implants 'Squire' Capacitor Systems Operation EO (6 of 6)
Implants named like: Wightstorm Rapture Booster (4 of 4)
Implants named like: grade Rapture (15 of 18)
Modules named like: Capacitor Control Circuit (8 of 8)
Implant: AIR Overclocker Booster III
@@ -1775,7 +1777,6 @@ class Effect584(BaseEffect):
Implants named like: Eifyr and Co. 'Gunslinger' Surgical Strike SS (6 of 6)
Implants named like: Halcyon Y Booster (5 of 5)
Implant: AIR Pyrolancea Booster II
Implant: Standard Cerebral Accelerator
"""
type = 'passive'
@@ -5253,7 +5254,6 @@ class Effect1763(BaseEffect):
Used by:
Implants named like: Halcyon R Booster (5 of 5)
Implants named like: Zainou 'Deadeye' Rapid Launch RL (6 of 6)
Implant: Standard Cerebral Accelerator
Implant: Whelan Machorin's Ballistic Smartlink
Skill: Missile Launcher Operation
Skill: Rapid Launch
@@ -8363,6 +8363,7 @@ class Effect2803(BaseEffect):
Used by:
Implants named like: Harvest Damage Booster (4 of 4)
Implants named like: Wightstorm Vitarka Booster (4 of 4)
Modules named like: Energy Collision Accelerator (8 of 8)
Implant: Wisdom of Gheinok
"""
@@ -9640,6 +9641,7 @@ class Effect3196(BaseEffect):
thermodynamicsSkillDamageBonus
Used by:
Implants named like: Wightstorm Sunyata Booster (4 of 4)
Skill: Thermodynamics
"""
@@ -16293,6 +16295,7 @@ class Effect4902(BaseEffect):
Used by:
Ships from group: Assault Frigate (10 of 15)
Ships from group: Command Destroyer (5 of 5)
Ships from group: Heavy Assault Cruiser (9 of 14)
"""
type = 'passive'
@@ -18009,6 +18012,7 @@ class Effect5189(BaseEffect):
Used by:
Implants named like: Tetrimon Precision Booster (4 of 4)
Implants named like: Wightstorm Manasikara Booster (4 of 4)
Modules named like: Energy Metastasis Adjuster (8 of 8)
"""
@@ -37793,6 +37797,23 @@ class Effect8279(BaseEffect):
skill='Industrial Command Ships', **kwargs)
class Effect8291(BaseEffect):
"""
afterburnerSpeedBoostBonusPassive
Used by:
Implants named like: Wightstorm Cetana Booster (4 of 4)
"""
type = 'passive'
@staticmethod
def handler(fit, booster, context, projectionRange, **kwargs):
fit.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill('Afterburner'), 'speedFactor',
booster.getModifiedItemAttr('speedFBonus'), **kwargs)
class Effect8294(BaseEffect):
"""
industrialCommandBonusDroneOreMiningYield
@@ -41151,7 +41172,7 @@ class Effect12195(BaseEffect):
def handler(fit, src, context, projectionRange, **kwargs):
fit.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill('Shield Operation'), 'shieldBonus',
src.getModifiedItemAttr('roleBonusCBC'), **kwargs)
src.getModifiedItemAttr('shipBonusCBC1'), skill='Caldari Battlecruiser', **kwargs)
class Effect12202(BaseEffect):

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

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

@@ -273,7 +273,7 @@ class GraphCanvasPanel(wx.Panel):
legendLines = []
for i, iData in enumerate(legendData):
color, lineStyle, label = iData
legendLines.append(Line2D([0], [0], color=color, linestyle=lineStyle, label=label.replace('$', '\$')))
legendLines.append(Line2D([0], [0], color=color, linestyle=lineStyle, label=label.replace('$', r'\$')))
if len(legendLines) > 0 and self.graphFrame.ctrlPanel.showLegend:
legend = self.subplot.legend(handles=legendLines)

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

@@ -52,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

@@ -22,9 +22,9 @@ class ItemDescription(wx.Panel):
desc = item.description.replace("\n", "<br>")
# Strip font tags
desc = re.sub("<( *)font( *)color( *)=(.*?)>(?P<inside>.*?)<( *)/( *)font( *)>", "\g<inside>", desc)
desc = re.sub("<( *)font( *)color( *)=(.*?)>(?P<inside>.*?)<( *)/( *)font( *)>", r"\g<inside>", desc)
# Strip URLs
desc = re.sub("<( *)a(.*?)>(?P<inside>.*?)<( *)/( *)a( *)>", "\g<inside>", desc)
desc = re.sub("<( *)a(.*?)>(?P<inside>.*?)<( *)/( *)a( *)>", r"\g<inside>", desc)
desc = "<body bgcolor='{}' text='{}'>{}</body>".format(
bgcolor.GetAsString(wx.C2S_HTML_SYNTAX),
fgcolor.GetAsString(wx.C2S_HTML_SYNTAX),

View File

@@ -163,6 +163,10 @@ class PFListPane(wx.ScrolledWindow):
def RemoveAllChildren(self):
for widget in self._wList:
widget.Destroy()
# this forces the garbage collector to work properly by removing dangling references to objects which are still alive, otherwise widget cannot be gc-ed eventually causing GDI id exhaustion and crash
for i in widget.__dict__.keys():
widget.__dict__[i] =None
del widget
self.Scroll(0, 0)
self._wList = []

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

@@ -810,13 +810,16 @@ class Miscellanea(ViewColumn):
return text, tooltip
elif chargeGroup in ("SCARAB Breacher Pods",):
duration = stuff.getModifiedChargeAttr("dotDuration") / 1000
dmgAbs = stuff.getModifiedChargeAttr("dotMaxDamagePerTick") * duration
dmgRel = stuff.getModifiedChargeAttr("dotMaxHPPercentagePerTick") * duration
dmgAbs = stuff.getModifiedChargeAttr("dotMaxDamagePerTick")
dmgRel = stuff.getModifiedChargeAttr("dotMaxHPPercentagePerTick")
text = "{}/{}% over {}s".format(
formatAmount(dmgAbs, 3, 0, 6),
formatAmount(dmgRel, 3, 0, 6),
formatAmount(dmgAbs * duration, 3, 0, 6),
formatAmount(dmgRel * duration, 3, 0, 6),
formatAmount(duration, 0, 0, 0))
tooltip = "Pure damage done over time, minimum of absolute / relative"
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

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

@@ -13,8 +13,8 @@ from service.market import Market
def stripHtml(text):
text = re.sub('<\s*br\s*/?\s*>', '\n', text)
text = re.sub('</?[^/]+?(/\s*)?>', '', text)
text = re.sub(r'<\s*br\s*/?\s*>', '\n', text)
text = re.sub(r'</?[^/]+?(/\s*)?>', '', text)
return text

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()

View File

@@ -66,8 +66,8 @@ class UpdateDialog(wx.Dialog):
self.browser.Bind(wx.html2.EVT_WEBVIEW_NEWWINDOW, self.OnNewWindow)
link_patterns = [
(re.compile("#(\d+)", re.I), r"https://github.com/pyfa-org/Pyfa/issues/\1"),
(re.compile("@(\w+)", re.I), r"https://github.com/\1")
(re.compile(r"#(\d+)", re.I), r"https://github.com/pyfa-org/Pyfa/issues/\1"),
(re.compile(r"@(\w+)", re.I), r"https://github.com/\1")
]
markdowner = markdown2.Markdown(

32
gui/utils/gdi.py Normal file
View File

@@ -0,0 +1,32 @@
import gc
from ctypes import *
from collections import defaultdict
import os
def gdiReport(desc=''):
PH = windll.kernel32.OpenProcess(0x400, 0, os.getpid())
numGdi = windll.user32.GetGuiResources(PH, 0)
windll.kernel32.CloseHandle(PH)
print (f'{desc}, {numGdi}')
last = None
def output_memory():
global last
d = defaultdict(int)
for o in gc.get_objects():
name = type(o).__name__
if name == 'Bitmap':
del o
d[name] += 1
items = d.items()
items = sorted(items,key=lambda x:x[1])
print('------')
for key, value in items:
if last is not None:
if value -last[key] !=0:
print(f'{key} {value - last[key]}, {value}')
else:
print( key, value)
last = d

View File

@@ -96,7 +96,7 @@ class FloatBox(wx.TextCtrl):
if currentValue == self._storedValue:
event.Skip()
return
if currentValue == '' or re.match('^\d*\.?\d*$', currentValue):
if currentValue == '' or re.match(r'^\d*\.?\d*$', currentValue):
self._storedValue = currentValue
self.updateColor()
event.Skip()
@@ -131,7 +131,7 @@ class FloatRangeBox(wx.TextCtrl):
if currentValue == self._storedValue:
event.Skip()
return
if currentValue == '' or re.match('^\d*\.?\d*-?\d*\.?\d*$', currentValue):
if currentValue == '' or re.match(r'^\d*\.?\d*-?\d*\.?\d*$', currentValue):
self._storedValue = currentValue
event.Skip()
else:

BIN
imgs/gui/hp_big.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 671 B

After

Width:  |  Height:  |  Size: 890 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 676 B

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 676 B

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 679 B

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 906 B

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 913 B

After

Width:  |  Height:  |  Size: 905 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 911 B

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 915 B

After

Width:  |  Height:  |  Size: 905 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 890 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 666 B

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 B

After

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 675 B

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 B

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 B

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 663 B

After

Width:  |  Height:  |  Size: 899 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 B

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 893 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 663 B

After

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 B

After

Width:  |  Height:  |  Size: 909 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 B

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 B

After

Width:  |  Height:  |  Size: 885 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 670 B

After

Width:  |  Height:  |  Size: 891 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 670 B

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

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