Compare commits
210 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89e426815d | ||
|
|
cf0e88495d | ||
|
|
7c67f18be5 | ||
|
|
9ab7e0dbac | ||
|
|
7e92b58c62 | ||
|
|
9c519b878e | ||
|
|
bb07dd8553 | ||
|
|
60555e72d8 | ||
|
|
a457d1d629 | ||
|
|
0e62b29028 | ||
|
|
8a0cb156b2 | ||
|
|
654f8ed7f5 | ||
|
|
cfa927dcd0 | ||
|
|
4383481a5f | ||
|
|
22063842f9 | ||
|
|
0e5d29a8f9 | ||
|
|
5ee16476dc | ||
|
|
2abb73eab8 | ||
|
|
547d8be21e | ||
|
|
b6d8582867 | ||
|
|
87fd358c4e | ||
|
|
0f4b082b4b | ||
|
|
59847c3756 | ||
|
|
b5d3dc6d56 | ||
|
|
3500867336 | ||
|
|
ea26d57566 | ||
|
|
45f0743c57 | ||
|
|
08d4e852d3 | ||
|
|
7edf1f93cb | ||
|
|
31cfa7c93a | ||
|
|
da1d42b578 | ||
|
|
502e5a8d8e | ||
|
|
087867bdca | ||
|
|
86ad17b075 | ||
|
|
b8ad4772b8 | ||
|
|
e0e5be5870 | ||
|
|
8b2f7c56db | ||
|
|
0ca1a675c2 | ||
|
|
6a0bfce262 | ||
|
|
362aebe658 | ||
|
|
bbe3e931f7 | ||
|
|
f5743af6a4 | ||
|
|
dbc2993fb9 | ||
|
|
219173c43e | ||
|
|
6074cfe15a | ||
|
|
f0a72f4307 | ||
|
|
ecc3f9fa7e | ||
|
|
c660e4058c | ||
|
|
7664b00b59 | ||
|
|
5721beacf5 | ||
|
|
13f3793515 | ||
|
|
f8e0520344 | ||
|
|
2c8e306dc2 | ||
|
|
7c37d8fd74 | ||
|
|
88129c0df4 | ||
|
|
fa67efd9cb | ||
|
|
38a345df93 | ||
|
|
b41ec8a5a9 | ||
|
|
4ba9bb7b99 | ||
|
|
f3c8f485b9 | ||
|
|
8d204dd173 | ||
|
|
f870d7e6d2 | ||
|
|
715997041a | ||
|
|
003191d9a4 | ||
|
|
9302d79e1d | ||
|
|
401fc671d4 | ||
|
|
fbadcf0af6 | ||
|
|
c7f600f88c | ||
|
|
229dd0ddae | ||
|
|
8883b18025 | ||
|
|
ea9b67cedd | ||
|
|
c4cf1d1ecb | ||
|
|
f76f7e85db | ||
|
|
b807e2a36b | ||
|
|
2bb35a15ce | ||
|
|
376bda7a94 | ||
|
|
a642bdc8c9 | ||
|
|
71f3e7b858 | ||
|
|
88f5ed2da5 | ||
|
|
3b9bbae0c3 | ||
|
|
6d474492ea | ||
|
|
ebc61efef8 | ||
|
|
3892dfc78c | ||
|
|
7b5c95213f | ||
|
|
1312177e0e | ||
|
|
5b0df0a9c3 | ||
|
|
954a164922 | ||
|
|
1ea6136cb2 | ||
|
|
da2b6bbb6d | ||
|
|
87007af5f8 | ||
|
|
dd5ab872b1 | ||
|
|
65fb46885a | ||
|
|
fad1e401b2 | ||
|
|
d59f8afd8a | ||
|
|
7c42d00219 | ||
|
|
32fd38ad7c | ||
|
|
777ba69ac4 | ||
|
|
9a3bde872b | ||
|
|
01354a0a83 | ||
|
|
8c3b8589d5 | ||
|
|
451b5d4312 | ||
|
|
d204e70afc | ||
|
|
ce9ce17ad2 | ||
|
|
6a7cdda91a | ||
|
|
f38c61da51 | ||
|
|
735827a25b | ||
|
|
d3fcdcbe47 | ||
|
|
a3dce73663 | ||
|
|
594b58388f | ||
|
|
fa6be2edfb | ||
|
|
1177968b02 | ||
|
|
c04b370410 | ||
|
|
038b95531d | ||
|
|
56bc5c3376 | ||
|
|
f5d8be7861 | ||
|
|
5366c23db2 | ||
|
|
d147db22f1 | ||
|
|
28ebcd2739 | ||
|
|
2f57eb6ea6 | ||
|
|
3a7ee4699e | ||
|
|
fa7f991fae | ||
|
|
0be8b429aa | ||
|
|
7d94e2de7d | ||
|
|
6d32db5827 | ||
|
|
0fbb318d8a | ||
|
|
cd013e8287 | ||
|
|
e667453c1e | ||
|
|
24e2db0f88 | ||
|
|
3c47f8c6bb | ||
|
|
42c2b3f253 | ||
|
|
71d6830ac0 | ||
|
|
ac2fb01a7c | ||
|
|
fc66c212fc | ||
|
|
9625fafda7 | ||
|
|
6b1aca4306 | ||
|
|
fd5a094304 | ||
|
|
a3142ff62f | ||
|
|
58ebfd3643 | ||
|
|
e272267842 | ||
|
|
b0256542bc | ||
|
|
9b8fd24987 | ||
|
|
9c68889a6d | ||
|
|
a4ed6e8066 | ||
|
|
9fe8163a69 | ||
|
|
9f9b496726 | ||
|
|
3b8fa68a4c | ||
|
|
9f5a649b04 | ||
|
|
fb45736c14 | ||
|
|
b8a58fc7a6 | ||
|
|
b04c521805 | ||
|
|
526695fa9a | ||
|
|
d6be77d107 | ||
|
|
907da343b1 | ||
|
|
3fadccc715 | ||
|
|
82a912858f | ||
|
|
cc1fdddc0a | ||
|
|
e314ecf887 | ||
|
|
94b1c8b029 | ||
|
|
6e3b7ff132 | ||
|
|
ff14855808 | ||
|
|
4761857b84 | ||
|
|
bb53e75bfe | ||
|
|
cc146401b6 | ||
|
|
465e43ed45 | ||
|
|
f89a8a9f87 | ||
|
|
768c417c8e | ||
|
|
494b6353b8 | ||
|
|
69ef7825af | ||
|
|
e0a71754ce | ||
|
|
b959d9dd07 | ||
|
|
2c2455a644 | ||
|
|
e2dbfb52e9 | ||
|
|
612591b59b | ||
|
|
3e00d5d4ae | ||
|
|
539be1527b | ||
|
|
d00db1e167 | ||
|
|
fc6d5fea48 | ||
|
|
ecf4d6c0de | ||
|
|
f5f7f92166 | ||
|
|
f5d7e754a6 | ||
|
|
be85080d8d | ||
|
|
6ed34be118 | ||
|
|
59dceadfee | ||
|
|
2a74c779c8 | ||
|
|
f63331454b | ||
|
|
d105748348 | ||
|
|
cdd7077346 | ||
|
|
f965497186 | ||
|
|
6b06b5b46c | ||
|
|
d1e19406d0 | ||
|
|
2a3c586be6 | ||
|
|
23f014c32e | ||
|
|
0ac6a14ebb | ||
|
|
128de58b83 | ||
|
|
47eb8d422f | ||
|
|
38ddf83da4 | ||
|
|
58142a8641 | ||
|
|
d86f0f40ac | ||
|
|
62944e4ce3 | ||
|
|
812201d8b3 | ||
|
|
52abd5620f | ||
|
|
e47112a1f2 | ||
|
|
00a6835f50 | ||
|
|
27101732ca | ||
|
|
ed48a8b5d0 | ||
|
|
5a6fe373f1 | ||
|
|
3c6eb6d054 | ||
|
|
dc997f0dc4 | ||
|
|
14afc83e86 | ||
|
|
216dd2a787 |
@@ -17,7 +17,7 @@ for:
|
||||
# init:
|
||||
# - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e -
|
||||
install:
|
||||
- sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y update
|
||||
- sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y update --allow-releaseinfo-change
|
||||
# AppImage dependencies
|
||||
- sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install libfuse2
|
||||
# Preparation script dependencies
|
||||
@@ -25,6 +25,7 @@ for:
|
||||
before_build:
|
||||
# Prepare pyfa data
|
||||
- sh: find locale/ -type f -name "*.po" -exec msgen "{}" -o "{}" \;
|
||||
- sh: pyenv global system
|
||||
- sh: python3 -B scripts/compile_lang.py
|
||||
- sh: python3 -B scripts/dump_crowdin_progress.py
|
||||
- sh: python3 -B db_update.py
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.7
|
||||
- Python 3.11
|
||||
- Git CLI installed
|
||||
- Python, pip and git are all available as command-line commands (add to the path if needed)
|
||||
|
||||
|
||||
25
config.py
@@ -9,6 +9,7 @@ import hashlib
|
||||
from eos.const import FittingSlot
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
from collections import namedtuple
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
@@ -44,9 +45,16 @@ experimentalFeatures = None
|
||||
version = None
|
||||
language = None
|
||||
|
||||
API_CLIENT_ID = '095d8cd841ac40b581330919b49fe746'
|
||||
ApiServer = namedtuple('ApiBase', ['name', 'sso', 'esi', 'client_id', 'callback', 'supports_auto_login'])
|
||||
supported_servers = {
|
||||
"Tranquility": ApiServer("Tranquility", "login.eveonline.com", "esi.evetech.net", '095d8cd841ac40b581330919b49fe746', 'https://pyfa-org.github.io/Pyfa/callback', True),
|
||||
# No point having SISI: https://developers.eveonline.com/blog/article/removing-datasource-singularity
|
||||
# "Singularity": ApiServer("Singularity", "sisilogin.testeveonline.com", "esi.evetech.net", 'b9c3cc79448f449ab17f3aebd018842e', 'https://pyfa-org.github.io/Pyfa/callback'),
|
||||
"Serenity": ApiServer("Serenity", "login.evepc.163.com", "ali-esi.evepc.163.com", 'bc90aa496a404724a93f41b4f4e97761', 'https://ali-esi.evepc.163.com/ui/oauth2-redirect.html', False)
|
||||
}
|
||||
|
||||
SSO_LOGOFF_SERENITY='https://login.evepc.163.com/account/logoff'
|
||||
ESI_CACHE = 'esi_cache'
|
||||
SSO_CALLBACK = 'https://pyfa-org.github.io/Pyfa/callback'
|
||||
|
||||
LOGLEVEL_MAP = {
|
||||
"critical": CRITICAL,
|
||||
@@ -58,13 +66,22 @@ LOGLEVEL_MAP = {
|
||||
|
||||
CATALOG = 'lang'
|
||||
|
||||
|
||||
slotColourMapDark = {
|
||||
FittingSlot.LOW: wx.Colour(44, 36, 19), # yellow = low slots 24/13
|
||||
FittingSlot.MED: wx.Colour(28, 39, 51), # blue = mid slots 8.1/9.5
|
||||
FittingSlot.HIGH: wx.Colour(53, 31, 34), # red = high slots 6.5/11.5
|
||||
FittingSlot.RIG: '',
|
||||
FittingSlot.SUBSYSTEM: ''}
|
||||
errColorDark = wx.Colour(70, 20, 20)
|
||||
slotColourMap = {
|
||||
FittingSlot.LOW: wx.Colour(250, 235, 204), # yellow = low slots
|
||||
FittingSlot.MED: wx.Colour(188, 215, 241), # blue = mid slots
|
||||
FittingSlot.HIGH: wx.Colour(235, 204, 209), # red = high slots
|
||||
FittingSlot.RIG: '',
|
||||
FittingSlot.SUBSYSTEM: ''
|
||||
}
|
||||
FittingSlot.SUBSYSTEM: ''}
|
||||
errColor = wx.Colour(204, 51, 51)
|
||||
|
||||
|
||||
def getClientSecret():
|
||||
return clientHash
|
||||
|
||||
19
db_update.py
@@ -141,14 +141,14 @@ def update_db():
|
||||
(row['typeName_en-us'].startswith('Civilian') and "Shuttle" not in row['typeName_en-us'])
|
||||
or row['typeName_en-us'] == 'Capsule'
|
||||
or row['groupID'] == 4033 # destructible effect beacons
|
||||
or re.match('AIR .+Booster.*', row['typeName_en-us'])
|
||||
or re.match(r'AIR .+Booster.*', row['typeName_en-us'])
|
||||
):
|
||||
row['published'] = True
|
||||
# Nearly useless and clutter search results too much
|
||||
elif (
|
||||
row['typeName_en-us'].startswith('Limited Synth ')
|
||||
or row['typeName_en-us'].startswith('Expired ')
|
||||
or re.match('Mining Blitz .+ Booster Dose .+', row['typeName_en-us'])
|
||||
or re.match(r'Mining Blitz .+ Booster Dose .+', row['typeName_en-us'])
|
||||
or row['typeName_en-us'].endswith(' Filament') and (
|
||||
"'Needlejack'" not in row['typeName_en-us'] and
|
||||
"'Devana'" not in row['typeName_en-us'] and
|
||||
@@ -544,7 +544,7 @@ def update_db():
|
||||
continue
|
||||
typeName = row.get('typeName_en-us', '')
|
||||
# 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:
|
||||
implantSets.setdefault((m.group('grade'), m.group('set')), set()).add(row['typeID'])
|
||||
# Special set matching
|
||||
@@ -651,6 +651,16 @@ def update_db():
|
||||
effect.effectName = effectName
|
||||
item.effects[effectName] = effect
|
||||
|
||||
def hardcodeSuppressionTackleRange():
|
||||
beaconTypeID = 79839
|
||||
attrMap = {
|
||||
'warfareBuff1ID': 2405,
|
||||
'warfareBuff1Value': 10}
|
||||
effectMap = {100000: 'pyfaCustomSuppressionTackleRange'}
|
||||
_hardcodeAttribs(beaconTypeID, attrMap)
|
||||
_hardcodeEffects(beaconTypeID, effectMap)
|
||||
|
||||
|
||||
def hardcodeShapash():
|
||||
shapashTypeID = 1000000
|
||||
_copyItem(srcName='Utu', tgtTypeID=shapashTypeID, tgtName='Shapash')
|
||||
@@ -794,8 +804,7 @@ def update_db():
|
||||
_hardcodeAttribs(cybeleTypeID, attrMap)
|
||||
_hardcodeEffects(cybeleTypeID, effectMap)
|
||||
|
||||
# hardcodeShapash()
|
||||
# hardcodeCybele()
|
||||
hardcodeSuppressionTackleRange()
|
||||
|
||||
eos.db.gamedata_session.commit()
|
||||
eos.db.gamedata_engine.execute('VACUUM')
|
||||
|
||||
@@ -31,6 +31,10 @@ AppDir:
|
||||
- libgtk-3-0
|
||||
- librsvg2-common # GTK3 recommendation; without it, search in char editor crashes
|
||||
- libwebkit2gtk-4.0-37 # Needed for wx's HTML lib
|
||||
# Unknown
|
||||
- libpcre2-32-0 # https://github.com/pyfa-org/Pyfa/issues/2572
|
||||
- libnotify4 # https://github.com/pyfa-org/Pyfa/issues/2598
|
||||
- libwayland-client0 # https://github.com/pyfa-org/Pyfa/issues/2600
|
||||
exclude:
|
||||
- hicolor-icon-theme
|
||||
- humanity-icon-theme
|
||||
|
||||
@@ -14,7 +14,7 @@ with open("version.yml", 'r') as file:
|
||||
os.environ["PYFA_DIST_DIR"] = os.path.join(os.getcwd(), 'dist')
|
||||
|
||||
os.environ["PYFA_VERSION"] = version
|
||||
iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
|
||||
iscc = r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
|
||||
|
||||
source = os.path.join(os.environ["PYFA_DIST_DIR"], "pyfa")
|
||||
|
||||
|
||||
@@ -10,10 +10,15 @@
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows Vista -->
|
||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
<!-- Windows 10 and Windows 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
@@ -21,7 +21,7 @@ for modName in iterNamespace(__name__, __path__):
|
||||
# loop through python files, extracting update number and function, and
|
||||
# adding it to a list
|
||||
modname_tail = modName.rsplit('.', 1)[-1]
|
||||
m = re.match("^upgrade(?P<index>\d+)$", modname_tail)
|
||||
m = re.match(r"^upgrade(?P<index>\d+)$", modname_tail)
|
||||
if not m:
|
||||
continue
|
||||
index = int(m.group("index"))
|
||||
|
||||
@@ -7,7 +7,7 @@ Migration 1
|
||||
loaded as they no longer exist in the database. We therefore replace these
|
||||
modules with their new replacements
|
||||
|
||||
Based on http://community.eveonline.com/news/patch-notes/patch-notes-for-oceanus/
|
||||
Based on https://www.eveonline.com/news/view/patch-notes-for-oceanus
|
||||
and output of itemDiff.py
|
||||
"""
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Migration 25
|
||||
|
||||
- Converts T3C fitting configurations based on the spreadsheet noted here:
|
||||
https://community.eveonline.com/news/patch-notes/patch-notes-for-july-2017-release
|
||||
https://www.eveonline.com/news/view/patch-notes-for-july-2017-release
|
||||
|
||||
(csv copies can be found on the pyfa repo in case the official documents are deleted)
|
||||
|
||||
@@ -4228,8 +4228,8 @@ def upgrade(saveddata_engine):
|
||||
# We don't have a conversion for this. I don't think this will ever happen, but who knows
|
||||
continue
|
||||
|
||||
# It doesn't actully matter which old module is replaced with which new module, so we don't have to worry
|
||||
# about module position or anything like that. Just doe a straight up record UPDATE
|
||||
# It doesn't actually matter which old module is replaced with which new module, so we don't have to worry
|
||||
# about module position or anything like that. Just do a straight up record UPDATE
|
||||
for i, old in enumerate(oldModules[:4]):
|
||||
saveddata_engine.execute("UPDATE modules SET itemID = ? WHERE ID = ?", (newModules[i], old[0]))
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Migration 4
|
||||
from database), which causes pyfa to crash. We therefore replace these
|
||||
modules with their new replacements
|
||||
|
||||
Based on http://community.eveonline.com/news/patch-notes/patch-notes-for-proteus/
|
||||
Based on https://www.eveonline.com/news/view/patch-notes-for-proteus
|
||||
and output of itemDiff.py
|
||||
"""
|
||||
|
||||
|
||||
36
eos/db/migrations/upgrade47.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Migration 46
|
||||
|
||||
- add support for server selection for SSO characters
|
||||
"""
|
||||
import sqlalchemy
|
||||
|
||||
tmpTable = """
|
||||
CREATE TABLE ssoCharacterTemp (
|
||||
ID INTEGER NOT NULL,
|
||||
client VARCHAR NOT NULL,
|
||||
characterID INTEGER NOT NULL,
|
||||
characterName VARCHAR NOT NULL,
|
||||
refreshToken VARCHAR NOT NULL,
|
||||
accessToken VARCHAR NOT NULL,
|
||||
accessTokenExpires DATETIME NOT NULL,
|
||||
created DATETIME,
|
||||
modified DATETIME,
|
||||
server VARCHAR,
|
||||
PRIMARY KEY (ID),
|
||||
CONSTRAINT "uix_client_server_characterID" UNIQUE (client, server, characterID),
|
||||
CONSTRAINT "uix_client_server_characterName" UNIQUE (client, server, characterName)
|
||||
)
|
||||
"""
|
||||
|
||||
def upgrade(saveddata_engine):
|
||||
try:
|
||||
saveddata_engine.execute("SELECT server FROM ssoCharacter LIMIT 1")
|
||||
except sqlalchemy.exc.DatabaseError:
|
||||
saveddata_engine.execute(tmpTable)
|
||||
saveddata_engine.execute(
|
||||
"INSERT INTO ssoCharacterTemp (ID, client, characterID, characterName, refreshToken, accessToken, accessTokenExpires, created, modified, server) "
|
||||
"SELECT ID, client, characterID, characterName, refreshToken, accessToken, accessTokenExpires, created, modified, 'Tranquility' "
|
||||
"FROM ssoCharacter")
|
||||
saveddata_engine.execute("DROP TABLE ssoCharacter")
|
||||
saveddata_engine.execute("ALTER TABLE ssoCharacterTemp RENAME TO ssoCharacter")
|
||||
15
eos/db/migrations/upgrade48.py
Normal 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")
|
||||
15
eos/db/migrations/upgrade49.py
Normal 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;")
|
||||
@@ -40,18 +40,18 @@ characters_table = Table("characters", saveddata_meta,
|
||||
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now))
|
||||
|
||||
sso_table = Table("ssoCharacter", saveddata_meta,
|
||||
Column("ID", Integer, primary_key=True),
|
||||
Column("client", String, nullable=False),
|
||||
Column("characterID", Integer, nullable=False),
|
||||
Column("characterName", String, nullable=False),
|
||||
Column("refreshToken", String, nullable=False),
|
||||
Column("accessToken", String, nullable=False),
|
||||
Column("accessTokenExpires", DateTime, nullable=False),
|
||||
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
|
||||
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
|
||||
UniqueConstraint('client', 'characterID', name='uix_client_characterID'),
|
||||
UniqueConstraint('client', 'characterName', name='uix_client_characterName')
|
||||
)
|
||||
Column("ID", Integer, primary_key=True),
|
||||
Column("client", String, nullable=False),
|
||||
Column("characterID", Integer, nullable=False),
|
||||
Column("characterName", String, nullable=False),
|
||||
Column("server", String, nullable=False),
|
||||
Column("refreshToken", String, nullable=False),
|
||||
Column("accessToken", String, nullable=False),
|
||||
Column("accessTokenExpires", DateTime, nullable=False),
|
||||
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
|
||||
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
|
||||
UniqueConstraint('client', 'server', 'characterID', name='uix_client_server_characterID'),
|
||||
UniqueConstraint('client', 'server', 'characterName', name='uix_client_server_characterName'))
|
||||
|
||||
sso_character_map_table = Table("ssoCharacterMap", saveddata_meta,
|
||||
Column("characterID", ForeignKey("characters.ID"), primary_key=True),
|
||||
|
||||
@@ -63,7 +63,8 @@ fits_table = Table("fits", saveddata_meta,
|
||||
Column("ignoreRestrictions", Boolean, default=0),
|
||||
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
|
||||
Column("modified", DateTime, nullable=True, default=datetime.datetime.now, onupdate=datetime.datetime.now),
|
||||
Column("systemSecurity", Integer, nullable=True)
|
||||
Column("systemSecurity", Integer, nullable=True),
|
||||
Column("pilotSecurity", Float, nullable=True),
|
||||
)
|
||||
|
||||
projectedFits_table = Table("projectedFits", saveddata_meta,
|
||||
|
||||
@@ -493,9 +493,12 @@ def getSsoCharacters(clientHash, eager=None):
|
||||
|
||||
|
||||
@cachedQuery(SsoCharacter, 1, "lookfor", "clientHash")
|
||||
def getSsoCharacter(lookfor, clientHash, eager=None):
|
||||
def getSsoCharacter(lookfor, clientHash, server=None, eager=None):
|
||||
filter = SsoCharacter.client == clientHash
|
||||
|
||||
if server is not None:
|
||||
filter = and_(filter, SsoCharacter.server == server)
|
||||
|
||||
if isinstance(lookfor, int):
|
||||
filter = and_(filter, SsoCharacter.ID == lookfor)
|
||||
elif isinstance(lookfor, str):
|
||||
|
||||
@@ -37,6 +37,7 @@ targetProfiles_table = Table(
|
||||
Column('maxVelocity', Float, nullable=True),
|
||||
Column('signatureRadius', Float, nullable=True),
|
||||
Column('radius', Float, nullable=True),
|
||||
Column('hp', Float, nullable=True),
|
||||
Column('ownerID', ForeignKey('users.ID'), nullable=True),
|
||||
Column('created', DateTime, nullable=True, default=datetime.datetime.now),
|
||||
Column('modified', DateTime, nullable=True, onupdate=datetime.datetime.now))
|
||||
@@ -48,4 +49,5 @@ mapper(
|
||||
'rawName': targetProfiles_table.c.name,
|
||||
'_maxVelocity': targetProfiles_table.c.maxVelocity,
|
||||
'_signatureRadius': targetProfiles_table.c.signatureRadius,
|
||||
'_radius': targetProfiles_table.c.radius})
|
||||
'_radius': targetProfiles_table.c.radius,
|
||||
'_hp': targetProfiles_table.c.hp})
|
||||
|
||||
1436
eos/effects.py
@@ -343,7 +343,9 @@ class Item(EqBase):
|
||||
500018: "mordu",
|
||||
500019: "sansha",
|
||||
500020: "serpentis",
|
||||
500026: "triglavian"
|
||||
500026: "triglavian",
|
||||
500027: "upwell",
|
||||
500029: "deathless",
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -351,11 +353,7 @@ class Item(EqBase):
|
||||
if self.__race is None:
|
||||
|
||||
try:
|
||||
if (
|
||||
self.category.name == 'Structure' or
|
||||
# Here until CCP puts their shit together
|
||||
self.name in ("Thunderchild", "Stormbringer", "Skybreaker")
|
||||
):
|
||||
if self.category.name == 'Structure':
|
||||
self.__race = "upwell"
|
||||
else:
|
||||
self.__race = self.factionMap[self.factionID]
|
||||
@@ -377,7 +375,8 @@ class Item(EqBase):
|
||||
16 : "jove",
|
||||
32 : "sansha", # Incrusion Sansha
|
||||
128: "ore",
|
||||
135: "triglavian"
|
||||
135: "triglavian",
|
||||
168: "upwell",
|
||||
}
|
||||
# Race is None by default
|
||||
race = None
|
||||
@@ -571,13 +570,18 @@ class DynamicItem(EqBase):
|
||||
@property
|
||||
def shortName(self):
|
||||
name = self.item.customName
|
||||
keywords = ('Decayed', 'Gravid', 'Unstable', 'Radical')
|
||||
keywords = (
|
||||
'Decayed', 'Glorified Decayed',
|
||||
'Gravid', 'Glorified Gravid',
|
||||
'Unstable', 'Glorified Unstable',
|
||||
'Radical', 'Glorified Radical')
|
||||
for kw in keywords:
|
||||
if name.startswith(f'{kw} '):
|
||||
name = kw
|
||||
m = re.match('(?P<mutagrade>\S+) (?P<dronetype>\S+) Drone (?P<mutatype>\S+) Mutaplasmid', name)
|
||||
m = re.match(r'(?P<mutagrade>(Glorified )?\S+) (?P<dronetype>\S+) Drone (?P<mutatype>\S+) Mutaplasmid', name)
|
||||
if m:
|
||||
name = '{} {}'.format(m.group('mutagrade'), m.group('mutatype'))
|
||||
name = name.replace('Glorified ', 'Gl. ')
|
||||
return name
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import math
|
||||
|
||||
from copy import deepcopy
|
||||
from logbook import Logger
|
||||
from sqlalchemy.orm import reconstructor, validates
|
||||
|
||||
@@ -161,7 +162,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
|
||||
|
||||
def getVolleyParameters(self, targetProfile=None):
|
||||
if not self.dealsDamage or self.amountActive <= 0:
|
||||
return {0: DmgTypes(0, 0, 0, 0)}
|
||||
return {0: DmgTypes.default()}
|
||||
if self.__baseVolley is None:
|
||||
dmgGetter = self.getModifiedChargeAttr if self.hasAmmo else self.getModifiedItemAttr
|
||||
dmgMult = self.amountActive * (self.getModifiedItemAttr("damageMultiplier", 1))
|
||||
@@ -170,11 +171,8 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
|
||||
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
|
||||
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
|
||||
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
|
||||
volley = DmgTypes(
|
||||
em=self.__baseVolley.em * (1 - getattr(targetProfile, "emAmount", 0)),
|
||||
thermal=self.__baseVolley.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)),
|
||||
kinetic=self.__baseVolley.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)),
|
||||
explosive=self.__baseVolley.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)))
|
||||
volley = deepcopy(self.__baseVolley)
|
||||
volley.profile = targetProfile
|
||||
return {0: volley}
|
||||
|
||||
def getVolley(self, targetProfile=None):
|
||||
@@ -183,16 +181,12 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
|
||||
def getDps(self, targetProfile=None):
|
||||
volley = self.getVolley(targetProfile=targetProfile)
|
||||
if not volley:
|
||||
return DmgTypes(0, 0, 0, 0)
|
||||
return DmgTypes.default()
|
||||
cycleParams = self.getCycleParameters()
|
||||
if cycleParams is None:
|
||||
return DmgTypes(0, 0, 0, 0)
|
||||
return DmgTypes.default()
|
||||
dpsFactor = 1 / (cycleParams.averageTime / 1000)
|
||||
dps = DmgTypes(
|
||||
em=volley.em * dpsFactor,
|
||||
thermal=volley.thermal * dpsFactor,
|
||||
kinetic=volley.kinetic * dpsFactor,
|
||||
explosive=volley.explosive * dpsFactor)
|
||||
dps = volley * dpsFactor
|
||||
return dps
|
||||
|
||||
def isRemoteRepping(self, ignoreState=False):
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import math
|
||||
|
||||
from copy import deepcopy
|
||||
from logbook import Logger
|
||||
from sqlalchemy.orm import reconstructor, validates
|
||||
|
||||
@@ -198,16 +199,14 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
for ability in self.abilities:
|
||||
# Not passing resists here as we want to calculate and store base volley
|
||||
self.__baseVolley[ability.effectID] = {0: ability.getVolley()}
|
||||
adjustedVolley = {}
|
||||
adjustedVolleys = {}
|
||||
for effectID, effectData in self.__baseVolley.items():
|
||||
adjustedVolley[effectID] = {}
|
||||
for volleyTime, volleyValue in effectData.items():
|
||||
adjustedVolley[effectID][volleyTime] = DmgTypes(
|
||||
em=volleyValue.em * (1 - getattr(targetProfile, "emAmount", 0)),
|
||||
thermal=volleyValue.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)),
|
||||
kinetic=volleyValue.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)),
|
||||
explosive=volleyValue.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)))
|
||||
return adjustedVolley
|
||||
adjustedVolleys[effectID] = {}
|
||||
for volleyTime, baseVolley in effectData.items():
|
||||
adjustedVolley = deepcopy(baseVolley)
|
||||
adjustedVolley.profile = targetProfile
|
||||
adjustedVolleys[effectID][volleyTime] = adjustedVolley
|
||||
return adjustedVolleys
|
||||
|
||||
def getVolleyPerEffect(self, targetProfile=None):
|
||||
volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile)
|
||||
@@ -218,28 +217,16 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
|
||||
def getVolley(self, targetProfile=None):
|
||||
volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile)
|
||||
em = 0
|
||||
therm = 0
|
||||
kin = 0
|
||||
exp = 0
|
||||
volley = DmgTypes.default()
|
||||
for volleyData in volleyParams.values():
|
||||
em += volleyData[0].em
|
||||
therm += volleyData[0].thermal
|
||||
kin += volleyData[0].kinetic
|
||||
exp += volleyData[0].explosive
|
||||
return DmgTypes(em, therm, kin, exp)
|
||||
volley += volleyData[0]
|
||||
return volley
|
||||
|
||||
def getDps(self, targetProfile=None):
|
||||
em = 0
|
||||
thermal = 0
|
||||
kinetic = 0
|
||||
explosive = 0
|
||||
for dps in self.getDpsPerEffect(targetProfile=targetProfile).values():
|
||||
em += dps.em
|
||||
thermal += dps.thermal
|
||||
kinetic += dps.kinetic
|
||||
explosive += dps.explosive
|
||||
return DmgTypes(em=em, thermal=thermal, kinetic=kinetic, explosive=explosive)
|
||||
dps = DmgTypes.default()
|
||||
for subdps in self.getDpsPerEffect(targetProfile=targetProfile).values():
|
||||
dps += subdps
|
||||
return dps
|
||||
|
||||
def getDpsPerEffect(self, targetProfile=None):
|
||||
if not self.active or self.amount <= 0:
|
||||
|
||||
@@ -116,7 +116,7 @@ class FighterAbility:
|
||||
|
||||
def getVolley(self, targetProfile=None):
|
||||
if not self.dealsDamage or not self.active:
|
||||
return DmgTypes(0, 0, 0, 0)
|
||||
return DmgTypes.default()
|
||||
if self.attrPrefix == "fighterAbilityLaunchBomb":
|
||||
em = self.fighter.getModifiedChargeAttr("emDamage", 0)
|
||||
therm = self.fighter.getModifiedChargeAttr("thermalDamage", 0)
|
||||
@@ -128,24 +128,17 @@ class FighterAbility:
|
||||
kin = self.fighter.getModifiedItemAttr("{}DamageKin".format(self.attrPrefix), 0)
|
||||
exp = self.fighter.getModifiedItemAttr("{}DamageExp".format(self.attrPrefix), 0)
|
||||
dmgMult = self.fighter.amount * self.fighter.getModifiedItemAttr("{}DamageMultiplier".format(self.attrPrefix), 1)
|
||||
volley = DmgTypes(
|
||||
em=em * dmgMult * (1 - getattr(targetProfile, "emAmount", 0)),
|
||||
thermal=therm * dmgMult * (1 - getattr(targetProfile, "thermalAmount", 0)),
|
||||
kinetic=kin * dmgMult * (1 - getattr(targetProfile, "kineticAmount", 0)),
|
||||
explosive=exp * dmgMult * (1 - getattr(targetProfile, "explosiveAmount", 0)))
|
||||
volley = DmgTypes(em=em * dmgMult, thermal=therm * dmgMult, kinetic=kin * dmgMult, explosive=exp * dmgMult)
|
||||
volley.profile = targetProfile
|
||||
return volley
|
||||
|
||||
def getDps(self, targetProfile=None, cycleTimeOverride=None):
|
||||
volley = self.getVolley(targetProfile=targetProfile)
|
||||
if not volley:
|
||||
return DmgTypes(0, 0, 0, 0)
|
||||
return DmgTypes.default()
|
||||
cycleTime = cycleTimeOverride if cycleTimeOverride is not None else self.cycleTime
|
||||
dpsFactor = 1 / (cycleTime / 1000)
|
||||
dps = DmgTypes(
|
||||
em=volley.em * dpsFactor,
|
||||
thermal=volley.thermal * dpsFactor,
|
||||
kinetic=volley.kinetic * dpsFactor,
|
||||
explosive=volley.explosive * dpsFactor)
|
||||
dps = volley * dpsFactor
|
||||
return dps
|
||||
|
||||
def clear(self):
|
||||
|
||||
@@ -1688,27 +1688,33 @@ class Fit:
|
||||
self.__droneWaste = droneWaste
|
||||
|
||||
def calculateWeaponDmgStats(self, spoolOptions):
|
||||
weaponVolley = DmgTypes(0, 0, 0, 0)
|
||||
weaponDps = DmgTypes(0, 0, 0, 0)
|
||||
weaponVolley = DmgTypes.default()
|
||||
weaponDps = DmgTypes.default()
|
||||
|
||||
for mod in self.modules:
|
||||
weaponVolley += mod.getVolley(spoolOptions=spoolOptions, targetProfile=self.targetProfile)
|
||||
weaponDps += mod.getDps(spoolOptions=spoolOptions, targetProfile=self.targetProfile)
|
||||
weaponVolley += mod.getVolley(spoolOptions=spoolOptions)
|
||||
weaponDps += mod.getDps(spoolOptions=spoolOptions)
|
||||
|
||||
weaponVolley.profile = self.targetProfile
|
||||
weaponDps.profile = self.targetProfile
|
||||
|
||||
self.__weaponVolleyMap[spoolOptions] = weaponVolley
|
||||
self.__weaponDpsMap[spoolOptions] = weaponDps
|
||||
|
||||
def calculateDroneDmgStats(self):
|
||||
droneVolley = DmgTypes(0, 0, 0, 0)
|
||||
droneDps = DmgTypes(0, 0, 0, 0)
|
||||
droneVolley = DmgTypes.default()
|
||||
droneDps = DmgTypes.default()
|
||||
|
||||
for drone in self.drones:
|
||||
droneVolley += drone.getVolley(targetProfile=self.targetProfile)
|
||||
droneDps += drone.getDps(targetProfile=self.targetProfile)
|
||||
droneVolley += drone.getVolley()
|
||||
droneDps += drone.getDps()
|
||||
|
||||
for fighter in self.fighters:
|
||||
droneVolley += fighter.getVolley(targetProfile=self.targetProfile)
|
||||
droneDps += fighter.getDps(targetProfile=self.targetProfile)
|
||||
droneVolley += fighter.getVolley()
|
||||
droneDps += fighter.getDps()
|
||||
|
||||
droneVolley.profile = self.targetProfile
|
||||
droneDps.profile = self.targetProfile
|
||||
|
||||
self.__droneDps = droneDps
|
||||
self.__droneVolley = droneVolley
|
||||
@@ -1743,6 +1749,18 @@ class Fit:
|
||||
secstatus = FitSystemSecurity.NULLSEC
|
||||
return secstatus
|
||||
|
||||
def getPilotSecurity(self, low_limit=-10, high_limit=5):
|
||||
secstatus = self.pilotSecurity
|
||||
# Not defined -> use character SS, with 0.0 fallback if it fails
|
||||
if secstatus is None:
|
||||
try:
|
||||
secstatus = self.character.secStatus
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except:
|
||||
secstatus = 0
|
||||
return max(low_limit, min(high_limit, secstatus))
|
||||
|
||||
def activeModulesIter(self):
|
||||
for mod in self.modules:
|
||||
if mod.state >= FittingModuleState.ACTIVE:
|
||||
@@ -1824,6 +1842,7 @@ class Fit:
|
||||
fitCopy.targetProfile = self.targetProfile
|
||||
fitCopy.implantLocation = self.implantLocation
|
||||
fitCopy.systemSecurity = self.systemSecurity
|
||||
fitCopy.pilotSecurity = self.pilotSecurity
|
||||
fitCopy.notes = self.notes
|
||||
|
||||
for i in self.modules:
|
||||
|
||||
@@ -33,7 +33,7 @@ from eos.utils.cycles import CycleInfo, CycleSequence
|
||||
from eos.utils.default import DEFAULT
|
||||
from eos.utils.float import floatUnerr
|
||||
from eos.utils.spoolSupport import calculateSpoolup, resolveSpoolOptions
|
||||
from eos.utils.stats import DmgTypes, RRTypes
|
||||
from eos.utils.stats import BreacherInfo, DmgTypes, RRTypes
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
@@ -453,6 +453,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def isBreacher(self):
|
||||
return self.charge and 'dotMissileLaunching' in self.charge.effects
|
||||
|
||||
def canDealDamage(self, ignoreState=False):
|
||||
if self.isEmpty:
|
||||
return False
|
||||
@@ -469,75 +473,77 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
|
||||
|
||||
def getVolleyParameters(self, spoolOptions=None, targetProfile=None, ignoreState=False):
|
||||
if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState):
|
||||
return {0: DmgTypes(0, 0, 0, 0)}
|
||||
return {0: DmgTypes.default()}
|
||||
if self.__baseVolley is None:
|
||||
self.__baseVolley = {}
|
||||
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
|
||||
dmgMult = self.getModifiedItemAttr("damageMultiplier", 1)
|
||||
# Some delay attributes have non-0 default value, so we have to pick according to effects
|
||||
if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(self.item.effects):
|
||||
dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0)
|
||||
elif {'doomsdayBeamDOT', 'doomsdaySlash', 'doomsdayConeDOT', 'debuffLance'}.intersection(self.item.effects):
|
||||
dmgDelay = self.getModifiedItemAttr("doomsdayWarningDuration", 0)
|
||||
if self.isBreacher:
|
||||
dmgDelay = 1
|
||||
subcycles = math.floor(self.getModifiedChargeAttr("dotDuration", 0) / 1000)
|
||||
breacher_info = BreacherInfo(
|
||||
absolute=self.getModifiedChargeAttr("dotMaxDamagePerTick", 0),
|
||||
relative=self.getModifiedChargeAttr("dotMaxHPPercentagePerTick", 0) / 100)
|
||||
for i in range(subcycles):
|
||||
volley = DmgTypes.default()
|
||||
volley.add_breacher(dmgDelay + i, breacher_info)
|
||||
self.__baseVolley[dmgDelay + i] = volley
|
||||
else:
|
||||
dmgDelay = 0
|
||||
dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0)
|
||||
dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0)
|
||||
# Reaper DD can damage each target only once
|
||||
if dmgDuration != 0 and dmgSubcycle != 0 and 'doomsdaySlash' not in self.item.effects:
|
||||
subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle))
|
||||
else:
|
||||
subcycles = 1
|
||||
for i in range(subcycles):
|
||||
self.__baseVolley[dmgDelay + dmgSubcycle * i] = DmgTypes(
|
||||
em=(dmgGetter("emDamage", 0)) * dmgMult,
|
||||
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
|
||||
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
|
||||
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
|
||||
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
|
||||
dmgMult = self.getModifiedItemAttr("damageMultiplier", 1)
|
||||
# Some delay attributes have non-0 default value, so we have to pick according to effects
|
||||
if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(self.item.effects):
|
||||
dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0)
|
||||
elif {'doomsdayBeamDOT', 'doomsdaySlash', 'doomsdayConeDOT', 'debuffLance'}.intersection(self.item.effects):
|
||||
dmgDelay = self.getModifiedItemAttr("doomsdayWarningDuration", 0)
|
||||
else:
|
||||
dmgDelay = 0
|
||||
dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0)
|
||||
dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0)
|
||||
# Reaper DD can damage each target only once
|
||||
if dmgDuration != 0 and dmgSubcycle != 0 and 'doomsdaySlash' not in self.item.effects:
|
||||
subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle))
|
||||
else:
|
||||
subcycles = 1
|
||||
for i in range(subcycles):
|
||||
self.__baseVolley[dmgDelay + dmgSubcycle * i] = DmgTypes(
|
||||
em=(dmgGetter("emDamage", 0)) * dmgMult,
|
||||
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
|
||||
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
|
||||
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
|
||||
spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self)
|
||||
spoolBoost = calculateSpoolup(
|
||||
self.getModifiedItemAttr("damageMultiplierBonusMax", 0),
|
||||
self.getModifiedItemAttr("damageMultiplierBonusPerCycle", 0),
|
||||
self.rawCycleTime / 1000, spoolType, spoolAmount)[0]
|
||||
spoolMultiplier = 1 + spoolBoost
|
||||
adjustedVolley = {}
|
||||
for volleyTime, volleyValue in self.__baseVolley.items():
|
||||
adjustedVolley[volleyTime] = DmgTypes(
|
||||
em=volleyValue.em * spoolMultiplier * (1 - getattr(targetProfile, "emAmount", 0)),
|
||||
thermal=volleyValue.thermal * spoolMultiplier * (1 - getattr(targetProfile, "thermalAmount", 0)),
|
||||
kinetic=volleyValue.kinetic * spoolMultiplier * (1 - getattr(targetProfile, "kineticAmount", 0)),
|
||||
explosive=volleyValue.explosive * spoolMultiplier * (1 - getattr(targetProfile, "explosiveAmount", 0)))
|
||||
return adjustedVolley
|
||||
adjustedVolleys = {}
|
||||
for volleyTime, baseVolley in self.__baseVolley.items():
|
||||
adjustedVolley = baseVolley * spoolMultiplier
|
||||
adjustedVolley.profile = targetProfile
|
||||
adjustedVolleys[volleyTime] = adjustedVolley
|
||||
return adjustedVolleys
|
||||
|
||||
def getVolley(self, spoolOptions=None, targetProfile=None, ignoreState=False):
|
||||
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
|
||||
if len(volleyParams) == 0:
|
||||
return DmgTypes(0, 0, 0, 0)
|
||||
return DmgTypes.default()
|
||||
return volleyParams[min(volleyParams)]
|
||||
|
||||
def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False, getSpreadDPS=False):
|
||||
dmgDuringCycle = DmgTypes(0, 0, 0, 0)
|
||||
def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False):
|
||||
dps = DmgTypes.default()
|
||||
cycleParams = self.getCycleParameters()
|
||||
if cycleParams is None:
|
||||
return dmgDuringCycle
|
||||
return dps
|
||||
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
|
||||
avgCycleTime = cycleParams.averageTime
|
||||
if len(volleyParams) == 0 or avgCycleTime == 0:
|
||||
return dmgDuringCycle
|
||||
for volleyValue in volleyParams.values():
|
||||
dmgDuringCycle += volleyValue
|
||||
dpsFactor = 1 / (avgCycleTime / 1000)
|
||||
dps = DmgTypes(
|
||||
em=dmgDuringCycle.em * dpsFactor,
|
||||
thermal=dmgDuringCycle.thermal * dpsFactor,
|
||||
kinetic=dmgDuringCycle.kinetic * dpsFactor,
|
||||
explosive=dmgDuringCycle.explosive * dpsFactor)
|
||||
if not getSpreadDPS:
|
||||
return dps
|
||||
return {'em':dmgDuringCycle.em * dpsFactor,
|
||||
'therm': dmgDuringCycle.thermal * dpsFactor,
|
||||
'kin': dmgDuringCycle.kinetic * dpsFactor,
|
||||
'exp': dmgDuringCycle.explosive * dpsFactor}
|
||||
if self.isBreacher:
|
||||
return volleyParams[min(volleyParams)]
|
||||
for volleyValue in volleyParams.values():
|
||||
dps += volleyValue
|
||||
dpsFactor = 1 / (avgCycleTime / 1000)
|
||||
dps *= dpsFactor
|
||||
return dps
|
||||
|
||||
def isRemoteRepping(self, ignoreState=False):
|
||||
repParams = self.getRepAmountParameters(ignoreState=ignoreState)
|
||||
@@ -949,6 +955,13 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
|
||||
and ((gang and effect.isType("gang")) or not gang):
|
||||
effect.handler(fit, self, context, projectionRange, effect=effect)
|
||||
|
||||
def getCycleParametersForDps(self, reloadOverride=None):
|
||||
# Special hack for breachers, since those are DoT and work independently of gun cycle
|
||||
if self.isBreacher:
|
||||
return CycleInfo(activeTime=1000, inactiveTime=0, quantity=math.inf, isInactivityReload=False)
|
||||
else:
|
||||
return self.getCycleParameters(reloadOverride=reloadOverride)
|
||||
|
||||
def getCycleParameters(self, reloadOverride=None):
|
||||
"""Copied from new eos as well"""
|
||||
# Determine if we'll take into account reload time or not
|
||||
|
||||
@@ -25,10 +25,11 @@ import time
|
||||
|
||||
|
||||
class SsoCharacter:
|
||||
def __init__(self, charID, name, client, accessToken=None, refreshToken=None):
|
||||
def __init__(self, charID, name, client, server, accessToken=None, refreshToken=None):
|
||||
self.characterID = charID
|
||||
self.characterName = name
|
||||
self.client = client
|
||||
self.server = server
|
||||
self.accessToken = accessToken
|
||||
self.refreshToken = refreshToken
|
||||
self.accessTokenExpires = None
|
||||
@@ -37,6 +38,9 @@ class SsoCharacter:
|
||||
def init(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def characterDisplay(self):
|
||||
return "{} [{}]".format(self.characterName, self.server)
|
||||
def is_token_expired(self):
|
||||
if self.accessTokenExpires is None:
|
||||
return True
|
||||
|
||||
@@ -254,7 +254,7 @@ class TargetProfile:
|
||||
def init(self):
|
||||
self.builtin = False
|
||||
|
||||
def update(self, emAmount=0, thermalAmount=0, kineticAmount=0, explosiveAmount=0, maxVelocity=None, signatureRadius=None, radius=None):
|
||||
def update(self, emAmount=0, thermalAmount=0, kineticAmount=0, explosiveAmount=0, maxVelocity=None, signatureRadius=None, radius=None, hp=None):
|
||||
self.emAmount = emAmount
|
||||
self.thermalAmount = thermalAmount
|
||||
self.kineticAmount = kineticAmount
|
||||
@@ -262,6 +262,7 @@ class TargetProfile:
|
||||
self._maxVelocity = maxVelocity
|
||||
self._signatureRadius = signatureRadius
|
||||
self._radius = radius
|
||||
self._hp = hp
|
||||
|
||||
@classmethod
|
||||
def getBuiltinList(cls):
|
||||
@@ -331,6 +332,18 @@ class TargetProfile:
|
||||
def radius(self, val):
|
||||
self._radius = val
|
||||
|
||||
@property
|
||||
def hp(self):
|
||||
if self._hp is None or self._hp == -1:
|
||||
return math.inf
|
||||
return self._hp
|
||||
|
||||
@hp.setter
|
||||
def hp(self, val):
|
||||
if val is not None and math.isinf(val):
|
||||
val = None
|
||||
self._hp = val
|
||||
|
||||
@classmethod
|
||||
def importPatterns(cls, text):
|
||||
lines = re.split('[\n\r]+', text)
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
import math
|
||||
from collections import defaultdict
|
||||
|
||||
from eos.utils.float import floatUnerr
|
||||
from utils.repr import makeReprStr
|
||||
|
||||
@@ -26,15 +29,133 @@ def _t(x):
|
||||
return x
|
||||
|
||||
|
||||
class BreacherInfo:
|
||||
|
||||
def __init__(self, absolute, relative):
|
||||
self.absolute = absolute
|
||||
self.relative = relative
|
||||
|
||||
def __mul__(self, mul):
|
||||
return type(self)(absolute=self.absolute * mul, relative=self.relative * mul)
|
||||
|
||||
def __imul__(self, mul):
|
||||
if mul == 1:
|
||||
return self
|
||||
self.absolute *= mul
|
||||
self.relative *= mul
|
||||
return self
|
||||
|
||||
def __truediv__(self, div):
|
||||
return type(self)(absolute=self.absolute / div, relative=self.relative / div)
|
||||
|
||||
|
||||
class DmgTypes:
|
||||
"""Container for damage data stats."""
|
||||
"""
|
||||
Container for volley stats, which stores breacher pod data
|
||||
in raw form, before application of it to target profile.
|
||||
"""
|
||||
|
||||
def __init__(self, em, thermal, kinetic, explosive):
|
||||
self.em = em
|
||||
self.thermal = thermal
|
||||
self.kinetic = kinetic
|
||||
self.explosive = explosive
|
||||
self._calcTotal()
|
||||
self._em = em
|
||||
self._thermal = thermal
|
||||
self._kinetic = kinetic
|
||||
self._explosive = explosive
|
||||
self._breachers = defaultdict(lambda: [])
|
||||
self.__profile = None
|
||||
# Cached data
|
||||
self.__cached_em = None
|
||||
self.__cached_thermal = None
|
||||
self.__cached_kinetic = None
|
||||
self.__cached_explosive = None
|
||||
self.__cached_pure = None
|
||||
self.__cached_total = None
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
return cls(0, 0, 0, 0)
|
||||
|
||||
def _clear_cached(self):
|
||||
self.__cached_em = None
|
||||
self.__cached_thermal = None
|
||||
self.__cached_kinetic = None
|
||||
self.__cached_explosive = None
|
||||
self.__cached_pure = None
|
||||
self.__cached_total = None
|
||||
|
||||
def add_breacher(self, key, data):
|
||||
self._breachers[key].append(data)
|
||||
|
||||
@property
|
||||
def profile(self):
|
||||
return self.__profile
|
||||
|
||||
@profile.setter
|
||||
def profile(self, profile):
|
||||
self.__profile = profile
|
||||
self._clear_cached()
|
||||
|
||||
@property
|
||||
def em(self):
|
||||
if self.__cached_em is not None:
|
||||
return self.__cached_em
|
||||
dmg = self._em
|
||||
if self.profile is not None:
|
||||
dmg *= 1 - getattr(self.profile, "emAmount", 0)
|
||||
self.__cached_em = dmg
|
||||
return dmg
|
||||
|
||||
@property
|
||||
def thermal(self):
|
||||
if self.__cached_thermal is not None:
|
||||
return self.__cached_thermal
|
||||
dmg = self._thermal
|
||||
if self.profile is not None:
|
||||
dmg *= 1 - getattr(self.profile, "thermalAmount", 0)
|
||||
self.__cached_thermal = dmg
|
||||
return dmg
|
||||
|
||||
@property
|
||||
def kinetic(self):
|
||||
if self.__cached_kinetic is not None:
|
||||
return self.__cached_kinetic
|
||||
dmg = self._kinetic
|
||||
if self.profile is not None:
|
||||
dmg *= 1 - getattr(self.profile, "kineticAmount", 0)
|
||||
self.__cached_kinetic = dmg
|
||||
return dmg
|
||||
|
||||
@property
|
||||
def explosive(self):
|
||||
if self.__cached_explosive is not None:
|
||||
return self.__cached_explosive
|
||||
dmg = self._explosive
|
||||
if self.profile is not None:
|
||||
dmg *= 1 - getattr(self.profile, "explosiveAmount", 0)
|
||||
self.__cached_explosive = dmg
|
||||
return dmg
|
||||
|
||||
@property
|
||||
def pure(self):
|
||||
if self.__cached_pure is not None:
|
||||
return self.__cached_pure
|
||||
if self.profile is None:
|
||||
dmg = sum(
|
||||
max((b.absolute for b in bs), default=0)
|
||||
for bs in self._breachers.values())
|
||||
else:
|
||||
dmg = sum(
|
||||
max((min(b.absolute, b.relative * getattr(self.profile, "hp", math.inf)) for b in bs), default=0)
|
||||
for bs in self._breachers.values())
|
||||
self.__cached_pure = dmg
|
||||
return dmg
|
||||
|
||||
@property
|
||||
def total(self):
|
||||
if self.__cached_total is not None:
|
||||
return self.__cached_total
|
||||
dmg = self.em + self.thermal + self.kinetic + self.explosive + self.pure
|
||||
self.__cached_total = dmg
|
||||
return dmg
|
||||
|
||||
# Iterator is needed to support tuple-style unpacking
|
||||
def __iter__(self):
|
||||
@@ -42,6 +163,7 @@ class DmgTypes:
|
||||
yield self.thermal
|
||||
yield self.kinetic
|
||||
yield self.explosive
|
||||
yield self.pure
|
||||
yield self.total
|
||||
|
||||
def __eq__(self, other):
|
||||
@@ -50,77 +172,87 @@ class DmgTypes:
|
||||
# Round for comparison's sake because often damage profiles are
|
||||
# generated from data which includes float errors
|
||||
return (
|
||||
floatUnerr(self.em) == floatUnerr(other.em) and
|
||||
floatUnerr(self.thermal) == floatUnerr(other.thermal) and
|
||||
floatUnerr(self.kinetic) == floatUnerr(other.kinetic) and
|
||||
floatUnerr(self.explosive) == floatUnerr(other.explosive) and
|
||||
floatUnerr(self.total) == floatUnerr(other.total))
|
||||
|
||||
def __bool__(self):
|
||||
return any((
|
||||
self.em, self.thermal, self.kinetic,
|
||||
self.explosive, self.total))
|
||||
|
||||
def _calcTotal(self):
|
||||
self.total = self.em + self.thermal + self.kinetic + self.explosive
|
||||
floatUnerr(self._em) == floatUnerr(other._em) and
|
||||
floatUnerr(self._thermal) == floatUnerr(other._thermal) and
|
||||
floatUnerr(self._kinetic) == floatUnerr(other._kinetic) and
|
||||
floatUnerr(self._explosive) == floatUnerr(other._explosive) and
|
||||
sorted(self._breachers) == sorted(other._breachers) and
|
||||
self.profile == other.profile)
|
||||
|
||||
def __add__(self, other):
|
||||
return type(self)(
|
||||
em=self.em + other.em,
|
||||
thermal=self.thermal + other.thermal,
|
||||
kinetic=self.kinetic + other.kinetic,
|
||||
explosive=self.explosive + other.explosive)
|
||||
new = type(self)(
|
||||
em=self._em + other._em,
|
||||
thermal=self._thermal + other._thermal,
|
||||
kinetic=self._kinetic + other._kinetic,
|
||||
explosive=self._explosive + other._explosive)
|
||||
new.profile = self.profile
|
||||
for k, v in self._breachers.items():
|
||||
new._breachers[k].extend(v)
|
||||
for k, v in other._breachers.items():
|
||||
new._breachers[k].extend(v)
|
||||
return new
|
||||
|
||||
def __iadd__(self, other):
|
||||
self.em += other.em
|
||||
self.thermal += other.thermal
|
||||
self.kinetic += other.kinetic
|
||||
self.explosive += other.explosive
|
||||
self._calcTotal()
|
||||
self._em += other._em
|
||||
self._thermal += other._thermal
|
||||
self._kinetic += other._kinetic
|
||||
self._explosive += other._explosive
|
||||
for k, v in other._breachers.items():
|
||||
self._breachers[k].extend(v)
|
||||
self._clear_cached()
|
||||
return self
|
||||
|
||||
def __mul__(self, mul):
|
||||
return type(self)(
|
||||
em=self.em * mul,
|
||||
thermal=self.thermal * mul,
|
||||
kinetic=self.kinetic * mul,
|
||||
explosive=self.explosive * mul)
|
||||
new = type(self)(
|
||||
em=self._em * mul,
|
||||
thermal=self._thermal * mul,
|
||||
kinetic=self._kinetic * mul,
|
||||
explosive=self._explosive * mul)
|
||||
new.profile = self.profile
|
||||
for k, v in self._breachers.items():
|
||||
new._breachers[k] = [b * mul for b in v]
|
||||
return new
|
||||
|
||||
def __imul__(self, mul):
|
||||
if mul == 1:
|
||||
return
|
||||
self.em *= mul
|
||||
self.thermal *= mul
|
||||
self.kinetic *= mul
|
||||
self.explosive *= mul
|
||||
self._calcTotal()
|
||||
return self
|
||||
self._em *= mul
|
||||
self._thermal *= mul
|
||||
self._kinetic *= mul
|
||||
self._explosive *= mul
|
||||
for v in self._breachers.values():
|
||||
for b in v:
|
||||
b *= mul
|
||||
self._clear_cached()
|
||||
return self
|
||||
|
||||
def __truediv__(self, div):
|
||||
return type(self)(
|
||||
em=self.em / div,
|
||||
thermal=self.thermal / div,
|
||||
kinetic=self.kinetic / div,
|
||||
explosive=self.explosive / div)
|
||||
new = type(self)(
|
||||
em=self._em / div,
|
||||
thermal=self._thermal / div,
|
||||
kinetic=self._kinetic / div,
|
||||
explosive=self._explosive / div)
|
||||
new.profile = self.profile
|
||||
for k, v in self._breachers.items():
|
||||
new._breachers[k] = [b / div for b in v]
|
||||
return new
|
||||
|
||||
def __itruediv__(self, div):
|
||||
if div == 1:
|
||||
return
|
||||
self.em /= div
|
||||
self.thermal /= div
|
||||
self.kinetic /= div
|
||||
self.explosive /= div
|
||||
self._calcTotal()
|
||||
return self
|
||||
def __bool__(self):
|
||||
return any((
|
||||
self._em, self._thermal, self._kinetic, self._explosive,
|
||||
any(b.absolute or b.relative for b in self._breachers)))
|
||||
|
||||
def __repr__(self):
|
||||
spec = DmgTypes.names()
|
||||
spec.append('total')
|
||||
return makeReprStr(self, spec)
|
||||
class_name = type(self).__name__
|
||||
return (f'<{class_name}(em={self._em}, thermal={self._thermal}, kinetic={self._kinetic}, '
|
||||
f'explosive={self._explosive}, breachers={len(self._breachers)})>')
|
||||
|
||||
@staticmethod
|
||||
def names(short=None, postProcessor=None):
|
||||
def names(short=None, postProcessor=None, includePure=False):
|
||||
|
||||
value = [_t('em'), _t('th'), _t('kin'), _t('exp')] if short else [_t('em'), _t('thermal'), _t('kinetic'), _t('explosive')]
|
||||
if includePure:
|
||||
value += [_t('pure')]
|
||||
|
||||
if postProcessor:
|
||||
value = [postProcessor(x) for x in value]
|
||||
|
||||
24
graphs/data/fitDamageStats/cache/time.py
vendored
@@ -117,7 +117,7 @@ class TimeCache(FitDataCache):
|
||||
pointData[timeStart] = (dps, volley)
|
||||
# Gap between items
|
||||
elif floatUnerr(prevTimeEnd) < floatUnerr(timeStart):
|
||||
pointData[prevTimeEnd] = (DmgTypes(0, 0, 0, 0), DmgTypes(0, 0, 0, 0))
|
||||
pointData[prevTimeEnd] = (DmgTypes.default(), DmgTypes.default())
|
||||
pointData[timeStart] = (dps, volley)
|
||||
# Changed value
|
||||
elif dps != prevDps or volley != prevVolley:
|
||||
@@ -157,7 +157,7 @@ class TimeCache(FitDataCache):
|
||||
def addDpsVolley(ddKey, addedTimeStart, addedTimeFinish, addedVolleys):
|
||||
if not addedVolleys:
|
||||
return
|
||||
volleySum = sum(addedVolleys, DmgTypes(0, 0, 0, 0))
|
||||
volleySum = sum(addedVolleys, DmgTypes.default())
|
||||
if volleySum.total > 0:
|
||||
addedDps = volleySum / (addedTimeFinish - addedTimeStart)
|
||||
# We can take "just best" volley, no matter target resistances, because all
|
||||
@@ -170,24 +170,38 @@ class TimeCache(FitDataCache):
|
||||
def addDmg(ddKey, addedTime, addedDmg):
|
||||
if addedDmg.total == 0:
|
||||
return
|
||||
addedDmg._breachers = {addedTime + k: v for k, v in addedDmg._breachers.items()}
|
||||
addedDmg._clear_cached()
|
||||
intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg
|
||||
|
||||
# Modules
|
||||
for mod in src.item.activeModulesIter():
|
||||
if not mod.isDealingDamage():
|
||||
continue
|
||||
cycleParams = mod.getCycleParameters(reloadOverride=True)
|
||||
cycleParams = mod.getCycleParametersForDps(reloadOverride=True)
|
||||
if cycleParams is None:
|
||||
continue
|
||||
currentTime = 0
|
||||
nonstopCycles = 0
|
||||
isBreacher = mod.isBreacher
|
||||
for cycleTimeMs, inactiveTimeMs, isInactivityReload in cycleParams.iterCycles():
|
||||
cycleVolleys = []
|
||||
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
|
||||
|
||||
for volleyTimeMs, volley in volleyParams.items():
|
||||
cycleVolleys.append(volley)
|
||||
addDmg(mod, currentTime + volleyTimeMs / 1000, volley)
|
||||
addDpsVolley(mod, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
|
||||
time = currentTime + volleyTimeMs / 1000
|
||||
if isBreacher:
|
||||
time += 1
|
||||
addDmg(mod, time, volley)
|
||||
if isBreacher:
|
||||
break
|
||||
timeStart = currentTime
|
||||
timeFinish = currentTime + cycleTimeMs / 1000
|
||||
if isBreacher:
|
||||
timeStart += 1
|
||||
timeFinish += 1
|
||||
addDpsVolley(mod, timeStart, timeFinish, cycleVolleys)
|
||||
if inactiveTimeMs > 0:
|
||||
nonstopCycles = 0
|
||||
else:
|
||||
|
||||
@@ -98,6 +98,8 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
|
||||
tgt=tgt,
|
||||
distance=distance,
|
||||
tgtSigRadius=tgtSigRadius)
|
||||
elif mod.isBreacher:
|
||||
applicationMap[mod] = getBreacherMult(mod=mod, distance=distance) if inLockRange else 0
|
||||
for drone in src.item.activeDronesIter():
|
||||
if not drone.isDealingDamage():
|
||||
continue
|
||||
@@ -192,6 +194,21 @@ def getLauncherMult(mod, distance, tgtSpeed, tgtSigRadius):
|
||||
return distanceFactor * applicationFactor
|
||||
|
||||
|
||||
def getBreacherMult(mod, distance):
|
||||
missileMaxRangeData = mod.missileMaxRangeData
|
||||
if missileMaxRangeData is None:
|
||||
return 0
|
||||
# The ranges already consider ship radius
|
||||
lowerRange, higherRange, higherChance = missileMaxRangeData
|
||||
if distance is None or distance <= lowerRange:
|
||||
distanceFactor = 1
|
||||
elif lowerRange < distance <= higherRange:
|
||||
distanceFactor = higherChance
|
||||
else:
|
||||
distanceFactor = 0
|
||||
return distanceFactor
|
||||
|
||||
|
||||
def getSmartbombMult(mod, distance):
|
||||
modRange = mod.maxRange
|
||||
if modRange is None:
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
|
||||
import eos.config
|
||||
from eos.saveddata.targetProfile import TargetProfile
|
||||
from eos.utils.spoolSupport import SpoolOptions, SpoolType
|
||||
from eos.utils.stats import DmgTypes
|
||||
from graphs.data.base import PointGetter, SmoothPointGetter
|
||||
@@ -27,17 +28,16 @@ from .calc.application import getApplicationPerKey
|
||||
from .calc.projected import getScramRange, getScrammables, getTackledSpeed, getSigRadiusMult
|
||||
|
||||
|
||||
def applyDamage(dmgMap, applicationMap, tgtResists):
|
||||
total = DmgTypes(em=0, thermal=0, kinetic=0, explosive=0)
|
||||
def applyDamage(dmgMap, applicationMap, tgtResists, tgtFullHp):
|
||||
total = DmgTypes.default()
|
||||
for key, dmg in dmgMap.items():
|
||||
total += dmg * applicationMap.get(key, 0)
|
||||
if not GraphSettings.getInstance().get('ignoreResists'):
|
||||
emRes, thermRes, kinRes, exploRes = tgtResists
|
||||
total = DmgTypes(
|
||||
em=total.em * (1 - emRes),
|
||||
thermal=total.thermal * (1 - thermRes),
|
||||
kinetic=total.kinetic * (1 - kinRes),
|
||||
explosive=total.explosive * (1 - exploRes))
|
||||
else:
|
||||
emRes = thermRes = kinRes = exploRes = 0
|
||||
total.profile = TargetProfile(
|
||||
emAmount=emRes, thermalAmount=thermRes, kineticAmount=kinRes, explosiveAmount=exploRes, hp=tgtFullHp)
|
||||
return total
|
||||
|
||||
|
||||
@@ -144,7 +144,8 @@ class XDistanceMixin(SmoothPointGetter):
|
||||
'srcScramRange': getScramRange(src=src) if applyProjected else None,
|
||||
'tgtScrammables': getScrammables(tgt=tgt) if applyProjected else (),
|
||||
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
|
||||
'tgtResists': tgt.getResists()}
|
||||
'tgtResists': tgt.getResists(),
|
||||
'tgtFullHp': tgt.getFullHp()}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
|
||||
distance = x
|
||||
@@ -186,7 +187,8 @@ class XDistanceMixin(SmoothPointGetter):
|
||||
y = applyDamage(
|
||||
dmgMap=commonData['dmgMap'],
|
||||
applicationMap=applicationMap,
|
||||
tgtResists=commonData['tgtResists']).total
|
||||
tgtResists=commonData['tgtResists'],
|
||||
tgtFullHp=commonData['tgtFullHp']).total
|
||||
return y
|
||||
|
||||
|
||||
@@ -241,14 +243,17 @@ class XTimeMixin(PointGetter):
|
||||
self._prepareTimeCache(src=src, maxTime=maxTime)
|
||||
timeCache = self._getTimeCacheData(src=src)
|
||||
applicationMap = self._prepareApplicationMap(miscParams=miscParams, src=src, tgt=tgt)
|
||||
tgtResists = tgt.getResists()
|
||||
# Custom iteration for time graph to show all data points
|
||||
currentDmg = None
|
||||
currentTime = None
|
||||
for currentTime in sorted(timeCache):
|
||||
prevDmg = currentDmg
|
||||
currentDmgData = timeCache[currentTime]
|
||||
currentDmg = applyDamage(dmgMap=currentDmgData, applicationMap=applicationMap, tgtResists=tgtResists).total
|
||||
currentDmg = applyDamage(
|
||||
dmgMap=currentDmgData,
|
||||
applicationMap=applicationMap,
|
||||
tgtResists=tgt.getResists(),
|
||||
tgtFullHp=tgt.getFullHp()).total
|
||||
if currentTime < minTime:
|
||||
continue
|
||||
# First set of data points
|
||||
@@ -294,7 +299,11 @@ class XTimeMixin(PointGetter):
|
||||
self._prepareTimeCache(src=src, maxTime=time)
|
||||
dmgData = self._getTimeCacheDataPoint(src=src, time=time)
|
||||
applicationMap = self._prepareApplicationMap(miscParams=miscParams, src=src, tgt=tgt)
|
||||
y = applyDamage(dmgMap=dmgData, applicationMap=applicationMap, tgtResists=tgt.getResists()).total
|
||||
y = applyDamage(
|
||||
dmgMap=dmgData,
|
||||
applicationMap=applicationMap,
|
||||
tgtResists=tgt.getResists(),
|
||||
tgtFullHp=tgt.getFullHp()).total
|
||||
return y
|
||||
|
||||
|
||||
@@ -310,7 +319,8 @@ class XTgtSpeedMixin(SmoothPointGetter):
|
||||
return {
|
||||
'applyProjected': GraphSettings.getInstance().get('applyProjected'),
|
||||
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
|
||||
'tgtResists': tgt.getResists()}
|
||||
'tgtResists': tgt.getResists(),
|
||||
'tgtFullHp': tgt.getFullHp()}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
|
||||
tgtSpeed = x
|
||||
@@ -353,7 +363,8 @@ class XTgtSpeedMixin(SmoothPointGetter):
|
||||
y = applyDamage(
|
||||
dmgMap=commonData['dmgMap'],
|
||||
applicationMap=applicationMap,
|
||||
tgtResists=commonData['tgtResists']).total
|
||||
tgtResists=commonData['tgtResists'],
|
||||
tgtFullHp=commonData['tgtFullHp']).total
|
||||
return y
|
||||
|
||||
|
||||
@@ -398,7 +409,8 @@ class XTgtSigRadiusMixin(SmoothPointGetter):
|
||||
'tgtSpeed': tgtSpeed,
|
||||
'tgtSigMult': tgtSigMult,
|
||||
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
|
||||
'tgtResists': tgt.getResists()}
|
||||
'tgtResists': tgt.getResists(),
|
||||
'tgtFullHp': tgt.getFullHp()}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
|
||||
tgtSigRadius = x
|
||||
@@ -414,7 +426,8 @@ class XTgtSigRadiusMixin(SmoothPointGetter):
|
||||
y = applyDamage(
|
||||
dmgMap=commonData['dmgMap'],
|
||||
applicationMap=applicationMap,
|
||||
tgtResists=commonData['tgtResists']).total
|
||||
tgtResists=commonData['tgtResists'],
|
||||
tgtFullHp=commonData['tgtFullHp']).total
|
||||
return y
|
||||
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class FitDamageStatsGraph(FitGraph):
|
||||
cols = []
|
||||
if not GraphSettings.getInstance().get('ignoreResists'):
|
||||
cols.append('Target Resists')
|
||||
cols.extend(('Speed', 'SigRadius', 'Radius'))
|
||||
cols.extend(('Speed', 'SigRadius', 'Radius', 'FullHP'))
|
||||
return cols
|
||||
|
||||
# Calculation stuff
|
||||
|
||||
@@ -273,7 +273,7 @@ class GraphCanvasPanel(wx.Panel):
|
||||
legendLines = []
|
||||
for i, iData in enumerate(legendData):
|
||||
color, lineStyle, label = iData
|
||||
legendLines.append(Line2D([0], [0], color=color, linestyle=lineStyle, label=label.replace('$', '\$')))
|
||||
legendLines.append(Line2D([0], [0], color=color, linestyle=lineStyle, label=label.replace('$', r'\$')))
|
||||
|
||||
if len(legendLines) > 0 and self.graphFrame.ctrlPanel.showLegend:
|
||||
legend = self.subplot.legend(handles=legendLines)
|
||||
|
||||
@@ -145,6 +145,11 @@ class TargetWrapper(BaseWrapper):
|
||||
else:
|
||||
return em, therm, kin, explo
|
||||
|
||||
def getFullHp(self):
|
||||
if self.isProfile:
|
||||
return self.item.hp
|
||||
if self.isFit:
|
||||
return self.item.hp.get('shield', 0) + self.item.hp.get('armor', 0) + self.item.hp.get('hull', 0)
|
||||
|
||||
|
||||
def _getShieldResists(ship):
|
||||
|
||||
@@ -43,6 +43,10 @@ class BoosterViewDrop(wx.DropTarget):
|
||||
if self.GetData():
|
||||
dragged_data = DragDropHelper.data
|
||||
data = dragged_data.split(':')
|
||||
|
||||
if dragged_data is None:
|
||||
return t
|
||||
|
||||
self.dropFn(x, y, data)
|
||||
return t
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ class CargoViewDrop(wx.DropTarget):
|
||||
def OnData(self, x, y, t):
|
||||
if self.GetData():
|
||||
dragged_data = DragDropHelper.data
|
||||
|
||||
if dragged_data is None:
|
||||
return t
|
||||
|
||||
data = dragged_data.split(':')
|
||||
self.dropFn(x, y, data)
|
||||
return t
|
||||
|
||||
@@ -56,6 +56,10 @@ class CommandViewDrop(wx.DropTarget):
|
||||
def OnData(self, x, y, t):
|
||||
if self.GetData():
|
||||
dragged_data = DragDropHelper.data
|
||||
|
||||
if dragged_data is None:
|
||||
return t
|
||||
|
||||
data = dragged_data.split(':')
|
||||
self.dropFn(x, y, data)
|
||||
return t
|
||||
|
||||
@@ -52,6 +52,10 @@ class DroneViewDrop(wx.DropTarget):
|
||||
def OnData(self, x, y, t):
|
||||
if self.GetData():
|
||||
dragged_data = DragDropHelper.data
|
||||
|
||||
if dragged_data is None:
|
||||
return t
|
||||
|
||||
data = dragged_data.split(':')
|
||||
self.dropFn(x, y, data)
|
||||
return t
|
||||
@@ -195,7 +199,11 @@ class DroneView(Display):
|
||||
|
||||
@staticmethod
|
||||
def droneKey(drone):
|
||||
groupName = Market.getInstance().getMarketGroupByItem(drone.item).marketGroupName
|
||||
if drone.isMutated:
|
||||
item = drone.baseItem
|
||||
else:
|
||||
item = drone.item
|
||||
groupName = Market.getInstance().getMarketGroupByItem(item).marketGroupName
|
||||
return (DRONE_ORDER.index(groupName), drone.isMutated, drone.fullName)
|
||||
|
||||
def fitChanged(self, event):
|
||||
|
||||
@@ -34,7 +34,10 @@ from service.fit import Fit
|
||||
from service.market import Market
|
||||
|
||||
|
||||
FIGHTER_ORDER = ('Light Fighter', 'Heavy Fighter', 'Support Fighter')
|
||||
FIGHTER_ORDER = (
|
||||
'Light Fighter', 'Structure Light Fighter',
|
||||
'Heavy Fighter', 'Structure Heavy Fighter',
|
||||
'Support Fighter', 'Structure Support Fighter')
|
||||
_t = wx.GetTranslation
|
||||
|
||||
|
||||
@@ -49,6 +52,10 @@ class FighterViewDrop(wx.DropTarget):
|
||||
def OnData(self, x, y, t):
|
||||
if self.GetData():
|
||||
dragged_data = DragDropHelper.data
|
||||
|
||||
if dragged_data is None:
|
||||
return t
|
||||
|
||||
data = dragged_data.split(':')
|
||||
self.dropFn(x, y, data)
|
||||
return t
|
||||
|
||||
@@ -46,6 +46,10 @@ class ImplantViewDrop(wx.DropTarget):
|
||||
def OnData(self, x, y, t):
|
||||
if self.GetData():
|
||||
dragged_data = DragDropHelper.data
|
||||
|
||||
if dragged_data is None:
|
||||
return t
|
||||
|
||||
data = dragged_data.split(':')
|
||||
self.dropFn(x, y, data)
|
||||
return t
|
||||
|
||||
@@ -65,6 +65,10 @@ class ProjectedViewDrop(wx.DropTarget):
|
||||
def OnData(self, x, y, t):
|
||||
if self.GetData():
|
||||
dragged_data = DragDropHelper.data
|
||||
|
||||
if dragged_data is None:
|
||||
return t
|
||||
|
||||
data = dragged_data.split(':')
|
||||
self.dropFn(x, y, data)
|
||||
return t
|
||||
|
||||
@@ -20,6 +20,7 @@ from gui.builtinContextMenus.targetProfile import editor
|
||||
from gui.builtinContextMenus import itemStats
|
||||
from gui.builtinContextMenus import itemMarketJump
|
||||
from gui.builtinContextMenus import fitSystemSecurity # Not really an item info but want to keep it here
|
||||
from gui.builtinContextMenus import fitPilotSecurity # Not really an item info but want to keep it here
|
||||
from gui.builtinContextMenus import shipJump
|
||||
# Generic item manipulations
|
||||
from gui.builtinContextMenus import itemRemove
|
||||
|
||||
@@ -25,7 +25,7 @@ class AddToCargoAmmo(ContextMenuSingle):
|
||||
return True
|
||||
|
||||
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 (x1000)").format(itmContext)
|
||||
@@ -34,7 +34,7 @@ class AddToCargoAmmo(ContextMenuSingle):
|
||||
fitID = self.mainFrame.getActiveFit()
|
||||
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)
|
||||
else:
|
||||
command = cmd.GuiAddCargoCommand(fitID=fitID, itemID=typeID, amount=1000)
|
||||
|
||||
@@ -65,7 +65,6 @@ class DroneStackSplit(wx.Dialog):
|
||||
|
||||
self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER)
|
||||
self.input.SetValue(str(value))
|
||||
self.input.SelectAll()
|
||||
|
||||
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
|
||||
|
||||
@@ -75,12 +74,13 @@ class DroneStackSplit(wx.Dialog):
|
||||
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
|
||||
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
|
||||
|
||||
self.input.SetFocus()
|
||||
self.input.Bind(wx.EVT_CHAR, self.onChar)
|
||||
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
|
||||
self.SetSizer(bSizer1)
|
||||
self.CenterOnParent()
|
||||
self.Fit()
|
||||
self.CenterOnParent()
|
||||
self.input.SetFocus()
|
||||
self.input.SelectAll()
|
||||
|
||||
def processEnter(self, evt):
|
||||
self.EndModal(wx.ID_OK)
|
||||
|
||||
@@ -123,9 +123,11 @@ class AddEnvironmentEffect(ContextMenuUnconditional):
|
||||
data.groups[_t('Abyssal Weather')] = self.getAbyssalWeather()
|
||||
data.groups[_t('Sansha Incursion')] = self.getEffectBeacons(
|
||||
_t('ContextMenu|ProjectedEffectManipulation|Sansha Incursion'))
|
||||
data.groups[_t('Drifter Incursion')] = self.getDrifterIncursion()
|
||||
data.groups[_t('Triglavian Invasion')] = self.getInvasionBeacons()
|
||||
# data.groups[_t('Pirate Insurgency')] = self.getEffectBeacons(
|
||||
# _t('ContextMenu|ProjectedEffectManipulation|Insurgency'))
|
||||
data.groups[_t('Pirate Insurgency')] = self.getEffectBeacons(
|
||||
_t('ContextMenu|ProjectedEffectManipulation|Insurgency'),
|
||||
extra_garbage=(_t('ContextMenu|ProjectedEffectManipulation|Beacon'),))
|
||||
return data
|
||||
|
||||
def getEffectBeacons(self, *groups, extra_garbage=()):
|
||||
@@ -176,7 +178,6 @@ class AddEnvironmentEffect(ContextMenuUnconditional):
|
||||
container.append(Entry(beacon.ID, beaconname, shortname))
|
||||
# Break loop on 1st result
|
||||
break
|
||||
data.sort()
|
||||
return data
|
||||
|
||||
def getAbyssalWeather(self):
|
||||
@@ -233,8 +234,21 @@ class AddEnvironmentEffect(ContextMenuUnconditional):
|
||||
data.sort()
|
||||
return data
|
||||
|
||||
def getDrifterIncursion(self):
|
||||
data = self.getEffectBeacons(_t('ContextMenu|ProjectedEffectManipulation|Drifter Incursion'))
|
||||
# Drifter Crisis
|
||||
item = Market.getInstance().getItem(87294)
|
||||
data.items.append(Entry(item.ID, item.name, item.name))
|
||||
return data
|
||||
|
||||
def getInvasionBeacons(self):
|
||||
data = self.getDestructibleBeacons()
|
||||
# Trig Minor Victory
|
||||
item = Market.getInstance().getItem(87177)
|
||||
data.items.append(Entry(item.ID, item.name, item.name))
|
||||
# Trig Final Liminality
|
||||
item = Market.getInstance().getItem(87164)
|
||||
data.items.append(Entry(item.ID, item.name, item.name))
|
||||
# Turnur weather
|
||||
item = Market.getInstance().getItem(74002)
|
||||
data.items.append(Entry(item.ID, item.name, item.name))
|
||||
@@ -247,5 +261,4 @@ class AddEnvironmentEffect(ContextMenuUnconditional):
|
||||
data.items.append(Entry(item.ID, item.name, item.name))
|
||||
return data
|
||||
|
||||
|
||||
AddEnvironmentEffect.register()
|
||||
|
||||
157
gui/builtinContextMenus/fitPilotSecurity.py
Normal 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
|
||||
@@ -59,7 +59,6 @@ class NameDialog(wx.Dialog):
|
||||
else:
|
||||
value = str(value)
|
||||
self.input.SetValue(value)
|
||||
self.input.SelectAll()
|
||||
|
||||
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
|
||||
|
||||
@@ -69,11 +68,12 @@ class NameDialog(wx.Dialog):
|
||||
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
|
||||
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
|
||||
|
||||
self.input.SetFocus()
|
||||
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
|
||||
self.SetSizer(bSizer1)
|
||||
self.CenterOnParent()
|
||||
self.Fit()
|
||||
self.CenterOnParent()
|
||||
self.input.SetFocus()
|
||||
self.input.SelectAll()
|
||||
|
||||
def processEnter(self, evt):
|
||||
self.EndModal(wx.ID_OK)
|
||||
|
||||
@@ -108,7 +108,6 @@ class AmountChanger(wx.Dialog):
|
||||
|
||||
self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER)
|
||||
self.input.SetValue(str(value))
|
||||
self.input.SelectAll()
|
||||
|
||||
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
|
||||
|
||||
@@ -118,12 +117,13 @@ class AmountChanger(wx.Dialog):
|
||||
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
|
||||
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
|
||||
|
||||
self.input.SetFocus()
|
||||
self.input.Bind(wx.EVT_CHAR, self.onChar)
|
||||
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
|
||||
self.SetSizer(bSizer1)
|
||||
self.CenterOnParent()
|
||||
self.Fit()
|
||||
self.CenterOnParent()
|
||||
self.input.SetFocus()
|
||||
self.input.SelectAll()
|
||||
|
||||
def processEnter(self, evt):
|
||||
self.EndModal(wx.ID_OK)
|
||||
|
||||
@@ -13,6 +13,16 @@ from service.fit import Fit
|
||||
_t = wx.GetTranslation
|
||||
|
||||
|
||||
GLORIFIED_PREFIX = 'Gl. '
|
||||
|
||||
|
||||
def nameSorter(mutaplasmid):
|
||||
name = mutaplasmid.shortName
|
||||
if name.startswith(GLORIFIED_PREFIX):
|
||||
return name[len(GLORIFIED_PREFIX):], True
|
||||
return name, False
|
||||
|
||||
|
||||
class ChangeItemMutation(ContextMenuSingle):
|
||||
|
||||
def __init__(self):
|
||||
@@ -45,7 +55,7 @@ class ChangeItemMutation(ContextMenuSingle):
|
||||
|
||||
menu = rootMenu if msw else sub
|
||||
|
||||
for mutaplasmid in mainItem.item.mutaplasmids:
|
||||
for mutaplasmid in sorted(mainItem.item.mutaplasmids, key=nameSorter):
|
||||
id = ContextMenuSingle.nextID()
|
||||
self.eventIDs[id] = (mutaplasmid, mainItem)
|
||||
mItem = wx.MenuItem(menu, id, mutaplasmid.shortName)
|
||||
|
||||
@@ -94,7 +94,6 @@ class RangeChanger(wx.Dialog):
|
||||
value = int(value)
|
||||
value = str(value)
|
||||
self.input.SetValue(value)
|
||||
self.input.SelectAll()
|
||||
|
||||
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
|
||||
|
||||
@@ -104,12 +103,13 @@ class RangeChanger(wx.Dialog):
|
||||
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
|
||||
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
|
||||
|
||||
self.input.SetFocus()
|
||||
self.input.Bind(wx.EVT_CHAR, self.onChar)
|
||||
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
|
||||
self.SetSizer(bSizer1)
|
||||
self.CenterOnParent()
|
||||
self.Fit()
|
||||
self.CenterOnParent()
|
||||
self.input.SetFocus()
|
||||
self.input.SelectAll()
|
||||
|
||||
def processEnter(self, evt):
|
||||
self.EndModal(wx.ID_OK)
|
||||
|
||||
@@ -72,6 +72,7 @@ AttrGroupDict = {
|
||||
"specialAmmoHoldCapacity",
|
||||
"specialCommandCenterHoldCapacity",
|
||||
"specialPlanetaryCommoditiesHoldCapacity",
|
||||
"specialColonyResourcesHoldCapacity",
|
||||
"structureDamageLimit",
|
||||
"specialSubsystemHoldCapacity",
|
||||
"emDamageResonance",
|
||||
|
||||
@@ -4,13 +4,18 @@ import wx
|
||||
# noinspection PyPackageRequirements
|
||||
import wx.lib.mixins.listctrl as listmix
|
||||
|
||||
from gui.utils.dark import isDark
|
||||
|
||||
|
||||
class AutoListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin, listmix.ListRowHighlighter):
|
||||
def __init__(self, parent, ID, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0):
|
||||
wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
|
||||
listmix.ListCtrlAutoWidthMixin.__init__(self)
|
||||
listmix.ListRowHighlighter.__init__(self)
|
||||
|
||||
if isDark():
|
||||
listcol = wx.SystemSettings.GetColour(wx.SYS_COLOUR_LISTBOX)
|
||||
highlight = listcol.ChangeLightness(110)
|
||||
listmix.ListRowHighlighter.SetHighlightColor(self, highlight)
|
||||
|
||||
class AutoListCtrlNoHighlight(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin, listmix.ListRowHighlighter):
|
||||
def __init__(self, parent, ID, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0):
|
||||
|
||||
@@ -22,9 +22,9 @@ class ItemDescription(wx.Panel):
|
||||
|
||||
desc = item.description.replace("\n", "<br>")
|
||||
# Strip font tags
|
||||
desc = re.sub("<( *)font( *)color( *)=(.*?)>(?P<inside>.*?)<( *)/( *)font( *)>", "\g<inside>", desc)
|
||||
desc = re.sub("<( *)font( *)color( *)=(.*?)>(?P<inside>.*?)<( *)/( *)font( *)>", r"\g<inside>", desc)
|
||||
# Strip URLs
|
||||
desc = re.sub("<( *)a(.*?)>(?P<inside>.*?)<( *)/( *)a( *)>", "\g<inside>", desc)
|
||||
desc = re.sub("<( *)a(.*?)>(?P<inside>.*?)<( *)/( *)a( *)>", r"\g<inside>", desc)
|
||||
desc = "<body bgcolor='{}' text='{}'>{}</body>".format(
|
||||
bgcolor.GetAsString(wx.C2S_HTML_SYNTAX),
|
||||
fgcolor.GetAsString(wx.C2S_HTML_SYNTAX),
|
||||
|
||||
@@ -5,3 +5,5 @@ import wx.lib.newevent
|
||||
ItemSelected, ITEM_SELECTED = wx.lib.newevent.NewEvent()
|
||||
|
||||
RECENTLY_USED_MODULES = -2
|
||||
|
||||
CHARGES_FOR_FIT = -3
|
||||
|
||||
@@ -2,14 +2,17 @@ import wx
|
||||
from logbook import Logger
|
||||
|
||||
import gui.builtinMarketBrowser.pfSearchBox as SBox
|
||||
from config import slotColourMap
|
||||
import gui.globalEvents as GE
|
||||
from config import slotColourMap, slotColourMapDark
|
||||
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.display import Display
|
||||
from gui.utils.staticHelpers import DragDropHelper
|
||||
from gui.utils.dark import isDark
|
||||
from service.fit import Fit
|
||||
from service.market import Market
|
||||
from service.ammo import Ammo
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
@@ -31,6 +34,7 @@ class ItemView(Display):
|
||||
self.filteredStore = set()
|
||||
self.sMkt = marketBrowser.sMkt
|
||||
self.sFit = Fit.getInstance()
|
||||
self.sAmmo = Ammo.getInstance()
|
||||
|
||||
self.marketBrowser = marketBrowser
|
||||
self.marketView = marketBrowser.marketView
|
||||
@@ -50,6 +54,9 @@ class ItemView(Display):
|
||||
self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.itemActivated)
|
||||
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 = []
|
||||
|
||||
def delaySearch(self, evt):
|
||||
@@ -90,7 +97,11 @@ class ItemView(Display):
|
||||
if sel.IsOk():
|
||||
# Get data field of the selected item (which is a marketGroup ID if anything was selected)
|
||||
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),
|
||||
# then it should have items in it and we want to request them
|
||||
if self.marketView.ItemHasChildren(sel) is False:
|
||||
@@ -102,11 +113,7 @@ class ItemView(Display):
|
||||
else:
|
||||
items = set()
|
||||
else:
|
||||
# If method was called but selection wasn't actually made or we have a hit on recently used modules
|
||||
if seldata == RECENTLY_USED_MODULES:
|
||||
items = self.sMkt.getRecentlyUsed()
|
||||
else:
|
||||
items = set()
|
||||
items = set()
|
||||
|
||||
# Fill store
|
||||
self.updateItemStore(items)
|
||||
@@ -114,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)
|
||||
if seldata == RECENTLY_USED_MODULES:
|
||||
self.marketBrowser.mode = 'recent'
|
||||
|
||||
if seldata == CHARGES_FOR_FIT:
|
||||
self.marketBrowser.mode = 'charges'
|
||||
|
||||
self.setToggles()
|
||||
if context == 'tree' and self.marketBrowser.settings.get('marketMGMarketSelectMode') == 1:
|
||||
@@ -122,6 +132,41 @@ class ItemView(Display):
|
||||
btn.setUserSelection(True)
|
||||
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):
|
||||
self.unfilteredStore = items
|
||||
|
||||
@@ -243,6 +288,7 @@ class ItemView(Display):
|
||||
|
||||
def columnBackground(self, colItem, item):
|
||||
if self.sFit.serviceFittingOptions["colorFitBySlot"]:
|
||||
return slotColourMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour()
|
||||
colorMap = slotColourMapDark if isDark() else slotColourMap
|
||||
return colorMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour()
|
||||
else:
|
||||
return self.GetBackgroundColour()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import wx
|
||||
|
||||
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
|
||||
|
||||
@@ -35,6 +35,9 @@ class MarketTree(wx.TreeCtrl):
|
||||
# Add recently used modules node
|
||||
rumIconId = self.addImage("market_small", "gui")
|
||||
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
|
||||
self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.expandLookup)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# noinspection PyPackageRequirements
|
||||
import wx
|
||||
|
||||
import config
|
||||
import gui.mainFrame
|
||||
from gui.bitmap_loader import BitmapLoader
|
||||
from gui.preferenceView import PreferenceView
|
||||
from service.esi import Esi
|
||||
from service.settings import EsiSettings
|
||||
|
||||
# noinspection PyPackageRequirements
|
||||
@@ -41,38 +43,68 @@ class PFEsiPref(PreferenceView):
|
||||
"due to 'Signature has expired' error")))
|
||||
mainSizer.Add(self.enforceJwtExpiration, 0, wx.ALL | wx.EXPAND, 5)
|
||||
|
||||
self.ssoServer = wx.CheckBox(panel, wx.ID_ANY, _t("Auto-login (starts local server)"), wx.DefaultPosition,
|
||||
wx.DefaultSize,
|
||||
0)
|
||||
self.ssoServer.SetToolTip(wx.ToolTip(_t("This allows the EVE SSO to callback to your local pyfa instance and complete the authentication process without manual intervention.")))
|
||||
mainSizer.Add(self.ssoServer, 0, wx.ALL | wx.EXPAND, 5)
|
||||
|
||||
rbSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.rbMode = wx.RadioBox(panel, -1, _t("Login Authentication Method"), wx.DefaultPosition, wx.DefaultSize,
|
||||
[_t('Local Server'), _t('Manual')], 1, wx.RA_SPECIFY_COLS)
|
||||
self.rbMode.SetItemToolTip(0, _t("This option starts a local webserver that EVE SSO Server will call back to"
|
||||
" with information about the character login."))
|
||||
self.rbMode.SetItemToolTip(1, _t("This option prompts users to copy and paste information to allow for"
|
||||
" character login. Use this if having issues with the local server."))
|
||||
|
||||
self.rbMode.SetSelection(self.settings.get('loginMode'))
|
||||
self.enforceJwtExpiration.SetValue(self.settings.get("enforceJwtExpiration" or True))
|
||||
self.enforceJwtExpiration.SetValue(self.settings.get("enforceJwtExpiration") or True)
|
||||
self.ssoServer.SetValue(True if self.settings.get("loginMode") == 0 else False)
|
||||
|
||||
rbSizer.Add(self.rbMode, 1, wx.TOP | wx.RIGHT, 5)
|
||||
mainSizer.Add(rbSizer, 0, wx.ALL | wx.EXPAND, 0)
|
||||
|
||||
self.rbMode.Bind(wx.EVT_RADIOBOX, self.OnModeChange)
|
||||
esiSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
|
||||
self.esiServer = wx.StaticText(panel, wx.ID_ANY, _t("Default SSO Server:"), wx.DefaultPosition, wx.DefaultSize, 0)
|
||||
|
||||
self.esiServer.Wrap(-1)
|
||||
|
||||
esiSizer.Add(self.esiServer, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
|
||||
|
||||
self.esiServer.SetToolTip(wx.ToolTip(_t('The source you choose will be used on connection.')))
|
||||
|
||||
self.chESIserver = wx.Choice(panel, choices=list(self.settings.keys()))
|
||||
|
||||
self.chESIserver.SetStringSelection(self.settings.get("server"))
|
||||
|
||||
esiSizer.Add(self.chESIserver, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 10)
|
||||
|
||||
mainSizer.Add(esiSizer, 0, wx.TOP | wx.RIGHT, 10)
|
||||
|
||||
self.chESIserver.Bind(wx.EVT_CHOICE, self.OnServerChange)
|
||||
self.enforceJwtExpiration.Bind(wx.EVT_CHECKBOX, self.OnEnforceChange)
|
||||
mainSizer.Add(rbSizer, 1, wx.ALL | wx.EXPAND, 0)
|
||||
self.ssoServer.Bind(wx.EVT_CHECKBOX, self.OnModeChange)
|
||||
|
||||
panel.SetSizer(mainSizer)
|
||||
|
||||
panel.Layout()
|
||||
|
||||
def OnTimeoutChange(self, event):
|
||||
self.settings.set('timeout', event.GetEventObject().GetValue())
|
||||
event.Skip()
|
||||
|
||||
def OnModeChange(self, event):
|
||||
self.settings.set('loginMode', event.GetInt())
|
||||
self.settings.set('loginMode', 0 if self.ssoServer.GetValue() else 1)
|
||||
event.Skip()
|
||||
|
||||
def OnEnforceChange(self, event):
|
||||
self.settings.set('enforceJwtExpiration', self.enforceJwtExpiration.GetValue())
|
||||
event.Skip()
|
||||
|
||||
def OnServerChange(self, event):
|
||||
# pass
|
||||
source = self.chESIserver.GetString(self.chESIserver.GetSelection())
|
||||
esiService = Esi.getInstance()
|
||||
# init servers
|
||||
esiService.init(config.supported_servers[source])
|
||||
self.settings.set("server", source)
|
||||
event.Skip()
|
||||
|
||||
def getImage(self):
|
||||
return BitmapLoader.getBitmap("eve", "gui")
|
||||
|
||||
|
||||
PFEsiPref.register()
|
||||
PFEsiPref.register()
|
||||
@@ -163,6 +163,10 @@ class PFListPane(wx.ScrolledWindow):
|
||||
def RemoveAllChildren(self):
|
||||
for widget in self._wList:
|
||||
widget.Destroy()
|
||||
# this forces the garbage collector to work properly by removing dangling references to objects which are still alive, otherwise widget cannot be gc-ed eventually causing GDI id exhaustion and crash
|
||||
for i in widget.__dict__.keys():
|
||||
widget.__dict__[i] =None
|
||||
del widget
|
||||
|
||||
self.Scroll(0, 0)
|
||||
self._wList = []
|
||||
|
||||
@@ -173,7 +173,7 @@ class FirepowerViewFull(StatsView):
|
||||
if hasSpool:
|
||||
lines.append("")
|
||||
lines.append(_t("Current") + ": {}".format(formatAmount(normal.total, prec, lowest, highest)))
|
||||
for dmgType in normal.names():
|
||||
for dmgType in normal.names(includePure=True):
|
||||
val = getattr(normal, dmgType, None)
|
||||
if val:
|
||||
lines.append("{}{}: {}%".format(
|
||||
@@ -215,13 +215,13 @@ class FirepowerViewFull(StatsView):
|
||||
val = val() if fit is not None else None
|
||||
preSpoolVal = preSpoolVal() if fit is not None else None
|
||||
fullSpoolVal = fullSpoolVal() if fit is not None else None
|
||||
if self._cachedValues[counter] != val:
|
||||
if self._cachedValues[counter] != getattr(val, 'total', None):
|
||||
tooltipText = dpsToolTip(val, preSpoolVal, fullSpoolVal, prec, lowest, highest)
|
||||
label.SetLabel(valueFormat.format(
|
||||
formatAmount(0 if val is None else val.total, prec, lowest, highest),
|
||||
"\u02e2" if hasSpoolUp(preSpoolVal, fullSpoolVal) else ""))
|
||||
label.SetToolTip(wx.ToolTip(tooltipText))
|
||||
self._cachedValues[counter] = val
|
||||
self._cachedValues[counter] = getattr(val, 'total', None)
|
||||
counter += 1
|
||||
|
||||
self.panel.Layout()
|
||||
|
||||
@@ -112,6 +112,7 @@ class TargetingMiscViewMinimal(StatsView):
|
||||
cargoNamesOrder = OrderedDict((
|
||||
("fleetHangarCapacity", _t("Fleet hangar")),
|
||||
("shipMaintenanceBayCapacity", _t("Maintenance bay")),
|
||||
("specialColonyResourcesHoldCapacity", _t("Infrastructure hold")),
|
||||
("specialAmmoHoldCapacity", _t("Ammo hold")),
|
||||
("specialFuelBayCapacity", _t("Fuel bay")),
|
||||
("specialShipHoldCapacity", _t("Ship hold")),
|
||||
@@ -134,6 +135,7 @@ class TargetingMiscViewMinimal(StatsView):
|
||||
cargoValues = {
|
||||
"main": lambda: fit.ship.getModifiedItemAttr("capacity"),
|
||||
"fleetHangarCapacity": lambda: fit.ship.getModifiedItemAttr("fleetHangarCapacity"),
|
||||
"specialColonyResourcesHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialColonyResourcesHoldCapacity"),
|
||||
"shipMaintenanceBayCapacity": lambda: fit.ship.getModifiedItemAttr("shipMaintenanceBayCapacity"),
|
||||
"specialAmmoHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialAmmoHoldCapacity"),
|
||||
"specialFuelBayCapacity": lambda: fit.ship.getModifiedItemAttr("specialFuelBayCapacity"),
|
||||
|
||||
@@ -197,6 +197,30 @@ class SignatureRadiusColumn(GraphColumn):
|
||||
SignatureRadiusColumn.register()
|
||||
|
||||
|
||||
class FullHpColumn(GraphColumn):
|
||||
|
||||
name = 'FullHP'
|
||||
stickPrefixToValue = True
|
||||
|
||||
def __init__(self, fittingView, params):
|
||||
super().__init__(fittingView, 68)
|
||||
|
||||
def _getValue(self, stuff):
|
||||
if isinstance(stuff, Fit):
|
||||
full_hp = stuff.hp.get('shield', 0) + stuff.hp.get('armor', 0) + stuff.hp.get('hull', 0)
|
||||
elif isinstance(stuff, TargetProfile):
|
||||
full_hp = stuff.hp
|
||||
else:
|
||||
full_hp = 0
|
||||
return full_hp, 'hp'
|
||||
|
||||
def _getFitTooltip(self):
|
||||
return 'Total raw HP'
|
||||
|
||||
|
||||
FullHpColumn.register()
|
||||
|
||||
|
||||
class ShieldAmountColumn(GraphColumn):
|
||||
|
||||
name = 'ShieldAmount'
|
||||
|
||||
@@ -93,8 +93,6 @@ class Miscellanea(ViewColumn):
|
||||
text = "{} dmg".format(formatAmount(dmg, 3, 0, 6))
|
||||
tooltip = "Raw damage done"
|
||||
return text, tooltip
|
||||
|
||||
pass
|
||||
elif itemGroup in ("Energy Weapon", "Hybrid Weapon", "Projectile Weapon", "Combat Drone", "Fighter Drone"):
|
||||
trackingSpeed = stuff.getModifiedItemAttr("trackingSpeed")
|
||||
optimalSig = stuff.getModifiedItemAttr("optimalSigRadius")
|
||||
@@ -167,7 +165,7 @@ class Miscellanea(ViewColumn):
|
||||
text = "{0}/s".format(formatAmount(capPerSec, 3, 0, 3))
|
||||
tooltip = "Energy neutralization per second"
|
||||
return text, tooltip
|
||||
elif itemGroup == "Salvager":
|
||||
elif itemGroup in ("Salvager", "Salvage Drone"):
|
||||
chance = stuff.getModifiedItemAttr("accessDifficultyBonus")
|
||||
if not chance:
|
||||
return "", None
|
||||
@@ -590,7 +588,7 @@ class Miscellanea(ViewColumn):
|
||||
text = "{0}/s".format(formatAmount(capPerSec, 3, 0, 3))
|
||||
tooltip = "Energy neutralization per second"
|
||||
return text, tooltip
|
||||
elif itemGroup in ("Micro Jump Drive", "Micro Jump Field Generators"):
|
||||
elif itemGroup in ("Micro Jump Drive", "Micro Jump Field Generators", "Capital Mobility Modules"):
|
||||
cycleTime = stuff.getModifiedItemAttr("duration") / 1000
|
||||
text = "{0}s".format(formatAmount(cycleTime, 3, 0, 3))
|
||||
tooltip = "Spoolup time"
|
||||
@@ -810,6 +808,19 @@ class Miscellanea(ViewColumn):
|
||||
text = "{}".format(formatAmount(scanStr, 4, 0, 3))
|
||||
tooltip = "Scan strength at {} AU scan range".format(formatAmount(baseRange, 3, 0, 0))
|
||||
return text, tooltip
|
||||
elif chargeGroup in ("SCARAB Breacher Pods",):
|
||||
duration = stuff.getModifiedChargeAttr("dotDuration") / 1000
|
||||
dmgAbs = stuff.getModifiedChargeAttr("dotMaxDamagePerTick")
|
||||
dmgRel = stuff.getModifiedChargeAttr("dotMaxHPPercentagePerTick")
|
||||
text = "{}/{}% over {}s".format(
|
||||
formatAmount(dmgAbs * duration, 3, 0, 6),
|
||||
formatAmount(dmgRel * duration, 3, 0, 6),
|
||||
formatAmount(duration, 0, 0, 0))
|
||||
fullDmgHp = dmgAbs / (dmgRel / 100)
|
||||
tooltip = (
|
||||
'Pure damage inflicted over time, minimum of absolute / relative\n'
|
||||
'Full DPS from {} target HP').format(formatAmount(fullDmgHp, 3, 0, 6))
|
||||
return text, tooltip
|
||||
else:
|
||||
return "", None
|
||||
else:
|
||||
|
||||
@@ -39,9 +39,10 @@ from gui.builtinViewColumns.state import State
|
||||
from gui.chrome_tabs import EVT_NOTEBOOK_PAGE_CHANGED
|
||||
from gui.contextMenu import ContextMenu
|
||||
from gui.utils.staticHelpers import DragDropHelper
|
||||
from gui.utils.dark import isDark
|
||||
from service.fit import Fit
|
||||
from service.market import Market
|
||||
from config import slotColourMap
|
||||
from config import slotColourMap, slotColourMapDark, errColor, errColorDark
|
||||
from gui.fitCommands.helpers import getSimilarModPositions
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
@@ -126,6 +127,10 @@ class FittingViewDrop(wx.DropTarget):
|
||||
if self.GetData():
|
||||
dragged_data = DragDropHelper.data
|
||||
# pyfalog.debug("fittingView: recieved drag: " + self.dropData.GetText())
|
||||
|
||||
if dragged_data is None:
|
||||
return t
|
||||
|
||||
data = dragged_data.split(':')
|
||||
self.dropFn(x, y, data)
|
||||
return t
|
||||
@@ -729,7 +734,10 @@ class FittingView(d.Display):
|
||||
event.Skip()
|
||||
|
||||
def slotColour(self, slot):
|
||||
return slotColourMap.get(slot) or self.GetBackgroundColour()
|
||||
if isDark():
|
||||
return slotColourMapDark.get(slot) or self.GetBackgroundColour()
|
||||
else:
|
||||
return slotColourMap.get(slot) or self.GetBackgroundColour()
|
||||
|
||||
def refresh(self, stuff):
|
||||
"""
|
||||
@@ -774,7 +782,7 @@ class FittingView(d.Display):
|
||||
|
||||
|
||||
if slotMap[mod.slot] or hasRestrictionOverriden: # Color too many modules as red
|
||||
self.SetItemBackgroundColour(i, wx.Colour(204, 51, 51))
|
||||
self.SetItemBackgroundColour(i, errColorDark if isDark() else errColor)
|
||||
elif sFit.serviceFittingOptions["colorFitBySlot"]: # Color by slot it enabled
|
||||
self.SetItemBackgroundColour(i, self.slotColour(mod.slot))
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ from service.market import Market
|
||||
|
||||
|
||||
def stripHtml(text):
|
||||
text = re.sub('<\s*br\s*/?\s*>', '\n', text)
|
||||
text = re.sub('</?[^/]+?(/\s*)?>', '', text)
|
||||
text = re.sub(r'<\s*br\s*/?\s*>', '\n', text)
|
||||
text = re.sub(r'</?[^/]+?(/\s*)?>', '', text)
|
||||
return text
|
||||
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ class CharacterEntityEditor(EntityEditor):
|
||||
sChar = Character.getInstance()
|
||||
|
||||
if entity.alphaCloneID:
|
||||
trimmed_name = re.sub('[ \(\u03B1\)]+$', '', name)
|
||||
trimmed_name = re.sub('[ \\(\u03B1\\)]+$', '', name)
|
||||
sChar.rename(entity, trimmed_name)
|
||||
else:
|
||||
sChar.rename(entity, name)
|
||||
@@ -371,7 +371,7 @@ class SkillTreeView(wx.Panel):
|
||||
bSizerButtons.AddStretchSpacer()
|
||||
|
||||
importExport = ((_t("Import skills from clipboard"), wx.ART_FILE_OPEN, "import"),
|
||||
(_t("Export skills from clipboard"), wx.ART_FILE_SAVE_AS, "export"))
|
||||
(_t("Export skills to clipboard"), wx.ART_FILE_SAVE_AS, "export"))
|
||||
|
||||
for tooltip, art, attr in importExport:
|
||||
bitmap = wx.ArtProvider.GetBitmap(art, wx.ART_BUTTON)
|
||||
@@ -446,6 +446,7 @@ class SkillTreeView(wx.Panel):
|
||||
|
||||
text = fromClipboard().strip()
|
||||
if text:
|
||||
sCharacter = Character.getInstance()
|
||||
char = self.charEditor.entityEditor.getActiveEntity()
|
||||
try:
|
||||
lines = text.splitlines()
|
||||
@@ -455,7 +456,7 @@ class SkillTreeView(wx.Panel):
|
||||
skill, level = s.rsplit(None, 1)[0], arabicOrRomanToInt(s.rsplit(None, 1)[1])
|
||||
skill = char.getSkill(skill)
|
||||
if skill:
|
||||
skill.setLevel(level, ignoreRestrict=True)
|
||||
sCharacter.changeLevel(char.ID, skill.item.ID, level)
|
||||
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
@@ -480,6 +481,35 @@ class SkillTreeView(wx.Panel):
|
||||
|
||||
toClipboard(list)
|
||||
|
||||
def exportSkillsSuperCondensed(self, evt):
|
||||
char = self.charEditor.entityEditor.getActiveEntity()
|
||||
|
||||
skills = {}
|
||||
explicit_levels = {}
|
||||
implicit_levels = {}
|
||||
for s in char.__class__.getSkillNameMap().keys():
|
||||
skill = char.getSkill(s)
|
||||
if skill.level < 1:
|
||||
continue
|
||||
skills[skill.item.ID] = skill
|
||||
explicit_levels[skill.item.ID] = skill.level
|
||||
|
||||
for skill in skills.values():
|
||||
for req_skill, level in skill.item.requiredSkills.items():
|
||||
if req_skill.ID not in implicit_levels or implicit_levels[req_skill.ID] < level:
|
||||
implicit_levels[req_skill.ID] = level
|
||||
|
||||
condensed = {}
|
||||
for typeID, level in explicit_levels.items():
|
||||
if typeID not in implicit_levels or implicit_levels[typeID] < level:
|
||||
condensed[skills[typeID].item.name] = level
|
||||
|
||||
lines = []
|
||||
for skill in sorted(condensed):
|
||||
lines.append(f'{skill}\t{condensed[skill]}')
|
||||
|
||||
toClipboard('\n'.join(lines))
|
||||
|
||||
def onSecStatus(self, event):
|
||||
sChar = Character.getInstance()
|
||||
char = self.charEditor.entityEditor.getActiveEntity()
|
||||
@@ -806,7 +836,12 @@ class APIView(wx.Panel):
|
||||
|
||||
self.SetSizer(pmainSizer)
|
||||
self.Layout()
|
||||
self.ssoListChanged(None)
|
||||
try:
|
||||
self.ssoListChanged(None)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
pass
|
||||
|
||||
def ssoCharChanged(self, event):
|
||||
sChar = Character.getInstance()
|
||||
@@ -858,7 +893,7 @@ class APIView(wx.Panel):
|
||||
noneID = self.charChoice.Append(_t("None"), None)
|
||||
|
||||
for char in ssoChars:
|
||||
currId = self.charChoice.Append(char.characterName, char.ID)
|
||||
currId = self.charChoice.Append(char.characterDisplay, char.ID)
|
||||
|
||||
if sso is not None and char.ID == sso.ID:
|
||||
self.charChoice.SetSelection(currId)
|
||||
@@ -912,7 +947,7 @@ class SecStatusDialog(wx.Dialog):
|
||||
self.m_staticText1.Wrap(-1)
|
||||
bSizer1.Add(self.m_staticText1, 1, wx.ALL | wx.EXPAND, 5)
|
||||
|
||||
self.floatSpin = FloatSpin(self, value=sec, min_val=-5.0, max_val=5.0, increment=0.1, digits=2, size=(-1, -1))
|
||||
self.floatSpin = FloatSpin(self, value=sec, min_val=-10.0, max_val=5.0, increment=0.1, digits=2, size=(-1, -1))
|
||||
bSizer1.Add(self.floatSpin, 0, wx.ALIGN_CENTER | wx.ALL, 5)
|
||||
|
||||
btnOk = wx.Button(self, wx.ID_OK)
|
||||
|
||||
@@ -29,8 +29,9 @@ class Display(wx.ListCtrl):
|
||||
DEFAULT_COLS = None
|
||||
|
||||
def __init__(self, parent, size=wx.DefaultSize, style=0):
|
||||
|
||||
wx.ListCtrl.__init__(self, parent, size=size, style=wx.LC_REPORT | style)
|
||||
wx.ListCtrl.__init__(self)
|
||||
self.EnableSystemTheme(False)
|
||||
self.Create(parent, size=size, style=wx.LC_REPORT | style)
|
||||
self.imageList = CachingImageList(16, 16)
|
||||
self.SetImageList(self.imageList, wx.IMAGE_LIST_SMALL)
|
||||
self.activeColumns = []
|
||||
|
||||
@@ -96,7 +96,7 @@ class EveFittings(AuxiliaryFrame):
|
||||
|
||||
self.charChoice.Clear()
|
||||
for char in chars:
|
||||
self.charChoice.Append(char.characterName, char.ID)
|
||||
self.charChoice.Append(char.characterDisplay, char.ID)
|
||||
if len(chars) > 0:
|
||||
self.charChoice.SetSelection(0)
|
||||
|
||||
@@ -227,21 +227,6 @@ class EveFittings(AuxiliaryFrame):
|
||||
self.fitView.update([])
|
||||
|
||||
|
||||
class ESIServerExceptionHandler:
|
||||
def __init__(self, parentWindow, ex):
|
||||
pyfalog.error(ex)
|
||||
with wx.MessageDialog(
|
||||
parentWindow,
|
||||
_t("There was an issue starting up the localized server, try setting "
|
||||
"Login Authentication Method to Manual by going to Preferences -> EVE SS0 -> "
|
||||
"Login Authentication Method. If this doesn't fix the problem please file an "
|
||||
"issue on Github."),
|
||||
_t("Add Character Error"),
|
||||
wx.OK | wx.ICON_ERROR
|
||||
) as dlg:
|
||||
dlg.ShowModal()
|
||||
|
||||
|
||||
class ESIExceptionHandler:
|
||||
# todo: make this a generate excetpion handler for all calls
|
||||
def __init__(self, ex):
|
||||
@@ -348,7 +333,7 @@ class ExportToEve(AuxiliaryFrame):
|
||||
|
||||
self.charChoice.Clear()
|
||||
for char in chars:
|
||||
self.charChoice.Append(char.characterName, char.ID)
|
||||
self.charChoice.Append(char.characterDisplay, char.ID)
|
||||
|
||||
if len(chars) > 0:
|
||||
self.charChoice.SetSelection(0)
|
||||
@@ -434,6 +419,7 @@ class SsoCharacterMgmt(AuxiliaryFrame):
|
||||
|
||||
self.lcCharacters.InsertColumn(0, heading=_t('Character'))
|
||||
self.lcCharacters.InsertColumn(1, heading=_t('Character ID'))
|
||||
self.lcCharacters.InsertColumn(2, heading=_t('Server'))
|
||||
|
||||
self.popCharList()
|
||||
|
||||
@@ -496,9 +482,11 @@ class SsoCharacterMgmt(AuxiliaryFrame):
|
||||
self.lcCharacters.InsertItem(index, char.characterName)
|
||||
self.lcCharacters.SetItem(index, 1, str(char.characterID))
|
||||
self.lcCharacters.SetItemData(index, char.ID)
|
||||
self.lcCharacters.SetItem(index, 2, char.server or "<unknown>")
|
||||
|
||||
self.lcCharacters.SetColumnWidth(0, wx.LIST_AUTOSIZE)
|
||||
self.lcCharacters.SetColumnWidth(1, wx.LIST_AUTOSIZE)
|
||||
self.lcCharacters.SetColumnWidth(2, wx.LIST_AUTOSIZE)
|
||||
|
||||
def addChar(self, event):
|
||||
try:
|
||||
@@ -506,8 +494,6 @@ class SsoCharacterMgmt(AuxiliaryFrame):
|
||||
sEsi.login()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except Exception as ex:
|
||||
ESIServerExceptionHandler(self, ex)
|
||||
|
||||
def delChar(self, event):
|
||||
item = self.lcCharacters.GetFirstSelected()
|
||||
|
||||
@@ -112,7 +112,7 @@ class FitBrowserLiteDialog(wx.Dialog):
|
||||
return True
|
||||
|
||||
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:
|
||||
if isMatch(fit, searchTokens):
|
||||
matches.append(fit)
|
||||
|
||||
@@ -12,6 +12,7 @@ from .gui.cargo.remove import GuiRemoveCargosCommand
|
||||
from .gui.commandFit.add import GuiAddCommandFitsCommand
|
||||
from .gui.commandFit.remove import GuiRemoveCommandFitsCommand
|
||||
from .gui.commandFit.toggleStates import GuiToggleCommandFitStatesCommand
|
||||
from .gui.fitPilotSecurity import GuiChangeFitPilotSecurityCommand
|
||||
from .gui.fitRename import GuiRenameFitCommand
|
||||
from .gui.fitRestrictionToggle import GuiToggleFittingRestrictionsCommand
|
||||
from .gui.fitSystemSecurity import GuiChangeFitSystemSecurityCommand
|
||||
|
||||
32
gui/fitCommands/calc/fitPilotSecurity.py
Normal 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()
|
||||
36
gui/fitCommands/gui/fitPilotSecurity.py
Normal 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
|
||||
164
gui/mainFrame.py
@@ -61,10 +61,11 @@ from gui.statsPane import StatsPane
|
||||
from gui.targetProfileEditor import TargetProfileEditor
|
||||
from gui.updateDialog import UpdateDialog
|
||||
from gui.utils.clipboard import fromClipboard
|
||||
from gui.utils.progressHelper import ProgressHelper
|
||||
from service.character import Character
|
||||
from service.esi import Esi
|
||||
from service.fit import Fit
|
||||
from service.port import IPortUser, Port
|
||||
from service.port import Port
|
||||
from service.price import Price
|
||||
from service.settings import HTMLExportSettings, SettingsProvider
|
||||
from service.update import Update
|
||||
@@ -130,7 +131,6 @@ class OpenFitsThread(threading.Thread):
|
||||
self.running = False
|
||||
|
||||
|
||||
# todo: include IPortUser again
|
||||
class MainFrame(wx.Frame):
|
||||
__instance = None
|
||||
|
||||
@@ -845,14 +845,15 @@ class MainFrame(wx.Frame):
|
||||
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
|
||||
) as dlg:
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
self.progressDialog = wx.ProgressDialog(
|
||||
_t("Importing fits"),
|
||||
" " * 100, # set some arbitrary spacing to create width in window
|
||||
parent=self,
|
||||
style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL
|
||||
)
|
||||
Port.importFitsThreaded(dlg.GetPaths(), self)
|
||||
self.progressDialog.ShowModal()
|
||||
# set some arbitrary spacing to create width in window
|
||||
progress = ProgressHelper(message=" " * 100, callback=self._openAfterImport)
|
||||
call = (Port.importFitsThreaded, [dlg.GetPaths(), progress], {})
|
||||
self.handleProgress(
|
||||
title=_t("Importing fits"),
|
||||
style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_APP_MODAL | wx.PD_AUTO_HIDE,
|
||||
call=call,
|
||||
progress=progress,
|
||||
errMsgLbl=_t("Import Error"))
|
||||
|
||||
def backupToXml(self, event):
|
||||
""" Back up all fits to EVE XML file """
|
||||
@@ -863,32 +864,30 @@ class MainFrame(wx.Frame):
|
||||
_t("Save Backup As..."),
|
||||
wildcard=_t("EVE XML fitting file") + " (*.xml)|*.xml",
|
||||
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
|
||||
defaultFile=defaultFile,
|
||||
) as dlg:
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
filePath = dlg.GetPath()
|
||||
defaultFile=defaultFile) as fileDlg:
|
||||
if fileDlg.ShowModal() == wx.ID_OK:
|
||||
filePath = fileDlg.GetPath()
|
||||
if '.' not in os.path.basename(filePath):
|
||||
filePath += ".xml"
|
||||
|
||||
sFit = Fit.getInstance()
|
||||
max_ = sFit.countAllFits()
|
||||
|
||||
self.progressDialog = wx.ProgressDialog(
|
||||
_t("Backup fits"),
|
||||
_t("Backing up {} fits to: {}").format(max_, filePath),
|
||||
maximum=max_,
|
||||
parent=self,
|
||||
style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL
|
||||
)
|
||||
Port.backupFits(filePath, self)
|
||||
self.progressDialog.ShowModal()
|
||||
fitAmount = Fit.getInstance().countAllFits()
|
||||
progress = ProgressHelper(
|
||||
message=_t("Backing up {} fits to: {}").format(fitAmount, filePath),
|
||||
maximum=fitAmount + 1)
|
||||
call = (Port.backupFits, [filePath, progress], {})
|
||||
self.handleProgress(
|
||||
title=_t("Backup fits"),
|
||||
style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL | wx.PD_AUTO_HIDE,
|
||||
call=call,
|
||||
progress=progress,
|
||||
errMsgLbl=_t("Export Error"))
|
||||
|
||||
def exportHtml(self, event):
|
||||
from gui.utils.exportHtml import exportHtml
|
||||
|
||||
sFit = Fit.getInstance()
|
||||
settings = HTMLExportSettings.getInstance()
|
||||
|
||||
max_ = sFit.countAllFits()
|
||||
path = settings.getPath()
|
||||
|
||||
if not os.path.isdir(os.path.dirname(path)):
|
||||
@@ -903,82 +902,44 @@ class MainFrame(wx.Frame):
|
||||
) as dlg:
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
return
|
||||
progress = ProgressHelper(
|
||||
message=_t("Generating HTML file at: {}").format(path),
|
||||
maximum=sFit.countAllFits() + 1)
|
||||
call = (exportHtml.getInstance().refreshFittingHtml, [True, progress], {})
|
||||
self.handleProgress(
|
||||
title=_t("Backup fits"),
|
||||
style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME,
|
||||
call=call,
|
||||
progress=progress)
|
||||
|
||||
self.progressDialog = wx.ProgressDialog(
|
||||
_t("Backup fits"),
|
||||
_t("Generating HTML file at: {}").format(path),
|
||||
maximum=max_, parent=self,
|
||||
style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME)
|
||||
|
||||
exportHtml.getInstance().refreshFittingHtml(True, self.backupCallback)
|
||||
self.progressDialog.ShowModal()
|
||||
|
||||
def backupCallback(self, info):
|
||||
if info == -1:
|
||||
self.closeProgressDialog()
|
||||
else:
|
||||
self.progressDialog.Update(info)
|
||||
|
||||
def on_port_process_start(self):
|
||||
# flag for progress dialog.
|
||||
self.__progress_flag = True
|
||||
|
||||
def on_port_processing(self, action, data=None):
|
||||
# 2017/03/29 NOTE: implementation like interface
|
||||
wx.CallAfter(
|
||||
self._on_port_processing, action, data
|
||||
)
|
||||
|
||||
return self.__progress_flag
|
||||
|
||||
def _on_port_processing(self, action, data):
|
||||
"""
|
||||
While importing fits from file, the logic calls back to this function to
|
||||
update progress bar to show activity. XML files can contain multiple
|
||||
ships with multiple fits, whereas EFT cfg files contain many fits of
|
||||
a single ship. When iterating through the files, we update the message
|
||||
when we start a new file, and then Pulse the progress bar with every fit
|
||||
that is processed.
|
||||
|
||||
action : a flag that lets us know how to deal with :data
|
||||
None: Pulse the progress bar
|
||||
1: Replace message with data
|
||||
other: Close dialog and handle based on :action (-1 open fits, -2 display error)
|
||||
"""
|
||||
_message = None
|
||||
if action & IPortUser.ID_ERROR:
|
||||
self.closeProgressDialog()
|
||||
_message = _t("Import Error") if action & IPortUser.PROCESS_IMPORT else _t("Export Error")
|
||||
def handleProgress(self, title, style, call, progress, errMsgLbl=None):
|
||||
extraArgs = {}
|
||||
if progress.maximum is not None:
|
||||
extraArgs['maximum'] = progress.maximum
|
||||
with wx.ProgressDialog(
|
||||
parent=self,
|
||||
title=title,
|
||||
message=progress.message,
|
||||
style=style,
|
||||
**extraArgs
|
||||
) as dlg:
|
||||
func, args, kwargs = call
|
||||
func(*args, **kwargs)
|
||||
while progress.working:
|
||||
wx.MilliSleep(250)
|
||||
wx.Yield()
|
||||
(progress.dlgWorking, skip) = dlg.Update(progress.current, progress.message)
|
||||
if progress.error and errMsgLbl:
|
||||
with wx.MessageDialog(
|
||||
self,
|
||||
_t("The following error was generated") +
|
||||
f"\n\n{data}\n\n" +
|
||||
f"\n\n{progress.error}\n\n" +
|
||||
_t("Be aware that already processed fits were not saved"),
|
||||
_message, wx.OK | wx.ICON_ERROR
|
||||
errMsgLbl, wx.OK | wx.ICON_ERROR
|
||||
) as dlg:
|
||||
dlg.ShowModal()
|
||||
return
|
||||
|
||||
# data is str
|
||||
if action & IPortUser.PROCESS_IMPORT:
|
||||
if action & IPortUser.ID_PULSE:
|
||||
_message = ()
|
||||
# update message
|
||||
elif action & IPortUser.ID_UPDATE: # and data != self.progressDialog.message:
|
||||
_message = data
|
||||
|
||||
if _message is not None:
|
||||
self.__progress_flag, _unuse = self.progressDialog.Pulse(_message)
|
||||
else:
|
||||
self.closeProgressDialog()
|
||||
if action & IPortUser.ID_DONE:
|
||||
self._openAfterImport(data)
|
||||
# data is tuple(int, str)
|
||||
elif action & IPortUser.PROCESS_EXPORT:
|
||||
if action & IPortUser.ID_DONE:
|
||||
self.closeProgressDialog()
|
||||
else:
|
||||
self.__progress_flag, _unuse = self.progressDialog.Update(data[0], data[1])
|
||||
elif progress.callback:
|
||||
progress.callback(*progress.cbArgs)
|
||||
|
||||
def _openAfterImport(self, fits):
|
||||
if len(fits) > 0:
|
||||
@@ -988,6 +949,8 @@ class MainFrame(wx.Frame):
|
||||
wx.PostEvent(self.shipBrowser, Stage3Selected(shipID=fit.shipID, back=True))
|
||||
else:
|
||||
fits.sort(key=lambda _fit: (_fit.ship.item.name, _fit.name))
|
||||
# Show 100 fits max
|
||||
fits = fits[:100]
|
||||
results = []
|
||||
for fit in fits:
|
||||
results.append((
|
||||
@@ -999,15 +962,6 @@ class MainFrame(wx.Frame):
|
||||
))
|
||||
wx.PostEvent(self.shipBrowser, ImportSelected(fits=results, back=True))
|
||||
|
||||
def closeProgressDialog(self):
|
||||
# Windows apparently handles ProgressDialogs differently. We can
|
||||
# simply Destroy it here, but for other platforms we must Close it
|
||||
if 'wxMSW' in wx.PlatformInfo:
|
||||
self.progressDialog.Destroy()
|
||||
else:
|
||||
self.progressDialog.EndModal(wx.ID_OK)
|
||||
self.progressDialog.Close()
|
||||
|
||||
def importCharacter(self, event):
|
||||
""" Imports character XML file from EVE API """
|
||||
with wx.FileDialog(
|
||||
|
||||
@@ -146,7 +146,7 @@ class MarketBrowser(wx.Panel):
|
||||
setting = self.settings.get('marketMGSearchMode')
|
||||
# We turn on all meta buttons for the duration of search/recents
|
||||
if setting == 1:
|
||||
if newMode in ('search', 'recent'):
|
||||
if newMode in ('search', 'recent', 'charges'):
|
||||
for btn in self.metaButtons:
|
||||
btn.setUserSelection(True)
|
||||
if newMode == 'normal':
|
||||
|
||||
@@ -191,7 +191,7 @@ class ShipBrowser(wx.Panel):
|
||||
"amarr", "caldari", "gallente", "minmatar",
|
||||
"sisters", "ore", "concord",
|
||||
"serpentis", "angel", "blood", "sansha", "guristas", "mordu",
|
||||
"jove", "triglavian", "upwell", None
|
||||
"deathless", "jove", "triglavian", "upwell", None
|
||||
]
|
||||
|
||||
def raceNameKey(self, ship):
|
||||
|
||||
@@ -2,30 +2,48 @@ import wx
|
||||
import gui.mainFrame
|
||||
import webbrowser
|
||||
import gui.globalEvents as GE
|
||||
import config
|
||||
import time
|
||||
|
||||
from service.settings import EsiSettings
|
||||
|
||||
_t = wx.GetTranslation
|
||||
|
||||
|
||||
class SsoLogin(wx.Dialog):
|
||||
|
||||
def __init__(self):
|
||||
mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
|
||||
def __init__(self, server: config.ApiServer, start_local_server=True):
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
from service.esi import Esi
|
||||
super().__init__(
|
||||
mainFrame, id=wx.ID_ANY, title=_t("SSO Login"), style=wx.DEFAULT_DIALOG_STYLE,
|
||||
self.mainFrame, id=wx.ID_ANY, title=_t("SSO Login"), style=wx.DEFAULT_DIALOG_STYLE,
|
||||
size=wx.Size(450, 240) if "wxGTK" in wx.PlatformInfo else wx.Size(400, 240))
|
||||
|
||||
bSizer1 = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
text = wx.StaticText(self, wx.ID_ANY, _t("Copy and paste the block of text provided by pyfa.io"))
|
||||
bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10)
|
||||
if start_local_server:
|
||||
text = wx.StaticText(self, wx.ID_ANY, _t("Waiting for character login through EVE Single Sign-On."))
|
||||
bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10)
|
||||
bSizer1.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.EXPAND, 15)
|
||||
text = wx.StaticText(self, wx.ID_ANY, _t("If auto-login fails, copy and paste the token provided by pyfa.io"))
|
||||
bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10)
|
||||
elif server.name == "Serenity":
|
||||
text = wx.StaticText(self, wx.ID_ANY, _t("Please copy and paste the url when your authorization is completed"))
|
||||
bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10)
|
||||
|
||||
else:
|
||||
text = wx.StaticText(self, wx.ID_ANY, _t("Please copy and paste the token provided by pyfa.io"))
|
||||
bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10)
|
||||
|
||||
self.ssoInfoCtrl = wx.TextCtrl(self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, (-1, -1), style=wx.TE_MULTILINE)
|
||||
self.ssoInfoCtrl.SetFont(wx.Font(8, wx.FONTFAMILY_TELETYPE, wx.NORMAL, wx.NORMAL))
|
||||
self.ssoInfoCtrl.Layout()
|
||||
self.ssoInfoCtrl.Bind(wx.EVT_TEXT, self.OnTextEnter)
|
||||
|
||||
bSizer1.Add(self.ssoInfoCtrl, 1, wx.LEFT | wx.RIGHT | wx.EXPAND, 10)
|
||||
|
||||
self.Esisettings = EsiSettings.getInstance()
|
||||
|
||||
bSizer3 = wx.BoxSizer(wx.VERTICAL)
|
||||
bSizer3.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.BOTTOM | wx.EXPAND, 10)
|
||||
|
||||
@@ -34,51 +52,43 @@ class SsoLogin(wx.Dialog):
|
||||
|
||||
self.SetSizer(bSizer1)
|
||||
self.Center()
|
||||
|
||||
from service.esi import Esi
|
||||
|
||||
self.sEsi = Esi.getInstance()
|
||||
uri = self.sEsi.get_login_uri(None)
|
||||
webbrowser.open(uri)
|
||||
|
||||
|
||||
class SsoLoginServer(wx.Dialog):
|
||||
|
||||
def __init__(self, port):
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
super().__init__(self.mainFrame, id=wx.ID_ANY, title=_t("SSO Login"), size=(-1, -1), style=wx.DEFAULT_DIALOG_STYLE)
|
||||
|
||||
from service.esi import Esi
|
||||
|
||||
self.sEsi = Esi.getInstance()
|
||||
serverAddr = self.sEsi.startServer(port)
|
||||
|
||||
serverAddr = self.sEsi.startServer(0) if start_local_server else None
|
||||
uri = self.sEsi.get_login_uri(serverAddr)
|
||||
|
||||
bSizer1 = wx.BoxSizer(wx.VERTICAL)
|
||||
self.mainFrame.Bind(GE.EVT_SSO_LOGIN, self.OnLogin)
|
||||
self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
|
||||
if server.name == "Serenity":
|
||||
webbrowser.open(config.SSO_LOGOFF_SERENITY)
|
||||
time.sleep(1)
|
||||
|
||||
text = wx.StaticText(self, wx.ID_ANY, _t("Waiting for character login through EVE Single Sign-On."))
|
||||
bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10)
|
||||
|
||||
bSizer3 = wx.BoxSizer(wx.VERTICAL)
|
||||
bSizer3.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.BOTTOM | wx.EXPAND, 10)
|
||||
|
||||
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.CANCEL), 0, wx.EXPAND)
|
||||
bSizer1.Add(bSizer3, 0, wx.BOTTOM | wx.RIGHT | wx.LEFT | wx.EXPAND, 10)
|
||||
|
||||
self.SetSizer(bSizer1)
|
||||
self.Fit()
|
||||
self.Center()
|
||||
self.okBtn = self.FindWindow(wx.ID_OK)
|
||||
self.okBtn.Enable(False)
|
||||
# Ensure we clean up once they hit the "OK" button
|
||||
self.okBtn.Bind(wx.EVT_BUTTON, self.OnDestroy)
|
||||
|
||||
webbrowser.open(uri)
|
||||
|
||||
self.mainFrame.Bind(GE.EVT_SSO_LOGIN, self.OnLogin)
|
||||
# Ensure we clean up if ESC is pressed
|
||||
self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
|
||||
|
||||
def OnTextEnter(self, event):
|
||||
t = event.String.strip()
|
||||
if t == "":
|
||||
self.okBtn.Enable(False)
|
||||
else:
|
||||
self.okBtn.Enable(True)
|
||||
event.Skip()
|
||||
|
||||
def OnLogin(self, event):
|
||||
self.EndModal(wx.ID_OK)
|
||||
# This would normally happen if it was logged in via server auto-login. In this case, the modal is done, we effectively want to cancel out
|
||||
self.EndModal(wx.ID_CANCEL)
|
||||
event.Skip()
|
||||
|
||||
def OnDestroy(self, event):
|
||||
# Clean up by unbinding some events and stopping the server
|
||||
self.mainFrame.Unbind(GE.EVT_SSO_LOGIN, handler=self.OnLogin)
|
||||
if self:
|
||||
self.Unbind(wx.EVT_WINDOW_DESTROY, handler=self.OnDestroy)
|
||||
self.sEsi.stopServer()
|
||||
event.Skip()
|
||||
|
||||
@@ -123,13 +123,14 @@ class TargetProfileEditor(AuxiliaryFrame):
|
||||
ATTRIBUTES = OrderedDict([
|
||||
('maxVelocity', (_t('Maximum speed'), 'm/s')),
|
||||
('signatureRadius', (_t('Signature radius\nLeave blank for infinitely big value'), 'm')),
|
||||
('radius', (_t('Radius'), 'm'))])
|
||||
('radius', (_t('Radius\nThe radius of the sphere that represents a ship/drone in space. Affects range calculations.'), 'm')),
|
||||
('hp', (_t('Total HP\nAffects how much damage breacher pods can do. Leave blank for infinitely big value'), 'hp'))])
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(
|
||||
parent, id=wx.ID_ANY, title=_t("Target Profile Editor"), resizeable=True,
|
||||
# Dropdown list widget is scaled to its longest content line on GTK, adapt to that
|
||||
size=wx.Size(500, 240) if "wxGTK" in wx.PlatformInfo else wx.Size(350, 240))
|
||||
size=wx.Size(630, 240) if "wxGTK" in wx.PlatformInfo else wx.Size(450, 240))
|
||||
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
self.block = False
|
||||
@@ -145,36 +146,29 @@ class TargetProfileEditor(AuxiliaryFrame):
|
||||
|
||||
contentSizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
resistEditSizer = wx.FlexGridSizer(2, 6, 0, 2)
|
||||
resistEditSizer.AddGrowableCol(0)
|
||||
resistEditSizer.AddGrowableCol(5)
|
||||
resistEditSizer.SetFlexibleDirection(wx.BOTH)
|
||||
resistEditSizer.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED)
|
||||
resistEditSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
resistEditSizer.AddStretchSpacer()
|
||||
|
||||
defSize = wx.Size(50, -1)
|
||||
defSize = wx.Size(70, -1)
|
||||
|
||||
for i, type_ in enumerate(self.DAMAGE_TYPES):
|
||||
if i % 2:
|
||||
style = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.LEFT
|
||||
border = 25
|
||||
else:
|
||||
style = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT
|
||||
border = 5
|
||||
for type_ in self.DAMAGE_TYPES:
|
||||
leftPad = 25 if type_ != list(self.DAMAGE_TYPES)[0] else 0
|
||||
ttText = self.DAMAGE_TYPES[type_]
|
||||
bmp = wx.StaticBitmap(self, wx.ID_ANY, BitmapLoader.getBitmap("%s_big" % type_, "gui"))
|
||||
bmp.SetToolTip(wx.ToolTip(ttText))
|
||||
resistEditSizer.Add(bmp, 0, style, border)
|
||||
resistEditSizer.Add(bmp, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, leftPad)
|
||||
# set text edit
|
||||
editBox = FloatBox(parent=self, id=wx.ID_ANY, value=None, pos=wx.DefaultPosition, size=defSize, validator=ResistValidator())
|
||||
editBox = FloatBox(parent=self, id=wx.ID_ANY, value=None, pos=wx.DefaultPosition, size=defSize)
|
||||
editBox.SetToolTip(wx.ToolTip(ttText))
|
||||
self.Bind(event=wx.EVT_TEXT, handler=self.OnFieldChanged, source=editBox)
|
||||
setattr(self, '{}Edit'.format(type_), editBox)
|
||||
resistEditSizer.Add(editBox, 0, wx.BOTTOM | wx.TOP | wx.ALIGN_CENTER_VERTICAL, 5)
|
||||
resistEditSizer.Add(editBox, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
|
||||
unit = wx.StaticText(self, wx.ID_ANY, "%", wx.DefaultPosition, wx.DefaultSize, 0)
|
||||
unit.SetToolTip(wx.ToolTip(ttText))
|
||||
resistEditSizer.Add(unit, 0, wx.BOTTOM | wx.TOP | wx.ALIGN_CENTER_VERTICAL, 5)
|
||||
|
||||
contentSizer.Add(resistEditSizer, 0, wx.EXPAND | wx.ALL, 5)
|
||||
resistEditSizer.AddStretchSpacer()
|
||||
contentSizer.Add(resistEditSizer, 1, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
miscAttrSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
miscAttrSizer.AddStretchSpacer()
|
||||
|
||||
@@ -66,8 +66,8 @@ class UpdateDialog(wx.Dialog):
|
||||
self.browser.Bind(wx.html2.EVT_WEBVIEW_NEWWINDOW, self.OnNewWindow)
|
||||
|
||||
link_patterns = [
|
||||
(re.compile("#(\d+)", re.I), r"https://github.com/pyfa-org/Pyfa/issues/\1"),
|
||||
(re.compile("@(\w+)", re.I), r"https://github.com/\1")
|
||||
(re.compile(r"#(\d+)", re.I), r"https://github.com/pyfa-org/Pyfa/issues/\1"),
|
||||
(re.compile(r"@(\w+)", re.I), r"https://github.com/\1")
|
||||
]
|
||||
|
||||
markdowner = markdown2.Markdown(
|
||||
|
||||
12
gui/utils/dark.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import wx
|
||||
|
||||
|
||||
def isDark():
|
||||
if 'wxMSW' in wx.PlatformInfo:
|
||||
return False
|
||||
try:
|
||||
return wx.SystemSettings.GetAppearance().IsDark()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
@@ -26,20 +26,20 @@ class exportHtml:
|
||||
def __init__(self):
|
||||
self.thread = exportHtmlThread()
|
||||
|
||||
def refreshFittingHtml(self, force=False, callback=False):
|
||||
def refreshFittingHtml(self, force=False, progress=None):
|
||||
settings = HTMLExportSettings.getInstance()
|
||||
|
||||
if force or settings.getEnabled():
|
||||
self.thread.stop()
|
||||
self.thread = exportHtmlThread(callback)
|
||||
self.thread = exportHtmlThread(progress)
|
||||
self.thread.start()
|
||||
|
||||
|
||||
class exportHtmlThread(threading.Thread):
|
||||
def __init__(self, callback=False):
|
||||
def __init__(self, progress=False):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = "HTMLExport"
|
||||
self.callback = callback
|
||||
self.progress = progress
|
||||
self.stopRunning = False
|
||||
|
||||
def stop(self):
|
||||
@@ -72,11 +72,13 @@ class exportHtmlThread(threading.Thread):
|
||||
pass
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except Exception as ex:
|
||||
pass
|
||||
|
||||
if self.callback:
|
||||
wx.CallAfter(self.callback, -1)
|
||||
except Exception as e:
|
||||
if self.progress:
|
||||
self.progress.error = f'{e}'
|
||||
finally:
|
||||
if self.progress:
|
||||
self.progress.current += 1
|
||||
self.progress.workerWorking = False
|
||||
|
||||
def generateFullHTML(self, sMkt, sFit, dnaUrl):
|
||||
""" Generate the complete HTML with styling and javascript """
|
||||
@@ -171,13 +173,13 @@ class exportHtmlThread(threading.Thread):
|
||||
</head>
|
||||
<body>
|
||||
<div id="canvas" data-role="page">
|
||||
<div style="text-align: center;"><strong>Last updated:</strong> %s <small>(<span class="timer"></span>)</small></div>
|
||||
<div data-role="header">
|
||||
<h1>Pyfa fits</h1>
|
||||
<h1>Pyfa fits by Group</h1>
|
||||
</div>
|
||||
<div data-role="content">
|
||||
<div style="text-align: center;"><strong>Last updated:</strong> %s <small>(<span class="timer"></span>)</small></div>
|
||||
|
||||
""" % (time.time(), dnaUrl, localDate)
|
||||
|
||||
HTML += ' <ul data-role="listview" class="ui-listview-outer" data-inset="true" data-filter="true">\n'
|
||||
categoryList = list(sMkt.getShipRoot())
|
||||
categoryList.sort(key=lambda _ship: _ship.name)
|
||||
@@ -214,7 +216,9 @@ class exportHtmlThread(threading.Thread):
|
||||
eftFit = Port.exportEft(getFit(fit[0]), options={
|
||||
PortEftOptions.IMPLANTS: True,
|
||||
PortEftOptions.MUTATIONS: True,
|
||||
PortEftOptions.LOADED_CHARGES: True})
|
||||
PortEftOptions.LOADED_CHARGES: True,
|
||||
PortEftOptions.BOOSTERS: True,
|
||||
PortEftOptions.CARGO: True})
|
||||
|
||||
HTMLfit = (
|
||||
' <li data-role="collapsible" data-iconpos="right" data-shadow="false" '
|
||||
@@ -234,8 +238,8 @@ class exportHtmlThread(threading.Thread):
|
||||
pyfalog.warning("Failed to export line")
|
||||
continue
|
||||
finally:
|
||||
if self.callback:
|
||||
wx.CallAfter(self.callback, count)
|
||||
if self.progress:
|
||||
self.progress.current = count
|
||||
count += 1
|
||||
HTMLgroup += HTMLship + (' </ul>\n'
|
||||
' </li>\n')
|
||||
@@ -254,6 +258,68 @@ class exportHtmlThread(threading.Thread):
|
||||
HTML += """
|
||||
</ul>
|
||||
</div>
|
||||
<div data-role="header">
|
||||
<h1>Pyfa fits by Name</h1>
|
||||
</div>
|
||||
<div data-role="content">
|
||||
"""
|
||||
HTML += ' <ul data-role="listview" class="ui-listview-outer" data-inset="true" data-filter="true">\n'
|
||||
categoryList = list(sMkt.getShipRoot())
|
||||
categoryList.sort(key=lambda _ship: _ship.name)
|
||||
|
||||
count = 0
|
||||
|
||||
for group in categoryList:
|
||||
# init market group string to give ships something to attach to
|
||||
HTMLgroup = ''
|
||||
|
||||
ships = list(sMkt.getShipList(group.ID))
|
||||
ships.sort(key=lambda _ship: _ship.name)
|
||||
|
||||
# Keep track of how many ships per group
|
||||
groupFits = 0
|
||||
for ship in ships:
|
||||
fits = sFit.getFitsWithShip(ship.ID)
|
||||
|
||||
if len(fits) > 0:
|
||||
groupFits += len(fits)
|
||||
|
||||
for fit in fits:
|
||||
if self.stopRunning:
|
||||
return
|
||||
try:
|
||||
eftFit = Port.exportEft(getFit(fit[0]), options={
|
||||
PortEftOptions.IMPLANTS: True,
|
||||
PortEftOptions.MUTATIONS: True,
|
||||
PortEftOptions.LOADED_CHARGES: True,
|
||||
PortEftOptions.BOOSTERS: True,
|
||||
PortEftOptions.CARGO: True})
|
||||
|
||||
HTMLfit = (
|
||||
' <li data-role="collapsible" data-iconpos="right" data-shadow="false" '
|
||||
'data-corners="false">\n'
|
||||
' <h2>' + ship.name + " - " + fit[1] + '</h2>\n'
|
||||
' <ul data-role="listview" data-shadow="false" data-inset="true" '
|
||||
'data-corners="false">\n'
|
||||
)
|
||||
|
||||
HTMLfit += ' <li><pre>' + eftFit + '\n </pre></li>\n'
|
||||
|
||||
HTMLfit += ' </ul>\n </li>\n'
|
||||
HTML += HTMLfit
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
pyfalog.warning("Failed to export line")
|
||||
continue
|
||||
finally:
|
||||
if self.progress:
|
||||
self.progress.current = count
|
||||
count += 1
|
||||
|
||||
HTML += """
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
@@ -291,7 +357,7 @@ class exportHtmlThread(threading.Thread):
|
||||
pyfalog.error("Failed to export line")
|
||||
continue
|
||||
finally:
|
||||
if self.callback:
|
||||
wx.CallAfter(self.callback, count)
|
||||
if self.progress:
|
||||
self.progress.current = count
|
||||
count += 1
|
||||
return HTML
|
||||
|
||||
32
gui/utils/gdi.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import gc
|
||||
from ctypes import *
|
||||
from collections import defaultdict
|
||||
import os
|
||||
def gdiReport(desc=''):
|
||||
PH = windll.kernel32.OpenProcess(0x400, 0, os.getpid())
|
||||
numGdi = windll.user32.GetGuiResources(PH, 0)
|
||||
windll.kernel32.CloseHandle(PH)
|
||||
print (f'{desc}, {numGdi}')
|
||||
|
||||
|
||||
last = None
|
||||
def output_memory():
|
||||
global last
|
||||
d = defaultdict(int)
|
||||
for o in gc.get_objects():
|
||||
name = type(o).__name__
|
||||
if name == 'Bitmap':
|
||||
del o
|
||||
d[name] += 1
|
||||
|
||||
items = d.items()
|
||||
items = sorted(items,key=lambda x:x[1])
|
||||
print('------')
|
||||
for key, value in items:
|
||||
if last is not None:
|
||||
if value -last[key] !=0:
|
||||
print(f'{key} {value - last[key]}, {value}')
|
||||
else:
|
||||
print( key, value)
|
||||
|
||||
last = d
|
||||
@@ -58,6 +58,9 @@ class InputValidator(metaclass=ABCMeta):
|
||||
class FloatBox(wx.TextCtrl):
|
||||
|
||||
def __init__(self, parent, value, id=wx.ID_ANY, style=0, validator=None, **kwargs):
|
||||
# Workaround for #2591
|
||||
if 'wxMac' in wx.PlatformInfo and 'size' not in kwargs:
|
||||
kwargs['size'] = wx.Size(97, 26)
|
||||
super().__init__(parent=parent, id=id, style=style, **kwargs)
|
||||
self.Bind(wx.EVT_TEXT, self.OnText)
|
||||
self._storedValue = ''
|
||||
@@ -93,7 +96,7 @@ class FloatBox(wx.TextCtrl):
|
||||
if currentValue == self._storedValue:
|
||||
event.Skip()
|
||||
return
|
||||
if currentValue == '' or re.match('^\d*\.?\d*$', currentValue):
|
||||
if currentValue == '' or re.match(r'^\d*\.?\d*$', currentValue):
|
||||
self._storedValue = currentValue
|
||||
self.updateColor()
|
||||
event.Skip()
|
||||
@@ -107,6 +110,9 @@ class FloatBox(wx.TextCtrl):
|
||||
class FloatRangeBox(wx.TextCtrl):
|
||||
|
||||
def __init__(self, parent, value, id=wx.ID_ANY, style=0, **kwargs):
|
||||
# Workaround for #2591
|
||||
if 'wxMac' in wx.PlatformInfo and 'size' not in kwargs:
|
||||
kwargs['size'] = wx.Size(97, 26)
|
||||
super().__init__(parent=parent, id=id, style=style, **kwargs)
|
||||
self.Bind(wx.EVT_TEXT, self.OnText)
|
||||
self._storedValue = ''
|
||||
@@ -125,7 +131,7 @@ class FloatRangeBox(wx.TextCtrl):
|
||||
if currentValue == self._storedValue:
|
||||
event.Skip()
|
||||
return
|
||||
if currentValue == '' or re.match('^\d*\.?\d*-?\d*\.?\d*$', currentValue):
|
||||
if currentValue == '' or re.match(r'^\d*\.?\d*-?\d*\.?\d*$', currentValue):
|
||||
self._storedValue = currentValue
|
||||
event.Skip()
|
||||
else:
|
||||
|
||||
19
gui/utils/progressHelper.py
Normal file
@@ -0,0 +1,19 @@
|
||||
class ProgressHelper:
|
||||
|
||||
def __init__(self, message, maximum=None, callback=None):
|
||||
self.message = message
|
||||
self.current = 0
|
||||
self.maximum = maximum
|
||||
self.workerWorking = True
|
||||
self.dlgWorking = True
|
||||
self.error = None
|
||||
self.callback = callback
|
||||
self.cbArgs = []
|
||||
|
||||
@property
|
||||
def working(self):
|
||||
return self.workerWorking and self.dlgWorking and not self.error
|
||||
|
||||
@property
|
||||
def userCancelled(self):
|
||||
return not self.dlgWorking
|
||||
BIN
imgs/gui/hp_big.png
Normal file
|
After Width: | Height: | Size: 779 B |
BIN
imgs/gui/race_deathless_small.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
imgs/icons/10155@1x.png
Normal file
|
After Width: | Height: | Size: 828 B |
BIN
imgs/icons/10155@2x.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
imgs/icons/10848@1x.png
Normal file
|
After Width: | Height: | Size: 673 B |
BIN
imgs/icons/10848@2x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
imgs/icons/10877@1x.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
imgs/icons/10877@2x.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
imgs/icons/1156@1x.png
Normal file
|
After Width: | Height: | Size: 762 B |
BIN
imgs/icons/1156@2x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 585 B After Width: | Height: | Size: 645 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 604 B After Width: | Height: | Size: 836 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 645 B After Width: | Height: | Size: 878 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 634 B After Width: | Height: | Size: 805 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.8 KiB |