Compare commits

..

95 Commits

Author SHA1 Message Date
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
DarkPhoenix
fa67efd9cb Revert wxpython version change, since binary wheels seem to be unavailable for it yet 2024-11-09 23:54:00 +01:00
DarkPhoenix
38a345df93 Allow release info change on linux 2024-11-09 17:29:53 +01:00
DarkPhoenix
b41ec8a5a9 Bump version 2024-11-09 16:45:26 +01:00
DarkPhoenix
4ba9bb7b99 Add info to misc column 2024-11-09 16:37:46 +01:00
DarkPhoenix
f3c8f485b9 Implement breacher pod skill effects 2024-11-09 12:01:28 +01:00
DarkPhoenix
8d204dd173 Add deathless faction icon 2024-11-09 11:37:24 +01:00
DarkPhoenix
f870d7e6d2 Add effects for new ships 2024-11-09 11:28:29 +01:00
DarkPhoenix
715997041a Update icons 2024-11-09 09:23:19 +01:00
DarkPhoenix
003191d9a4 Update effects for everything but new ships 2024-11-09 09:21:48 +01:00
DarkPhoenix
9302d79e1d Sort glorified mutaplasmids right after their base versions, and shorten them to Gl. 2024-11-09 09:04:18 +01:00
DarkPhoenix
401fc671d4 Fix a few regex warnings 2024-11-09 08:40:55 +01:00
DarkPhoenix
fbadcf0af6 Update static data 2024-11-09 08:28:44 +01:00
DarkPhoenix
c7f600f88c Bump wxpython version 2024-11-02 08:15:33 +01:00
DarkPhoenix
229dd0ddae Fix sorter for structure fighters 2024-11-02 08:15:21 +01:00
DarkPhoenix
8883b18025 Bump version 2024-10-31 00:50:26 +01:00
DarkPhoenix
ea9b67cedd Implement python effects 2024-10-31 00:50:08 +01:00
DarkPhoenix
c4cf1d1ecb Implement Cobra effects 2024-10-31 00:32:50 +01:00
DarkPhoenix
f76f7e85db Add ability to override character security status via a menu 2024-10-31 00:08:31 +01:00
DarkPhoenix
b807e2a36b Add on-fit pilot security attribute, and use it in effects 2024-10-30 20:09:26 +01:00
DarkPhoenix
2bb35a15ce Change lower limit of SS to be -10 instead of -5 2024-10-30 19:12:19 +01:00
DarkPhoenix
376bda7a94 Implement sidewinder effects 2024-10-30 19:10:25 +01:00
DarkPhoenix
a642bdc8c9 Move prize ships to separate category 2024-10-30 18:16:14 +01:00
DarkPhoenix
71f3e7b858 Update icons 2024-10-30 18:12:47 +01:00
DarkPhoenix
88f5ed2da5 Update staticdata 2024-10-30 18:04:41 +01:00
DarkPhoenix
3b9bbae0c3 Bump version 2024-10-03 13:37:54 +02:00
DarkPhoenix
6d474492ea Update effects file 2024-10-03 13:37:25 +02:00
DarkPhoenix
ebc61efef8 Update static data 2024-10-03 13:28:30 +02:00
Ugur Ilter
3892dfc78c fix arbitrary content being dragged and dropped causing errors
Fixes #2630
2024-09-26 15:52:26 +03:00
DarkPhoenix
7b5c95213f Bump version 2024-09-18 13:54:33 +02:00
DarkPhoenix
1312177e0e Add new renames 2024-09-18 13:42:53 +02:00
DarkPhoenix
5b0df0a9c3 Update static data to 2682489 2024-09-18 13:33:03 +02:00
wereii
954a164922 Fix invalid escape syntax warnings
Additionally use rawstrings with all regex calls
2024-09-11 17:25:38 +02:00
DarkPhoenix
1ea6136cb2 Update static data 2024-09-11 16:32:13 +02:00
DarkPhoenix
da2b6bbb6d Bump version 2024-09-10 00:01:40 +02:00
DarkPhoenix
87007af5f8 Pass package path to importlib call, not module path 2024-09-09 23:35:07 +02:00
DarkPhoenix
dd5ab872b1 Reapply "Make it 3.9+, but get rid of deprecation warnings"
This reverts commit 65fb46885a.
2024-09-09 23:31:19 +02:00
DarkPhoenix
65fb46885a Revert "Make it 3.9+, but get rid of deprecation warnings"
This reverts commit 01354a0a83.
2024-09-09 23:00:24 +02:00
DarkPhoenix
fad1e401b2 Bump version 2024-09-09 20:25:02 +02:00
DarkPhoenix
d59f8afd8a Update icons 2024-09-09 20:24:33 +02:00
DarkPhoenix
7c42d00219 Update static data 2024-09-09 20:20:44 +02:00
DarkPhoenix
777ba69ac4 Ensure context menus can be called on items w/o market group 2024-07-20 19:09:00 +02:00
DarkPhoenix
9a3bde872b Fix regex pattern to remove syntax warning 2024-07-09 23:23:49 +02:00
DarkPhoenix
01354a0a83 Make it 3.9+, but get rid of deprecation warnings 2024-07-09 21:49:05 +02:00
DarkPhoenix
8c3b8589d5 Adjust jargon loader to be py3.12-compatible 2024-07-09 21:46:39 +02:00
Anton Vorobyov
451b5d4312 Merge pull request #2624 from lunedis/feature/charges-for-fit
Add "Charges For Active Fit" market tree shortcut
2024-07-09 23:31:53 +04:00
Anton Vorobyov
d204e70afc Merge pull request #2614 from kgasiorowski/issue/2613
Rename Industrial to Hauler
2024-07-09 23:29:02 +04:00
Karl Werner
ce9ce17ad2 use mode to limit refresh 2024-07-07 14:41:16 +02:00
Karl Werner
6a7cdda91a add comments 2024-07-06 16:01:58 +02:00
Karl Werner
f38c61da51 update on fit change, new icon 2024-07-06 15:52:18 +02:00
Karl Werner
735827a25b proof of concept 2024-07-06 12:12:48 +02:00
DarkPhoenix
d3fcdcbe47 Marshal bonus is not stacking penalized 2024-06-29 23:50:03 +02:00
DarkPhoenix
a3dce73663 Update static data 2024-06-27 21:21:35 +02:00
Kuba Gasiorowski
594b58388f Merge branch 'master' into issue/2613 2024-06-14 18:40:49 -04:00
DarkPhoenix
fa6be2edfb Bump version 2024-06-13 16:20:49 +02:00
DarkPhoenix
1177968b02 Update renders 2024-06-13 16:20:29 +02:00
DarkPhoenix
c04b370410 Update static data 2024-06-13 16:15:45 +02:00
DarkPhoenix
038b95531d Bump version 2024-06-12 16:04:00 +02:00
DarkPhoenix
56bc5c3376 Fix icons script and add missing renders 2024-06-12 16:03:09 +02:00
DarkPhoenix
f5d8be7861 Update static data and add new effect 2024-06-12 15:11:15 +02:00
Kuba Gasiorowski
5366c23db2 Merge branch 'master' into issue/2613 2024-06-11 14:03:24 -04:00
DarkPhoenix
d147db22f1 Bump version 2024-06-11 16:33:10 +02:00
DarkPhoenix
28ebcd2739 Fix upwell ship race detection 2024-06-08 03:12:34 +02:00
Kuba Gasiorowski
6d32db5827 Rename Industrial to Hauler 2024-05-26 23:45:27 -04:00
530 changed files with 118721 additions and 26035 deletions

View File

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

View File

@@ -141,14 +141,14 @@ def update_db():
(row['typeName_en-us'].startswith('Civilian') and "Shuttle" not in row['typeName_en-us']) (row['typeName_en-us'].startswith('Civilian') and "Shuttle" not in row['typeName_en-us'])
or row['typeName_en-us'] == 'Capsule' or row['typeName_en-us'] == 'Capsule'
or row['groupID'] == 4033 # destructible effect beacons 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 row['published'] = True
# Nearly useless and clutter search results too much # Nearly useless and clutter search results too much
elif ( elif (
row['typeName_en-us'].startswith('Limited Synth ') row['typeName_en-us'].startswith('Limited Synth ')
or row['typeName_en-us'].startswith('Expired ') 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 ( or row['typeName_en-us'].endswith(' Filament') and (
"'Needlejack'" not in row['typeName_en-us'] and "'Needlejack'" not in row['typeName_en-us'] and
"'Devana'" not in row['typeName_en-us'] and "'Devana'" not in row['typeName_en-us'] and
@@ -544,7 +544,7 @@ def update_db():
continue continue
typeName = row.get('typeName_en-us', '') typeName = row.get('typeName_en-us', '')
# Regular sets matching # Regular sets matching
m = re.match('(?P<grade>(High|Mid|Low)-grade) (?P<set>\w+) (?P<implant>(Alpha|Beta|Gamma|Delta|Epsilon|Omega))', typeName, re.IGNORECASE) m = re.match(r'(?P<grade>(High|Mid|Low)-grade) (?P<set>\w+) (?P<implant>(Alpha|Beta|Gamma|Delta|Epsilon|Omega))', typeName, re.IGNORECASE)
if m: if m:
implantSets.setdefault((m.group('grade'), m.group('set')), set()).add(row['typeID']) implantSets.setdefault((m.group('grade'), m.group('set')), set()).add(row['typeID'])
# Special set matching # Special set matching

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_DIST_DIR"] = os.path.join(os.getcwd(), 'dist')
os.environ["PYFA_VERSION"] = version 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") 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 # loop through python files, extracting update number and function, and
# adding it to a list # adding it to a list
modname_tail = modName.rsplit('.', 1)[-1] 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: if not m:
continue continue
index = int(m.group("index")) index = int(m.group("index"))

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("ignoreRestrictions", Boolean, default=0),
Column("created", DateTime, nullable=True, default=datetime.datetime.now), Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, default=datetime.datetime.now, onupdate=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, projectedFits_table = Table("projectedFits", saveddata_meta,

View File

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

@@ -343,7 +343,9 @@ class Item(EqBase):
500018: "mordu", 500018: "mordu",
500019: "sansha", 500019: "sansha",
500020: "serpentis", 500020: "serpentis",
500026: "triglavian" 500026: "triglavian",
500027: "upwell",
500029: "deathless",
} }
@property @property
@@ -351,11 +353,7 @@ class Item(EqBase):
if self.__race is None: if self.__race is None:
try: try:
if ( if self.category.name == 'Structure':
self.category.name == 'Structure' or
# Here until CCP puts their shit together
self.name in ("Thunderchild", "Stormbringer", "Skybreaker")
):
self.__race = "upwell" self.__race = "upwell"
else: else:
self.__race = self.factionMap[self.factionID] self.__race = self.factionMap[self.factionID]
@@ -572,13 +570,18 @@ class DynamicItem(EqBase):
@property @property
def shortName(self): def shortName(self):
name = self.item.customName 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: for kw in keywords:
if name.startswith(f'{kw} '): if name.startswith(f'{kw} '):
name = kw name = kw
m = re.match('(?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: if m:
name = '{} {}'.format(m.group('mutagrade'), m.group('mutatype')) name = '{} {}'.format(m.group('mutagrade'), m.group('mutatype'))
name = name.replace('Glorified ', 'Gl. ')
return name return name

View File

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

View File

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

View File

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

View File

@@ -1688,27 +1688,33 @@ class Fit:
self.__droneWaste = droneWaste self.__droneWaste = droneWaste
def calculateWeaponDmgStats(self, spoolOptions): def calculateWeaponDmgStats(self, spoolOptions):
weaponVolley = DmgTypes(0, 0, 0, 0) weaponVolley = DmgTypes.default()
weaponDps = DmgTypes(0, 0, 0, 0) weaponDps = DmgTypes.default()
for mod in self.modules: for mod in self.modules:
weaponVolley += mod.getVolley(spoolOptions=spoolOptions, targetProfile=self.targetProfile) weaponVolley += mod.getVolley(spoolOptions=spoolOptions)
weaponDps += mod.getDps(spoolOptions=spoolOptions, targetProfile=self.targetProfile) weaponDps += mod.getDps(spoolOptions=spoolOptions)
weaponVolley.profile = self.targetProfile
weaponDps.profile = self.targetProfile
self.__weaponVolleyMap[spoolOptions] = weaponVolley self.__weaponVolleyMap[spoolOptions] = weaponVolley
self.__weaponDpsMap[spoolOptions] = weaponDps self.__weaponDpsMap[spoolOptions] = weaponDps
def calculateDroneDmgStats(self): def calculateDroneDmgStats(self):
droneVolley = DmgTypes(0, 0, 0, 0) droneVolley = DmgTypes.default()
droneDps = DmgTypes(0, 0, 0, 0) droneDps = DmgTypes.default()
for drone in self.drones: for drone in self.drones:
droneVolley += drone.getVolley(targetProfile=self.targetProfile) droneVolley += drone.getVolley()
droneDps += drone.getDps(targetProfile=self.targetProfile) droneDps += drone.getDps()
for fighter in self.fighters: for fighter in self.fighters:
droneVolley += fighter.getVolley(targetProfile=self.targetProfile) droneVolley += fighter.getVolley()
droneDps += fighter.getDps(targetProfile=self.targetProfile) droneDps += fighter.getDps()
droneVolley.profile = self.targetProfile
droneDps.profile = self.targetProfile
self.__droneDps = droneDps self.__droneDps = droneDps
self.__droneVolley = droneVolley self.__droneVolley = droneVolley
@@ -1743,6 +1749,18 @@ class Fit:
secstatus = FitSystemSecurity.NULLSEC secstatus = FitSystemSecurity.NULLSEC
return secstatus 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): def activeModulesIter(self):
for mod in self.modules: for mod in self.modules:
if mod.state >= FittingModuleState.ACTIVE: if mod.state >= FittingModuleState.ACTIVE:
@@ -1824,6 +1842,7 @@ class Fit:
fitCopy.targetProfile = self.targetProfile fitCopy.targetProfile = self.targetProfile
fitCopy.implantLocation = self.implantLocation fitCopy.implantLocation = self.implantLocation
fitCopy.systemSecurity = self.systemSecurity fitCopy.systemSecurity = self.systemSecurity
fitCopy.pilotSecurity = self.pilotSecurity
fitCopy.notes = self.notes fitCopy.notes = self.notes
for i in self.modules: 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.default import DEFAULT
from eos.utils.float import floatUnerr from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import calculateSpoolup, resolveSpoolOptions 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__) pyfalog = Logger(__name__)
@@ -453,6 +453,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
return True return True
return False return False
@property
def isBreacher(self):
return self.charge and 'dotMissileLaunching' in self.charge.effects
def canDealDamage(self, ignoreState=False): def canDealDamage(self, ignoreState=False):
if self.isEmpty: if self.isEmpty:
return False return False
@@ -469,75 +473,77 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
def getVolleyParameters(self, spoolOptions=None, targetProfile=None, ignoreState=False): def getVolleyParameters(self, spoolOptions=None, targetProfile=None, ignoreState=False):
if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState): 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: if self.__baseVolley is None:
self.__baseVolley = {} self.__baseVolley = {}
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr if self.isBreacher:
dmgMult = self.getModifiedItemAttr("damageMultiplier", 1) dmgDelay = 1
# Some delay attributes have non-0 default value, so we have to pick according to effects subcycles = math.floor(self.getModifiedChargeAttr("dotDuration", 0) / 1000)
if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(self.item.effects): breacher_info = BreacherInfo(
dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0) absolute=self.getModifiedChargeAttr("dotMaxDamagePerTick", 0),
elif {'doomsdayBeamDOT', 'doomsdaySlash', 'doomsdayConeDOT', 'debuffLance'}.intersection(self.item.effects): relative=self.getModifiedChargeAttr("dotMaxHPPercentagePerTick", 0) / 100)
dmgDelay = self.getModifiedItemAttr("doomsdayWarningDuration", 0) for i in range(subcycles):
volley = DmgTypes.default()
volley.add_breacher(dmgDelay + i, breacher_info)
self.__baseVolley[dmgDelay + i] = volley
else: else:
dmgDelay = 0 dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0) dmgMult = self.getModifiedItemAttr("damageMultiplier", 1)
dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0) # Some delay attributes have non-0 default value, so we have to pick according to effects
# Reaper DD can damage each target only once if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(self.item.effects):
if dmgDuration != 0 and dmgSubcycle != 0 and 'doomsdaySlash' not in self.item.effects: dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0)
subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle)) elif {'doomsdayBeamDOT', 'doomsdaySlash', 'doomsdayConeDOT', 'debuffLance'}.intersection(self.item.effects):
else: dmgDelay = self.getModifiedItemAttr("doomsdayWarningDuration", 0)
subcycles = 1 else:
for i in range(subcycles): dmgDelay = 0
self.__baseVolley[dmgDelay + dmgSubcycle * i] = DmgTypes( dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0)
em=(dmgGetter("emDamage", 0)) * dmgMult, dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0)
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult, # Reaper DD can damage each target only once
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult, if dmgDuration != 0 and dmgSubcycle != 0 and 'doomsdaySlash' not in self.item.effects:
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult) 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) spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self)
spoolBoost = calculateSpoolup( spoolBoost = calculateSpoolup(
self.getModifiedItemAttr("damageMultiplierBonusMax", 0), self.getModifiedItemAttr("damageMultiplierBonusMax", 0),
self.getModifiedItemAttr("damageMultiplierBonusPerCycle", 0), self.getModifiedItemAttr("damageMultiplierBonusPerCycle", 0),
self.rawCycleTime / 1000, spoolType, spoolAmount)[0] self.rawCycleTime / 1000, spoolType, spoolAmount)[0]
spoolMultiplier = 1 + spoolBoost spoolMultiplier = 1 + spoolBoost
adjustedVolley = {} adjustedVolleys = {}
for volleyTime, volleyValue in self.__baseVolley.items(): for volleyTime, baseVolley in self.__baseVolley.items():
adjustedVolley[volleyTime] = DmgTypes( adjustedVolley = baseVolley * spoolMultiplier
em=volleyValue.em * spoolMultiplier * (1 - getattr(targetProfile, "emAmount", 0)), adjustedVolley.profile = targetProfile
thermal=volleyValue.thermal * spoolMultiplier * (1 - getattr(targetProfile, "thermalAmount", 0)), adjustedVolleys[volleyTime] = adjustedVolley
kinetic=volleyValue.kinetic * spoolMultiplier * (1 - getattr(targetProfile, "kineticAmount", 0)), return adjustedVolleys
explosive=volleyValue.explosive * spoolMultiplier * (1 - getattr(targetProfile, "explosiveAmount", 0)))
return adjustedVolley
def getVolley(self, spoolOptions=None, targetProfile=None, ignoreState=False): def getVolley(self, spoolOptions=None, targetProfile=None, ignoreState=False):
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState) volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
if len(volleyParams) == 0: if len(volleyParams) == 0:
return DmgTypes(0, 0, 0, 0) return DmgTypes.default()
return volleyParams[min(volleyParams)] return volleyParams[min(volleyParams)]
def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False, getSpreadDPS=False): def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False):
dmgDuringCycle = DmgTypes(0, 0, 0, 0) dps = DmgTypes.default()
cycleParams = self.getCycleParameters() cycleParams = self.getCycleParameters()
if cycleParams is None: if cycleParams is None:
return dmgDuringCycle return dps
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState) volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
avgCycleTime = cycleParams.averageTime avgCycleTime = cycleParams.averageTime
if len(volleyParams) == 0 or avgCycleTime == 0: 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 dps
return {'em':dmgDuringCycle.em * dpsFactor, if self.isBreacher:
'therm': dmgDuringCycle.thermal * dpsFactor, return volleyParams[min(volleyParams)]
'kin': dmgDuringCycle.kinetic * dpsFactor, for volleyValue in volleyParams.values():
'exp': dmgDuringCycle.explosive * dpsFactor} dps += volleyValue
dpsFactor = 1 / (avgCycleTime / 1000)
dps *= dpsFactor
return dps
def isRemoteRepping(self, ignoreState=False): def isRemoteRepping(self, ignoreState=False):
repParams = self.getRepAmountParameters(ignoreState=ignoreState) 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): and ((gang and effect.isType("gang")) or not gang):
effect.handler(fit, self, context, projectionRange, effect=effect) 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): def getCycleParameters(self, reloadOverride=None):
"""Copied from new eos as well""" """Copied from new eos as well"""
# Determine if we'll take into account reload time or not # Determine if we'll take into account reload time or not

View File

@@ -254,7 +254,7 @@ class TargetProfile:
def init(self): def init(self):
self.builtin = False 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.emAmount = emAmount
self.thermalAmount = thermalAmount self.thermalAmount = thermalAmount
self.kineticAmount = kineticAmount self.kineticAmount = kineticAmount
@@ -262,6 +262,7 @@ class TargetProfile:
self._maxVelocity = maxVelocity self._maxVelocity = maxVelocity
self._signatureRadius = signatureRadius self._signatureRadius = signatureRadius
self._radius = radius self._radius = radius
self._hp = hp
@classmethod @classmethod
def getBuiltinList(cls): def getBuiltinList(cls):
@@ -331,6 +332,18 @@ class TargetProfile:
def radius(self, val): def radius(self, val):
self._radius = 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 @classmethod
def importPatterns(cls, text): def importPatterns(cls, text):
lines = re.split('[\n\r]+', 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 eos.utils.float import floatUnerr
from utils.repr import makeReprStr from utils.repr import makeReprStr
@@ -26,15 +29,133 @@ def _t(x):
return 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: 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): def __init__(self, em, thermal, kinetic, explosive):
self.em = em self._em = em
self.thermal = thermal self._thermal = thermal
self.kinetic = kinetic self._kinetic = kinetic
self.explosive = explosive self._explosive = explosive
self._calcTotal() 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 # Iterator is needed to support tuple-style unpacking
def __iter__(self): def __iter__(self):
@@ -42,6 +163,7 @@ class DmgTypes:
yield self.thermal yield self.thermal
yield self.kinetic yield self.kinetic
yield self.explosive yield self.explosive
yield self.pure
yield self.total yield self.total
def __eq__(self, other): def __eq__(self, other):
@@ -50,77 +172,87 @@ class DmgTypes:
# Round for comparison's sake because often damage profiles are # Round for comparison's sake because often damage profiles are
# generated from data which includes float errors # generated from data which includes float errors
return ( return (
floatUnerr(self.em) == floatUnerr(other.em) and floatUnerr(self._em) == floatUnerr(other._em) and
floatUnerr(self.thermal) == floatUnerr(other.thermal) and floatUnerr(self._thermal) == floatUnerr(other._thermal) and
floatUnerr(self.kinetic) == floatUnerr(other.kinetic) and floatUnerr(self._kinetic) == floatUnerr(other._kinetic) and
floatUnerr(self.explosive) == floatUnerr(other.explosive) and floatUnerr(self._explosive) == floatUnerr(other._explosive) and
floatUnerr(self.total) == floatUnerr(other.total)) sorted(self._breachers) == sorted(other._breachers) and
self.profile == other.profile)
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
def __add__(self, other): def __add__(self, other):
return type(self)( new = type(self)(
em=self.em + other.em, em=self._em + other._em,
thermal=self.thermal + other.thermal, thermal=self._thermal + other._thermal,
kinetic=self.kinetic + other.kinetic, kinetic=self._kinetic + other._kinetic,
explosive=self.explosive + other.explosive) 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): def __iadd__(self, other):
self.em += other.em self._em += other._em
self.thermal += other.thermal self._thermal += other._thermal
self.kinetic += other.kinetic self._kinetic += other._kinetic
self.explosive += other.explosive self._explosive += other._explosive
self._calcTotal() for k, v in other._breachers.items():
self._breachers[k].extend(v)
self._clear_cached()
return self return self
def __mul__(self, mul): def __mul__(self, mul):
return type(self)( new = type(self)(
em=self.em * mul, em=self._em * mul,
thermal=self.thermal * mul, thermal=self._thermal * mul,
kinetic=self.kinetic * mul, kinetic=self._kinetic * mul,
explosive=self.explosive * 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): def __imul__(self, mul):
if mul == 1: if mul == 1:
return return self
self.em *= mul self._em *= mul
self.thermal *= mul self._thermal *= mul
self.kinetic *= mul self._kinetic *= mul
self.explosive *= mul self._explosive *= mul
self._calcTotal() for v in self._breachers.values():
for b in v:
b *= mul
self._clear_cached()
return self return self
def __truediv__(self, div): def __truediv__(self, div):
return type(self)( new = type(self)(
em=self.em / div, em=self._em / div,
thermal=self.thermal / div, thermal=self._thermal / div,
kinetic=self.kinetic / div, kinetic=self._kinetic / div,
explosive=self.explosive / 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): def __bool__(self):
if div == 1: return any((
return self._em, self._thermal, self._kinetic, self._explosive,
self.em /= div any(b.absolute or b.relative for b in self._breachers)))
self.thermal /= div
self.kinetic /= div
self.explosive /= div
self._calcTotal()
return self
def __repr__(self): def __repr__(self):
spec = DmgTypes.names() class_name = type(self).__name__
spec.append('total') return (f'<{class_name}(em={self._em}, thermal={self._thermal}, kinetic={self._kinetic}, '
return makeReprStr(self, spec) f'explosive={self._explosive}, breachers={len(self._breachers)})>')
@staticmethod @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')] 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: if postProcessor:
value = [postProcessor(x) for x in value] value = [postProcessor(x) for x in value]

View File

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

View File

@@ -98,6 +98,8 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
tgt=tgt, tgt=tgt,
distance=distance, distance=distance,
tgtSigRadius=tgtSigRadius) tgtSigRadius=tgtSigRadius)
elif mod.isBreacher:
applicationMap[mod] = getBreacherMult(mod=mod, distance=distance) if inLockRange else 0
for drone in src.item.activeDronesIter(): for drone in src.item.activeDronesIter():
if not drone.isDealingDamage(): if not drone.isDealingDamage():
continue continue
@@ -192,6 +194,21 @@ def getLauncherMult(mod, distance, tgtSpeed, tgtSigRadius):
return distanceFactor * applicationFactor 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): def getSmartbombMult(mod, distance):
modRange = mod.maxRange modRange = mod.maxRange
if modRange is None: if modRange is None:

View File

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

View File

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

View File

@@ -273,7 +273,7 @@ class GraphCanvasPanel(wx.Panel):
legendLines = [] legendLines = []
for i, iData in enumerate(legendData): for i, iData in enumerate(legendData):
color, lineStyle, label = iData 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: if len(legendLines) > 0 and self.graphFrame.ctrlPanel.showLegend:
legend = self.subplot.legend(handles=legendLines) legend = self.subplot.legend(handles=legendLines)

View File

@@ -145,6 +145,11 @@ class TargetWrapper(BaseWrapper):
else: else:
return em, therm, kin, explo 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): def _getShieldResists(ship):

View File

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

View File

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

View File

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

View File

@@ -52,6 +52,10 @@ class DroneViewDrop(wx.DropTarget):
def OnData(self, x, y, t): def OnData(self, x, y, t):
if self.GetData(): if self.GetData():
dragged_data = DragDropHelper.data dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':') data = dragged_data.split(':')
self.dropFn(x, y, data) self.dropFn(x, y, data)
return t return t
@@ -195,7 +199,11 @@ class DroneView(Display):
@staticmethod @staticmethod
def droneKey(drone): 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) return (DRONE_ORDER.index(groupName), drone.isMutated, drone.fullName)
def fitChanged(self, event): def fitChanged(self, event):

View File

@@ -34,7 +34,10 @@ from service.fit import Fit
from service.market import Market 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 _t = wx.GetTranslation
@@ -49,6 +52,10 @@ class FighterViewDrop(wx.DropTarget):
def OnData(self, x, y, t): def OnData(self, x, y, t):
if self.GetData(): if self.GetData():
dragged_data = DragDropHelper.data dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':') data = dragged_data.split(':')
self.dropFn(x, y, data) self.dropFn(x, y, data)
return t return t

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ from gui.builtinContextMenus.targetProfile import editor
from gui.builtinContextMenus import itemStats from gui.builtinContextMenus import itemStats
from gui.builtinContextMenus import itemMarketJump 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 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 from gui.builtinContextMenus import shipJump
# Generic item manipulations # Generic item manipulations
from gui.builtinContextMenus import itemRemove from gui.builtinContextMenus import itemRemove

View File

@@ -25,7 +25,7 @@ class AddToCargoAmmo(ContextMenuSingle):
return True return True
def getText(self, callingWindow, itmContext, mainItem): def getText(self, callingWindow, itmContext, mainItem):
if mainItem.marketGroup.name == "Scan Probes": if mainItem.marketGroup and mainItem.marketGroup.name == "Scan Probes":
return _t("Add {0} to Cargo (x8)").format(itmContext) return _t("Add {0} to Cargo (x8)").format(itmContext)
return _t("Add {0} to Cargo (x1000)").format(itmContext) return _t("Add {0} to Cargo (x1000)").format(itmContext)
@@ -34,7 +34,7 @@ class AddToCargoAmmo(ContextMenuSingle):
fitID = self.mainFrame.getActiveFit() fitID = self.mainFrame.getActiveFit()
typeID = int(mainItem.ID) typeID = int(mainItem.ID)
if mainItem.marketGroup.name == "Scan Probes": if mainItem.marketGroup and mainItem.marketGroup.name == "Scan Probes":
command = cmd.GuiAddCargoCommand(fitID=fitID, itemID=typeID, amount=8) command = cmd.GuiAddCargoCommand(fitID=fitID, itemID=typeID, amount=8)
else: else:
command = cmd.GuiAddCargoCommand(fitID=fitID, itemID=typeID, amount=1000) command = cmd.GuiAddCargoCommand(fitID=fitID, itemID=typeID, amount=1000)

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 _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): class ChangeItemMutation(ContextMenuSingle):
def __init__(self): def __init__(self):
@@ -45,7 +55,7 @@ class ChangeItemMutation(ContextMenuSingle):
menu = rootMenu if msw else sub menu = rootMenu if msw else sub
for mutaplasmid in mainItem.item.mutaplasmids: for mutaplasmid in sorted(mainItem.item.mutaplasmids, key=nameSorter):
id = ContextMenuSingle.nextID() id = ContextMenuSingle.nextID()
self.eventIDs[id] = (mutaplasmid, mainItem) self.eventIDs[id] = (mutaplasmid, mainItem)
mItem = wx.MenuItem(menu, id, mutaplasmid.shortName) mItem = wx.MenuItem(menu, id, mutaplasmid.shortName)

View File

@@ -22,9 +22,9 @@ class ItemDescription(wx.Panel):
desc = item.description.replace("\n", "<br>") desc = item.description.replace("\n", "<br>")
# Strip font tags # 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 # 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( desc = "<body bgcolor='{}' text='{}'>{}</body>".format(
bgcolor.GetAsString(wx.C2S_HTML_SYNTAX), bgcolor.GetAsString(wx.C2S_HTML_SYNTAX),
fgcolor.GetAsString(wx.C2S_HTML_SYNTAX), fgcolor.GetAsString(wx.C2S_HTML_SYNTAX),

View File

@@ -5,3 +5,5 @@ import wx.lib.newevent
ItemSelected, ITEM_SELECTED = wx.lib.newevent.NewEvent() ItemSelected, ITEM_SELECTED = wx.lib.newevent.NewEvent()
RECENTLY_USED_MODULES = -2 RECENTLY_USED_MODULES = -2
CHARGES_FOR_FIT = -3

View File

@@ -2,15 +2,17 @@ import wx
from logbook import Logger from logbook import Logger
import gui.builtinMarketBrowser.pfSearchBox as SBox import gui.builtinMarketBrowser.pfSearchBox as SBox
import gui.globalEvents as GE
from config import slotColourMap, slotColourMapDark from config import slotColourMap, slotColourMapDark
from eos.saveddata.module import Module from eos.saveddata.module import Module
from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES, CHARGES_FOR_FIT
from gui.contextMenu import ContextMenu from gui.contextMenu import ContextMenu
from gui.display import Display from gui.display import Display
from gui.utils.staticHelpers import DragDropHelper from gui.utils.staticHelpers import DragDropHelper
from gui.utils.dark import isDark from gui.utils.dark import isDark
from service.fit import Fit from service.fit import Fit
from service.market import Market from service.market import Market
from service.ammo import Ammo
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@@ -32,6 +34,7 @@ class ItemView(Display):
self.filteredStore = set() self.filteredStore = set()
self.sMkt = marketBrowser.sMkt self.sMkt = marketBrowser.sMkt
self.sFit = Fit.getInstance() self.sFit = Fit.getInstance()
self.sAmmo = Ammo.getInstance()
self.marketBrowser = marketBrowser self.marketBrowser = marketBrowser
self.marketView = marketBrowser.marketView self.marketView = marketBrowser.marketView
@@ -51,6 +54,9 @@ class ItemView(Display):
self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.itemActivated) self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.itemActivated)
self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag) self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag)
# the "charges for active fitting" needs to listen to fitting changes
self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged)
self.active = [] self.active = []
def delaySearch(self, evt): def delaySearch(self, evt):
@@ -91,7 +97,11 @@ class ItemView(Display):
if sel.IsOk(): if sel.IsOk():
# Get data field of the selected item (which is a marketGroup ID if anything was selected) # Get data field of the selected item (which is a marketGroup ID if anything was selected)
seldata = self.marketView.GetItemData(sel) seldata = self.marketView.GetItemData(sel)
if seldata is not None and seldata != RECENTLY_USED_MODULES: if seldata == RECENTLY_USED_MODULES:
items = self.sMkt.getRecentlyUsed()
elif seldata == CHARGES_FOR_FIT:
items = self.getChargesForActiveFit()
elif seldata is not None:
# If market group treeview item doesn't have children (other market groups or dummies), # If market group treeview item doesn't have children (other market groups or dummies),
# then it should have items in it and we want to request them # then it should have items in it and we want to request them
if self.marketView.ItemHasChildren(sel) is False: if self.marketView.ItemHasChildren(sel) is False:
@@ -103,11 +113,7 @@ class ItemView(Display):
else: else:
items = set() items = set()
else: else:
# If method was called but selection wasn't actually made or we have a hit on recently used modules items = set()
if seldata == RECENTLY_USED_MODULES:
items = self.sMkt.getRecentlyUsed()
else:
items = set()
# Fill store # Fill store
self.updateItemStore(items) self.updateItemStore(items)
@@ -115,6 +121,9 @@ class ItemView(Display):
# Set toggle buttons / use search mode flag if recently used modules category is selected (in order to have all modules listed and not filtered) # Set toggle buttons / use search mode flag if recently used modules category is selected (in order to have all modules listed and not filtered)
if seldata == RECENTLY_USED_MODULES: if seldata == RECENTLY_USED_MODULES:
self.marketBrowser.mode = 'recent' self.marketBrowser.mode = 'recent'
if seldata == CHARGES_FOR_FIT:
self.marketBrowser.mode = 'charges'
self.setToggles() self.setToggles()
if context == 'tree' and self.marketBrowser.settings.get('marketMGMarketSelectMode') == 1: if context == 'tree' and self.marketBrowser.settings.get('marketMGMarketSelectMode') == 1:
@@ -123,6 +132,41 @@ class ItemView(Display):
btn.setUserSelection(True) btn.setUserSelection(True)
self.filterItemStore() self.filterItemStore()
def getChargesForActiveFit(self):
fitId = self.mainFrame.getActiveFit()
# no active fit => no charges
if fitId is None:
return set()
fit = self.sFit.getFit(fitId)
# use a set so we only add one entry for each charge
items = set()
for mod in fit.modules:
charges = self.sAmmo.getModuleFlatAmmo(mod)
for charge in charges:
items.add(charge)
return items
def fitChanged(self, event):
# skip the event so the other handlers also get called
event.Skip()
if self.marketBrowser.mode != 'charges':
return
activeFitID = self.mainFrame.getActiveFit()
# if it was not the active fitting that was changed, do not do anything
if activeFitID is not None and activeFitID not in event.fitIDs:
return
items = self.getChargesForActiveFit()
# update the UI
self.updateItemStore(items)
self.filterItemStore()
def updateItemStore(self, items): def updateItemStore(self, items):
self.unfilteredStore = items self.unfilteredStore = items

View File

@@ -1,7 +1,7 @@
import wx import wx
from gui.cachingImageList import CachingImageList from gui.cachingImageList import CachingImageList
from gui.builtinMarketBrowser.events import RECENTLY_USED_MODULES from gui.builtinMarketBrowser.events import RECENTLY_USED_MODULES, CHARGES_FOR_FIT
from logbook import Logger from logbook import Logger
@@ -35,6 +35,9 @@ class MarketTree(wx.TreeCtrl):
# Add recently used modules node # Add recently used modules node
rumIconId = self.addImage("market_small", "gui") rumIconId = self.addImage("market_small", "gui")
self.AppendItem(self.root, _t("Recently Used Items"), rumIconId, data=RECENTLY_USED_MODULES) self.AppendItem(self.root, _t("Recently Used Items"), rumIconId, data=RECENTLY_USED_MODULES)
# Add charges for active fitting node
cffIconId = self.addImage("damagePattern_small", "gui")
self.AppendItem(self.root, _t("Charges For Active Fit"), cffIconId, data=CHARGES_FOR_FIT)
# Bind our lookup method to when the tree gets expanded # Bind our lookup method to when the tree gets expanded
self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.expandLookup) self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.expandLookup)

View File

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

View File

@@ -197,6 +197,30 @@ class SignatureRadiusColumn(GraphColumn):
SignatureRadiusColumn.register() 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): class ShieldAmountColumn(GraphColumn):
name = 'ShieldAmount' name = 'ShieldAmount'

View File

@@ -93,8 +93,6 @@ class Miscellanea(ViewColumn):
text = "{} dmg".format(formatAmount(dmg, 3, 0, 6)) text = "{} dmg".format(formatAmount(dmg, 3, 0, 6))
tooltip = "Raw damage done" tooltip = "Raw damage done"
return text, tooltip return text, tooltip
pass
elif itemGroup in ("Energy Weapon", "Hybrid Weapon", "Projectile Weapon", "Combat Drone", "Fighter Drone"): elif itemGroup in ("Energy Weapon", "Hybrid Weapon", "Projectile Weapon", "Combat Drone", "Fighter Drone"):
trackingSpeed = stuff.getModifiedItemAttr("trackingSpeed") trackingSpeed = stuff.getModifiedItemAttr("trackingSpeed")
optimalSig = stuff.getModifiedItemAttr("optimalSigRadius") optimalSig = stuff.getModifiedItemAttr("optimalSigRadius")
@@ -810,6 +808,19 @@ class Miscellanea(ViewColumn):
text = "{}".format(formatAmount(scanStr, 4, 0, 3)) text = "{}".format(formatAmount(scanStr, 4, 0, 3))
tooltip = "Scan strength at {} AU scan range".format(formatAmount(baseRange, 3, 0, 0)) tooltip = "Scan strength at {} AU scan range".format(formatAmount(baseRange, 3, 0, 0))
return text, tooltip 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: else:
return "", None return "", None
else: else:

View File

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

View File

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

View File

@@ -115,7 +115,7 @@ class CharacterEntityEditor(EntityEditor):
sChar = Character.getInstance() sChar = Character.getInstance()
if entity.alphaCloneID: if entity.alphaCloneID:
trimmed_name = re.sub('[ \(\u03B1\)]+$', '', name) trimmed_name = re.sub('[ \\(\u03B1\\)]+$', '', name)
sChar.rename(entity, trimmed_name) sChar.rename(entity, trimmed_name)
else: else:
sChar.rename(entity, name) sChar.rename(entity, name)
@@ -918,7 +918,7 @@ class SecStatusDialog(wx.Dialog):
self.m_staticText1.Wrap(-1) self.m_staticText1.Wrap(-1)
bSizer1.Add(self.m_staticText1, 1, wx.ALL | wx.EXPAND, 5) 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) bSizer1.Add(self.floatSpin, 0, wx.ALIGN_CENTER | wx.ALL, 5)
btnOk = wx.Button(self, wx.ID_OK) btnOk = wx.Button(self, wx.ID_OK)

View File

@@ -112,7 +112,7 @@ class FitBrowserLiteDialog(wx.Dialog):
return True return True
matches = [] matches = []
searchTokens = [t.lower() for t in re.split('\s+', searchPattern)] searchTokens = [t.lower() for t in re.split(r'\s+', searchPattern)]
for fit in self.allFits: for fit in self.allFits:
if isMatch(fit, searchTokens): if isMatch(fit, searchTokens):
matches.append(fit) matches.append(fit)

View File

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

@@ -146,7 +146,7 @@ class MarketBrowser(wx.Panel):
setting = self.settings.get('marketMGSearchMode') setting = self.settings.get('marketMGSearchMode')
# We turn on all meta buttons for the duration of search/recents # We turn on all meta buttons for the duration of search/recents
if setting == 1: if setting == 1:
if newMode in ('search', 'recent'): if newMode in ('search', 'recent', 'charges'):
for btn in self.metaButtons: for btn in self.metaButtons:
btn.setUserSelection(True) btn.setUserSelection(True)
if newMode == 'normal': if newMode == 'normal':

View File

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

View File

@@ -123,13 +123,14 @@ class TargetProfileEditor(AuxiliaryFrame):
ATTRIBUTES = OrderedDict([ ATTRIBUTES = OrderedDict([
('maxVelocity', (_t('Maximum speed'), 'm/s')), ('maxVelocity', (_t('Maximum speed'), 'm/s')),
('signatureRadius', (_t('Signature radius\nLeave blank for infinitely big value'), 'm')), ('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): def __init__(self, parent):
super().__init__( super().__init__(
parent, id=wx.ID_ANY, title=_t("Target Profile Editor"), resizeable=True, 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 # 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.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.block = False self.block = False
@@ -145,36 +146,29 @@ class TargetProfileEditor(AuxiliaryFrame):
contentSizer = wx.BoxSizer(wx.VERTICAL) contentSizer = wx.BoxSizer(wx.VERTICAL)
resistEditSizer = wx.FlexGridSizer(2, 6, 0, 2) resistEditSizer = wx.BoxSizer(wx.HORIZONTAL)
resistEditSizer.AddGrowableCol(0) resistEditSizer.AddStretchSpacer()
resistEditSizer.AddGrowableCol(5)
resistEditSizer.SetFlexibleDirection(wx.BOTH)
resistEditSizer.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED)
defSize = wx.Size(50, -1) defSize = wx.Size(70, -1)
for i, type_ in enumerate(self.DAMAGE_TYPES): for type_ in self.DAMAGE_TYPES:
if i % 2: leftPad = 25 if type_ != list(self.DAMAGE_TYPES)[0] else 0
style = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.LEFT
border = 25
else:
style = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT
border = 5
ttText = self.DAMAGE_TYPES[type_] ttText = self.DAMAGE_TYPES[type_]
bmp = wx.StaticBitmap(self, wx.ID_ANY, BitmapLoader.getBitmap("%s_big" % type_, "gui")) bmp = wx.StaticBitmap(self, wx.ID_ANY, BitmapLoader.getBitmap("%s_big" % type_, "gui"))
bmp.SetToolTip(wx.ToolTip(ttText)) 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 # 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)) editBox.SetToolTip(wx.ToolTip(ttText))
self.Bind(event=wx.EVT_TEXT, handler=self.OnFieldChanged, source=editBox) self.Bind(event=wx.EVT_TEXT, handler=self.OnFieldChanged, source=editBox)
setattr(self, '{}Edit'.format(type_), 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 = wx.StaticText(self, wx.ID_ANY, "%", wx.DefaultPosition, wx.DefaultSize, 0)
unit.SetToolTip(wx.ToolTip(ttText)) unit.SetToolTip(wx.ToolTip(ttText))
resistEditSizer.Add(unit, 0, wx.BOTTOM | wx.TOP | wx.ALIGN_CENTER_VERTICAL, 5) 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 = wx.BoxSizer(wx.HORIZONTAL)
miscAttrSizer.AddStretchSpacer() miscAttrSizer.AddStretchSpacer()

View File

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

View File

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

Before

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

Before

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 B

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