Compare commits

...

237 Commits

Author SHA1 Message Date
DarkPhoenix
a317dab9a8 Bump version 2020-08-15 03:56:54 +03:00
DarkPhoenix
db6d8b93e9 Re-enable meta storm menu 2020-08-15 03:39:48 +03:00
DarkPhoenix
7d585c1a62 Merge branch 'master' into singularity
# Conflicts:
#	staticdata/fsd_lite/evetypes.json
#	staticdata/phobos/metadata.json
2020-08-15 03:34:45 +03:00
DarkPhoenix
c45e84470e Add storm effects 2020-08-15 03:27:47 +03:00
DarkPhoenix
ab3b40e136 Rerun effectUsedBy 2020-08-15 02:08:09 +03:00
DarkPhoenix
b566a8bfa6 Update static data to 1786419 2020-08-15 02:06:19 +03:00
DarkPhoenix
ccb395d592 Add standard outpost rig effect 2020-08-13 13:17:50 +03:00
DarkPhoenix
3d039724c9 Relax ship name format restrictions even further 2020-08-13 01:18:03 +03:00
DarkPhoenix
d0e7e7eed5 Merge branch 'master' of github.com:pyfa-org/Pyfa into master 2020-08-13 01:16:08 +03:00
DarkPhoenix
a387bc8d09 Fix import of ship/structure names containing single quotes 2020-08-13 01:15:41 +03:00
Anton Vorobyov
94344bd432 Merge pull request #2254 from Neugeniko/Issue/2253
Updated 'Retaliating Caldari Entities' target profile
2020-08-12 12:14:48 +03:00
DarkPhoenix
a976fb33f0 Fix defender velocity hardwiring 2020-08-12 00:11:52 +03:00
DarkPhoenix
f850fdf0d5 Bump version 2020-08-11 19:59:46 +03:00
DarkPhoenix
0fcbedba45 Disable metaliminal storm context menu 2020-08-11 19:58:44 +03:00
DarkPhoenix
5707bddacd Update staticdata to 1782806 2020-08-11 19:56:33 +03:00
Neugeniko
8ed9257dfa Updated 'Retaliating Caldari Entities' target profile
2020/08/12 Eve Online Patch increased Em Resists for some npc in the group of  'Retaliating Caldari Entities'
The groups target profile has been recalculated and edited.
2020-08-12 02:56:13 +10:00
DarkPhoenix
056ae590cf Merge branch 'master' into singularity 2020-08-10 17:39:37 +03:00
DarkPhoenix
7733fd38c2 Do not set read-only property 2020-08-10 17:39:08 +03:00
DarkPhoenix
fef78c971f Add effects used by metaliminal storms
Most of them are stubs, so just 2 so far
2020-08-05 22:57:45 +03:00
DarkPhoenix
e52ceacdb9 Add liminal storms to projection menu 2020-08-05 22:46:43 +03:00
DarkPhoenix
85b2d7af8d Add item conversions
Based on my own assumptions about them
2020-08-05 22:21:27 +03:00
DarkPhoenix
3e658a31bb Process renamed items 2020-08-05 21:54:56 +03:00
DarkPhoenix
0d2a4d4d44 Update metadata to 1780302 2020-08-05 21:40:59 +03:00
Anton Vorobyov
bc2cdcdea7 Merge pull request #2249 from m-sasha/undup-tab-selection
_TabsContainer.CheckTabSelected no longer posts PageChanged event twice
2020-07-31 11:59:38 +03:00
Anton Vorobyov
30ad554cc0 Merge pull request #2248 from m-sasha/no-half-second-sleep
Do not sleep for half a second in OpenFitsThread.run()
2020-07-31 11:59:06 +03:00
Anton Vorobyov
4a85fd5d1b Merge pull request #2247 from m-sasha/fast-tabs-resize
Ignore _TabRenderer.SetSize when new size is the same as old size
2020-07-31 11:58:35 +03:00
Alexander Maryanovsky
4d8dbe74bd _TabsContainer.CheckTabSelected no longer posts PageChanged event twice 2020-07-30 16:03:00 +03:00
Alexander Maryanovsky
016f2b44ff Do not sleep for half a second in OpenFitsThread.run() 2020-07-30 11:35:30 +03:00
Alexander Maryanovsky
676794baed Ignore _TabRenderer.SetSize when new size is the same as old size. 2020-07-30 11:31:00 +03:00
Anton Vorobyov
ca488089fd Merge pull request #2231 from fonsuiplaysvideogames/master
Updates to Abyss room-wide and localized weather effects
2020-07-30 08:04:42 +03:00
Anton Vorobyov
96f9b9a719 Merge branch 'master' into master 2020-07-30 08:04:09 +03:00
fonsuiplaysvideogames
508572e08b Update fit.py 2020-07-29 21:33:15 -04:00
DarkPhoenix
f91e0b2e23 Do not crash on ship w/o labels and w/o slots 2020-07-29 09:02:46 +03:00
DarkPhoenix
93ae9e0891 Bump version 2020-07-28 17:39:20 +03:00
DarkPhoenix
2ed5dbc3c7 Avoid using newer matplotlib 2020-07-28 17:12:15 +03:00
DarkPhoenix
bca8ba3114 Bump version 2020-07-28 15:58:15 +03:00
DarkPhoenix
174ac97682 Implement new CS effects 2020-07-28 15:57:55 +03:00
DarkPhoenix
2d9e873d42 Update staticdata to 1777281 2020-07-28 15:21:08 +03:00
DarkPhoenix
33377357f6 Change dampener definition to include named variations 2020-07-26 10:36:55 +03:00
DarkPhoenix
3ee0ee7e40 Fix drone sorting in projected view 2020-07-19 23:31:00 +03:00
fonsuiplaysvideogames
2f02747b29 Update fit.py 2020-07-16 16:24:08 -04:00
fonsuiplaysvideogames
78b176a135 Update fit.py 2020-07-16 16:20:24 -04:00
DarkPhoenix
52063beea9 BCS affect defender missiles now 2020-07-16 00:40:04 +03:00
DarkPhoenix
03a55d94e9 Bump version 2020-07-14 20:29:47 +03:00
DarkPhoenix
db256f57d1 Adjust ANP jargon entries 2020-07-14 20:29:27 +03:00
DarkPhoenix
8de6d78be0 Update effect docstrings 2020-07-14 20:25:45 +03:00
DarkPhoenix
902a00d37d Add conversions 2020-07-14 20:24:42 +03:00
DarkPhoenix
523cb1467e Merge branch 'issue_2205' 2020-07-14 20:21:23 +03:00
DarkPhoenix
f75de70d79 Update staticdata to 1769230 2020-07-14 20:00:23 +03:00
DarkPhoenix
4b635f4d21 Remove www to make dropdown shorter on gtk 2020-07-14 17:31:19 +03:00
DarkPhoenix
63632e09b3 Group market sources up to avoid cross-server price data confusion 2020-07-14 17:25:30 +03:00
Ryan Holmes
df78eb5781 Merge pull request #2214 from copyliu/cevemarket
add market source ceve-market
2020-06-29 20:07:44 -04:00
Copy Liu
c1a5828d6b add market source ceve-market 2020-06-29 20:58:46 +08:00
blitzmann
5377210b89 Merge branch 'master' of https://github.com/pyfa-org/Pyfa 2020-06-26 23:30:51 -04:00
blitzmann
1936255f2c Merge branch 'issue2209' 2020-06-26 23:30:40 -04:00
blitzmann
b3e5763cfc Fix for #2209 - Flip the None check to happen first instead of second, otherwise if Notes is None then the first check will fail due to trying to len(None) 2020-06-26 23:30:15 -04:00
DarkPhoenix
7442f315c9 Do not activate scanners by default 2020-06-26 21:30:43 +03:00
blitzmann
433c9555bf Removed the util function - this would actually break older migrations if we added things that didn't exist in future iterations. Each migration should be self-contained 2020-06-24 15:18:43 -04:00
blitzmann
d1345fc71e Fixes #2205 - runs conversions on modules.baseItemID since the creation of baseItemID. Also, centralized the function to convert modules so that in case we ever add other things that need to be converted, they are all in one spot. 2020-06-24 15:09:51 -04:00
DarkPhoenix
31cae0e54b Bump version 2020-06-18 17:28:02 +03:00
DarkPhoenix
b2cc3ae600 Update static data to 1751741 2020-06-18 13:52:46 +03:00
DarkPhoenix
62d1d6a06d Avoid crash when nothing is selected 2020-06-18 06:04:23 +03:00
DarkPhoenix
83fa567321 Pass primitives around instead of SQL Alchemy objects 2020-06-18 05:33:12 +03:00
DarkPhoenix
9b315b5870 Fix a few mistypes 2020-06-17 21:53:24 +03:00
DarkPhoenix
3094fd32fc Add destructible beacons to projected effects 2020-06-17 17:07:34 +03:00
DarkPhoenix
c3f1824a84 Rename charge export button 2020-06-16 21:06:42 +03:00
DarkPhoenix
b558ae3810 Add an option not to export charges to ESI 2020-06-15 23:38:07 +03:00
DarkPhoenix
c38f05902a Add alternate redo shortcut 2020-06-15 17:20:20 +03:00
DarkPhoenix
181e1e1e30 Make scanners active and expose data to misc column 2020-06-15 16:37:52 +03:00
DarkPhoenix
3fc26254ae bump version 2020-06-15 16:14:53 +03:00
DarkPhoenix
ca8822c455 Fix upwell ships' race 2020-06-15 16:14:20 +03:00
DarkPhoenix
4c55d32086 Update icons 2020-06-15 15:54:28 +03:00
DarkPhoenix
c13817708e Add vorton damage booster effect 2020-06-15 15:20:59 +03:00
DarkPhoenix
39cab4be72 Remove DB TDs from seen items 2020-06-15 15:00:36 +03:00
DarkPhoenix
f80473d073 Update static data to 1748920 2020-06-15 14:50:58 +03:00
DarkPhoenix
0d9be1b174 Merge branch 'singularity' 2020-06-15 14:42:13 +03:00
DarkPhoenix
83b0b3ff0e Fix jargon definitions for missile damage types 2020-06-13 12:33:06 +03:00
DarkPhoenix
c4b490907d Add alternate DNA format import 2020-06-12 02:20:32 +03:00
DarkPhoenix
f89fe4d5d7 Fix merge issue 2020-06-09 19:11:11 +03:00
DarkPhoenix
81dced03bf Disable ammo graph 2020-06-09 19:07:23 +03:00
DarkPhoenix
27d3bea8d8 Merge branch 'ammo_graph' into singularity
# Conflicts:
#	gui/targetProfileEditor.py
2020-06-09 19:04:20 +03:00
DarkPhoenix
7eaeea325d Bump version 2020-06-09 10:54:21 +03:00
DarkPhoenix
9695526352 Update staticdata to 1744483 2020-06-09 10:51:28 +03:00
DarkPhoenix
4f35bc38db Fix structure module drag'n'drop 2020-06-09 01:56:31 +03:00
DarkPhoenix
d9c7c1a0fd Fix burst projectors' misc column 2020-06-06 19:22:55 +03:00
DarkPhoenix
a94e66e5ed Invuln - alias for old invulns 2020-06-06 02:25:08 +03:00
DarkPhoenix
e9c41c6439 Add renders of 2/3 of new ships 2020-06-06 01:42:57 +03:00
DarkPhoenix
b24ceb95f5 Add missing icons 2020-06-05 22:22:03 +03:00
DarkPhoenix
6ba6e0f69b Implement skill and booster effects, and bump version 2020-06-06 01:09:55 +03:00
DarkPhoenix
9aca49e539 Add damage mod effect and gun application readout 2020-06-06 00:25:19 +03:00
DarkPhoenix
01f1cdb175 Implement gun effect and its support in graphs 2020-06-06 00:11:36 +03:00
DarkPhoenix
c1356906bb Add ship effects 2020-06-05 23:36:39 +03:00
DarkPhoenix
8229537e5f Run effect used by script 2020-06-05 22:50:34 +03:00
DarkPhoenix
6cfa2161ec Add conversions for amplifier tiericide 2020-06-05 22:47:13 +03:00
DarkPhoenix
fa3cf46786 Update static data to 1742848 2020-06-05 22:28:25 +03:00
DarkPhoenix
ac40dfd018 Update version again 2020-05-26 16:07:01 +03:00
DarkPhoenix
4c32d559bb Bump version 2020-05-26 16:04:51 +03:00
DarkPhoenix
02ee12586f Adjust jargon definitions to new realities 2020-05-26 16:03:48 +03:00
DarkPhoenix
496f15a83e Add conversions/migrations 2020-05-26 15:50:52 +03:00
DarkPhoenix
acf6af8f95 Update effect definitions and headers to all the renamed items 2020-05-26 15:30:50 +03:00
DarkPhoenix
225b00b9ff Update staticdata to 1733182 and update DB generation script 2020-05-26 15:22:08 +03:00
DarkPhoenix
7f2b7415ea Disable evemarketdata price source 2020-05-14 15:10:04 +03:00
DarkPhoenix
d1d9ae4dac Add fuzzwork as price data source 2020-05-14 15:09:38 +03:00
DarkPhoenix
ac7ccfb6a3 Change how double-clicking ammo behaves 2020-05-04 19:18:14 +03:00
DarkPhoenix
05c2c41762 Destroy effective label when switching from firepower to mining view, and add it back to correct position when needed 2020-05-04 13:46:52 +03:00
DarkPhoenix
10f5133282 Revert "Disable system theme for listctrls on windows"
This reverts commit 42658a8167.
2020-04-27 13:55:30 +03:00
DarkPhoenix
42658a8167 Disable system theme for listctrls on windows 2020-04-26 23:27:28 +03:00
DarkPhoenix
6dbf38dbb4 Revert "Switch to wxpython 4.1.0". Refer to #2179 for more info on
reasons

This reverts commit 15fe8f6261.
2020-04-25 17:37:29 +03:00
DarkPhoenix
64306e2366 Align attribute icons the same way as input boxes in target profile
editor
2020-04-25 17:29:29 +03:00
DarkPhoenix
f259df85cd Fix editors 2020-04-25 17:27:29 +03:00
DarkPhoenix
1090589cb2 Fix preferences for wx 4.1.0 2020-04-25 17:19:05 +03:00
DarkPhoenix
29e09fcdda Fix abyssal item slider on windows 2020-04-25 12:36:45 +03:00
DarkPhoenix
36fd4aeaff Redo fix for 2176 2020-04-25 15:14:27 +03:00
DarkPhoenix
c46a59e3b1 Fix gauge bars 2020-04-25 09:57:49 +03:00
DarkPhoenix
8a3a21972a Fix error dialog layout 2020-04-25 09:40:50 +03:00
DarkPhoenix
6839669e04 Remove a few flags the new wx complains about 2020-04-25 09:36:40 +03:00
DarkPhoenix
5645e20505 Do not float auxiliary windows on parent if there is no parent 2020-04-25 09:29:14 +03:00
DarkPhoenix
15fe8f6261 Switch to wxpython 4.1.0 2020-04-25 09:26:47 +03:00
DarkPhoenix
855fafa94d Bump version 2020-04-24 15:35:52 +03:00
DarkPhoenix
4e10335ae7 Revert "Try resetting locale on wx 4.0.6 as an attempt to work around #2174"
This reverts commit ea07bbf4f9.
2020-04-24 15:10:33 +03:00
DarkPhoenix
21ea9ce579 "Move" attributes at DB generation time 2020-04-24 15:10:00 +03:00
DarkPhoenix
ea07bbf4f9 Try resetting locale on wx 4.0.6 as an attempt to work around #2174 2020-04-24 11:37:27 +03:00
DarkPhoenix
8eed6fbe21 Ignore mutations with 0 base value 2020-04-23 19:47:57 +03:00
DarkPhoenix
0859f2fbe9 Hide limited synth boosters 2020-04-22 02:27:20 +03:00
DarkPhoenix
71ba33edeb Fix tail clear method 2020-04-21 19:00:44 +03:00
DarkPhoenix
ce80d92b35 Limit amount of fits returned by search by 100 2020-04-21 11:16:06 +03:00
DarkPhoenix
f17cf9b736 Fix undo for fill with module (which was caused by module add command not being undone properly in case of failure) 2020-04-20 16:36:16 +03:00
DarkPhoenix
98579c774b Change shortcuts for undo/redo from built-in wx standards to ctrl-z and ctrl-y 2020-04-20 14:42:32 +03:00
DarkPhoenix
509fa279e7 Initialize session container with main thread session 2020-04-17 19:15:47 +03:00
DarkPhoenix
091ee87761 Update char list when new character gets added 2020-04-17 13:31:15 +03:00
DarkPhoenix
c0c20cc92e Remove skill refresh button 2020-04-17 13:27:51 +03:00
DarkPhoenix
1341f7bca1 Make some jargon replacements a bit looser 2020-04-17 11:27:28 +03:00
DarkPhoenix
fe93db1d4b Round booster side-effect context menu 2020-04-17 00:25:33 +03:00
DarkPhoenix
5db97ea773 Bump version 2020-04-16 22:46:45 +03:00
DarkPhoenix
1758e4f320 Rework fix for projection, so that it does not recalculate projected fits too early 2020-04-16 22:25:10 +03:00
DarkPhoenix
1a897c0419 Pass search results as set of item IDs 2020-04-16 15:13:32 +03:00
DarkPhoenix
32db3e3179 Create scoped gamedata sessions 2020-04-16 15:01:00 +03:00
DarkPhoenix
d830a8957a Bump version 2020-04-15 16:54:24 +03:00
DarkPhoenix
652ea48223 Accept 0 free slots for guns which have already been fitted 2020-04-15 16:53:03 +03:00
DarkPhoenix
8c25b2b8f5 Make sure it's impossible to add extra cap boosters via dragging too 2020-04-15 15:40:54 +03:00
DarkPhoenix
db4c56be8e Bump version 2020-04-15 15:27:19 +03:00
DarkPhoenix
f3bcffe2f9 Implement fax cap booster limit 2020-04-15 15:23:49 +03:00
DarkPhoenix
bc5786d099 Update static data to 1706308 2020-04-15 14:50:55 +03:00
DarkPhoenix
5959fe5daf Ignore case when searching for implant sets to allow some inconsistencies on CCP side 2020-04-14 15:28:40 +03:00
DarkPhoenix
649d338bb1 Split implants and boosters in EFT and multibuy exports 2020-04-14 11:36:08 +03:00
DarkPhoenix
dcb058a718 Update existing character if character with the same name exists 2020-04-13 12:52:02 +03:00
DarkPhoenix
1772bb5e7f Merge branch 'master' into singularity 2020-04-13 12:45:25 +03:00
Anton Vorobyov
30bd0adb06 Merge pull request #2159 from soro/auto_create_char
Regarding #2045: automatically create a character for a new sso character
2020-04-12 22:49:01 +03:00
DarkPhoenix
44dfcf771c Ensure projected fits get recalculated, since they get checkStates'd anyway 2020-04-11 02:26:17 +03:00
DarkPhoenix
a1f8a7a930 Fix escaping in regex search 2020-04-11 01:33:53 +03:00
Soeren Roerden
b22887dfad automatically create a character for a new sso character and add button to retrieve all chars directly linked to existing sso characters 2020-04-10 21:12:38 +02:00
DarkPhoenix
28137fa3f4 Remove excessive space 2020-04-10 13:11:04 +03:00
DarkPhoenix
9cbdc6055d Add search timer to attribute overrides to prevent hangs 2020-04-10 11:52:57 +03:00
DarkPhoenix
fc93c61fcf Add more entries 2020-04-10 11:31:17 +03:00
DarkPhoenix
3fa2e7ebd1 More additions to jargon 2020-04-10 06:07:33 +03:00
DarkPhoenix
818628da0c Finalize new jargon dictionary 2020-04-10 05:53:24 +03:00
DarkPhoenix
adf90a8263 Split basic and regex search functions 2020-04-10 00:57:41 +03:00
DarkPhoenix
362923ac64 Overhaul jargon dictionary (some entries are still missing) 2020-04-09 23:29:51 +03:00
DarkPhoenix
7d73838ce1 Change the way user definitions are used 2020-04-08 14:06:49 +03:00
DarkPhoenix
b3278ca9ec Tokenize regexp requests taking into consideration regexp context 2020-04-08 13:24:48 +03:00
DarkPhoenix
5707914ad5 Make use of regex for search 2020-04-08 03:17:25 +03:00
DarkPhoenix
9b697b24d8 Implement regexp function for gamedata 2020-04-08 02:34:28 +03:00
DarkPhoenix
098b088da6 Bump version 2020-04-07 21:39:24 +03:00
DarkPhoenix
14d62f31e1 Add conversions for names 2020-04-07 21:37:40 +03:00
DarkPhoenix
e7b1f55d08 Update static data to 1702461 2020-04-07 20:59:07 +03:00
DarkPhoenix
8f501896a1 Give separate axis selector and axis label names to make it clearer what the graphs do 2020-04-07 20:37:28 +03:00
DarkPhoenix
befcb9b874 Un-hide and un-fuck bumping graphs 2020-04-07 20:14:06 +03:00
DarkPhoenix
c70afa9a4c Hide experimental bumping graphs 2020-04-07 18:25:40 +03:00
DarkPhoenix
029c7dd4c2 Recalculate fit when overrides are toggled 2020-04-07 18:05:43 +03:00
DarkPhoenix
1f83dba1ac Add missing hidden imports to platform-specific specifications 2020-04-07 18:00:17 +03:00
DarkPhoenix
6eae3405fd Add hidden import for newer version of setuptools 2020-04-07 17:39:24 +03:00
DarkPhoenix
c0e1d9e4de Attempt to use newer version of setuptools 2020-04-07 17:09:59 +03:00
DarkPhoenix
7c0bd7aa88 Leave only setuptools override 2020-04-07 16:41:28 +03:00
DarkPhoenix
394583584c Remove some of hardcoded libraries for MacOS X build 2020-04-07 15:08:30 +03:00
DarkPhoenix
4112e2aa6b Debugging Mac OS X build, step 1 2020-04-07 14:17:55 +03:00
DarkPhoenix
aa19e0da72 Add bunch of experimental graphs related to bumping 2020-04-07 13:46:28 +03:00
DarkPhoenix
bba9be1598 Add momentum graphs 2020-04-07 10:50:12 +03:00
DarkPhoenix
17998916b4 Bump version 2020-04-06 22:24:06 +03:00
DarkPhoenix
543089bcd9 Add context menu support for predefined implant sets 2020-04-06 22:20:28 +03:00
DarkPhoenix
7f35c78a65 Add data about implant sets to eve.db 2020-04-06 21:01:20 +03:00
DarkPhoenix
b25798dd83 Update effects 2020-04-06 20:14:19 +03:00
DarkPhoenix
744b9ff78a Updated staticdata to 1701385 2020-04-06 19:19:06 +03:00
DarkPhoenix
8dc1457ebb Change the way zip file is generated 2020-04-06 15:09:06 +03:00
DarkPhoenix
982ad54fab Merge branch 'Metallicow-optimize-images' 2020-04-06 15:29:10 +03:00
DarkPhoenix
912192cd7d Merge branch 'optimize-images' of https://github.com/Metallicow/Pyfa into Metallicow-optimize-images 2020-04-06 15:26:32 +03:00
DarkPhoenix
74cd6d48da Show propmpt to creade damage patterns when there're none 2020-04-06 11:14:21 +03:00
DarkPhoenix
3467a7fe3f Update static data to 1684558 2020-03-11 17:01:34 +03:00
DarkPhoenix
eb269a05ed Bump version 2020-03-10 19:39:22 +03:00
DarkPhoenix
c63cf4b3b0 Add cap booster conversions 2020-03-10 19:19:13 +03:00
DarkPhoenix
11ed94454d Update staticdata to 1683031 2020-03-10 19:07:42 +03:00
DarkPhoenix
8df07645da Show RR power for uncharged ancillary RRs 2020-03-06 00:50:18 +03:00
DarkPhoenix
5666fdd250 Bump version 2020-02-27 15:24:45 +03:00
DarkPhoenix
0b55ae40fc Update staticdata to 1675961 2020-02-27 15:09:54 +03:00
DarkPhoenix
3726e84697 Add dbuffcollections (just to have overview of changed fleet buffs when stuff changes) 2020-02-20 13:31:18 +03:00
DarkPhoenix
a0c4341102 Fix ECM range rigs affecting ECM bursts with penalty 2020-02-17 23:42:48 +03:00
Metallicow
cd6b1038e8 optimize all pngs 2020-02-13 14:58:27 -06:00
Metallicow
a93915cf04 trim trailing whitespace 2020-02-12 16:14:12 -06:00
DarkPhoenix
aec5a46452 Fix shebang line 2020-02-05 09:43:10 +03:00
DarkPhoenix
588f9a4490 Re-use market service item sorting for attribute overrides 2020-02-04 01:48:13 +03:00
DarkPhoenix
4ba948c6f9 Merge branch 'master' into ammo_graph
# Conflicts:
#	gui/builtinMarketBrowser/itemView.py
#	service/market.py
2020-02-04 01:43:52 +03:00
DarkPhoenix
eacc97160b Merge branch 'master' into ammo_graph 2020-01-21 15:25:39 +03:00
DarkPhoenix
78423f67dd Merge branch 'master' into ammo_graph 2019-12-07 03:34:26 +03:00
DarkPhoenix
61e32d0b1b Merge branch 'ammo_graph' of github.com:pyfa-org/Pyfa into ammo_graph 2019-12-06 01:09:16 +03:00
DarkPhoenix
bf04d26a7b Remove duplicate code 2019-12-06 01:08:11 +03:00
DarkPhoenix
2217aff5ab Add button to add custom drone groups 2019-11-26 14:53:47 +03:00
DarkPhoenix
400d0aaa22 Rework graph ammo picker to be dialog instead of frame 2019-11-25 16:54:25 +03:00
DarkPhoenix
bfc928934c Put entries into different sizers 2019-11-25 15:48:04 +03:00
DarkPhoenix
395c17270e Merge branch 'master' into ammo_graph 2019-11-25 13:56:16 +03:00
DarkPhoenix
1b40467775 Merge branch 'master' into ammo_graph 2019-11-15 20:17:02 +03:00
DarkPhoenix
27e3e53868 Merge branch 'master' into ammo_graph 2019-11-13 22:03:01 +03:00
DarkPhoenix
00b9884c68 Remove unnecessary color setting, as aux window already does it for us 2019-11-13 19:56:49 +03:00
DarkPhoenix
b05bd04801 Do not allow to open more than 1 ammo pickers 2019-11-13 18:45:43 +03:00
DarkPhoenix
7ba5585b83 Set min window size of ammo picker as well 2019-11-13 17:07:32 +03:00
DarkPhoenix
9386ba3fb9 Fix initial window sizing 2019-11-13 17:03:45 +03:00
DarkPhoenix
1321e70035 Add info on drones/fighters to ammo picker 2019-11-13 16:46:47 +03:00
DarkPhoenix
e2d943b0b0 Gray out items for inactive radiobuttons 2019-11-13 16:33:23 +03:00
DarkPhoenix
424b769ba9 Fix ammo section labels 2019-11-13 16:14:00 +03:00
DarkPhoenix
3671f10c9c Resize ammo picker window 2019-11-13 16:08:06 +03:00
DarkPhoenix
935ecd0ea7 Make structure scrollable 2019-11-13 15:55:02 +03:00
DarkPhoenix
92e15ece72 Add structure with checkboxes to ammo switcher window 2019-11-13 15:20:43 +03:00
DarkPhoenix
a5c1875a29 Make sure to cache ammo set to reuse it when applying ammo to multiple modules 2019-11-13 13:48:13 +03:00
DarkPhoenix
9146c0f2c6 Use ammo service to generate ammo switcher context menu 2019-11-13 13:31:40 +03:00
DarkPhoenix
a917207e07 Reimplement some of logic used in ammo picker in a service package 2019-11-12 19:39:52 +03:00
DarkPhoenix
c21b92945f Merge branch 'master' into ammo_graph 2019-11-12 16:58:34 +03:00
DarkPhoenix
049bd10797 Merge branch 'master' into ammo_graph 2019-11-07 19:02:12 +03:00
DarkPhoenix
b486276167 Merge branch 'master' into ammo_graph 2019-10-31 11:29:31 +03:00
DarkPhoenix
e4df215e47 Change order of market group sorting for ammo picker 2019-10-18 12:10:02 +03:00
DarkPhoenix
33cb332978 Plug market sort into module grouping 2019-10-17 18:21:03 +03:00
DarkPhoenix
7c8f4d51bb Move sorting from item view to market service 2019-10-17 17:39:44 +03:00
DarkPhoenix
66bf7b3dc6 Merge branch 'master' into ammo_graph 2019-10-17 17:19:40 +03:00
DarkPhoenix
8bb1d43d0c Show module groups / drone header / fighter header in dialog 2019-10-11 13:23:18 +03:00
DarkPhoenix
3d70ca941c Change ammo picker window to be auxiliary frame rather than modal dialog 2019-10-10 19:10:28 +03:00
DarkPhoenix
04a1e95730 Get list of fighters and drones as well 2019-10-10 16:03:56 +03:00
DarkPhoenix
9fddb64ef9 Get list of damage dealer mods with ammo data 2019-10-10 15:40:39 +03:00
DarkPhoenix
e883035120 Do not show menu item for non-dps graphs 2019-10-10 13:38:42 +03:00
DarkPhoenix
4d2da8e52e Merge branch 'master' into ammo_graph 2019-10-10 12:41:30 +03:00
DarkPhoenix
c1df335f08 Add context menu item which will handle fit duplication 2019-10-08 15:39:16 +03:00
528 changed files with 137361 additions and 81091 deletions

View File

@@ -65,7 +65,7 @@ build_script:
after_build:
- ps: "ls \"./\""
- ps: 7z a "pyfa-$env:PYFA_VERSION-win.zip" -r "$env:PYFA_DIST_DIR\pyfa\*.*"
- ps: 7z a "pyfa-$env:PYFA_VERSION-win.zip" -r "$env:PYFA_DIST_DIR\pyfa\*"
test_script:
# Ha... we're just building

View File

@@ -23,6 +23,7 @@ import functools
import itertools
import json
import os
import re
import sqlite3
import sys
@@ -32,7 +33,7 @@ DB_PATH = os.path.join(ROOT_DIR, 'eve.db')
JSON_DIR = os.path.join(ROOT_DIR, 'staticdata')
if ROOT_DIR not in sys.path:
sys.path.insert(0, ROOT_DIR)
GAMEDATA_SCHEMA_VERSION = 3
GAMEDATA_SCHEMA_VERSION = 4
def db_needs_update():
@@ -121,9 +122,13 @@ def update_db():
if (
# Apparently people really want Civilian modules available
(row['typeName'].startswith('Civilian') and "Shuttle" not in row['typeName']) or
row['typeName'] in ('Capsule', 'Dark Blood Tracking Disruptor')
row['typeName'] == 'Capsule' or
row['groupID'] == 4033 # destructible effect beacons
):
row['published'] = True
# Nearly useless and clutter search results too much
elif row['typeName'].startswith('Limited Synth '):
row['published'] = False
newData = []
for row in data:
@@ -135,11 +140,10 @@ def update_db():
row['typeID'] in (41549, 41548, 41551, 41550) or
# Abyssal weather (environment)
row['groupID'] in (
1882,
1975,
1971,
# the "container" for the abyssal environments
1983)
1882,
1975,
1971,
1983) # the "container" for the abyssal environments
):
newData.append(row)
@@ -167,16 +171,26 @@ def update_db():
data = _readData('fsd_binary', 'typedogma', keyIdName='typeID')
eveTypeIds = set(r['typeID'] for r in eveTypesData)
newData = []
for row in eveTypesData:
for attrId, attrName in {4: 'mass', 38: 'capacity', 161: 'volume', 162: 'radius'}.items():
if attrName in row:
newData.append({'typeID': row['typeID'], 'attributeID': attrId, 'value': row[attrName]})
seenKeys = set()
def checkKey(key):
if key in seenKeys:
return False
seenKeys.add(key)
return True
for typeData in data:
if typeData['typeID'] not in eveTypeIds:
continue
for row in typeData.get('dogmaAttributes', ()):
row['typeID'] = typeData['typeID']
newData.append(row)
if checkKey((row['typeID'], row['attributeID'])):
newData.append(row)
for row in eveTypesData:
for attrId, attrName in {4: 'mass', 38: 'capacity', 161: 'volume', 162: 'radius'}.items():
if attrName in row and checkKey((row['typeID'], attrId)):
newData.append({'typeID': row['typeID'], 'attributeID': attrId, 'value': row[attrName]})
_addRows(newData, eos.gamedata.Attribute)
return newData
@@ -314,8 +328,8 @@ def update_db():
def composeReqSkills(raw):
reqSkills = {}
for skillTypeID, skillLevels in raw.items():
reqSkills[int(skillTypeID)] = skillLevels[0]
for skillTypeID, skillLevel in raw.items():
reqSkills[int(skillTypeID)] = skillLevel
return reqSkills
eveTypeIds = set(r['typeID'] for r in eveTypesData)
@@ -458,6 +472,38 @@ def update_db():
if itemReplacements is not None:
item.replacements = ','.join('{}'.format(tid) for tid in sorted(itemReplacements))
def processImplantSets(eveTypesData):
print('composing implant sets')
# Includes only implants which can be considered part of sets, not all implants
implant_groups = (300, 1730)
specials = {'Genolution': ('Genolution Core Augmentation', r'CA-\d+')}
implantSets = {}
for row in eveTypesData:
if not row.get('published'):
continue
if row.get('groupID') not in implant_groups:
continue
typeName = row.get('typeName', '')
# 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)
if m:
implantSets.setdefault((m.group('grade'), m.group('set')), set()).add(row['typeID'])
# Special set matching
for setHandle, (setName, implantPattern) in specials.items():
pattern = '(?P<set>{}) (?P<implant>{})'.format(setName, implantPattern)
m = re.match(pattern, typeName)
if m:
implantSets.setdefault((None, setHandle), set()).add(row['typeID'])
break
data = []
for (gradeName, setName), implants in implantSets.items():
if len(implants) < 2:
continue
implants = ','.join('{}'.format(tid) for tid in sorted(implants))
row = {'setName': setName, 'gradeName': gradeName, 'implants': implants}
data.append(row)
_addRows(data, eos.gamedata.ImplantSet)
eveTypesData = processEveTypes()
eveGroupsData = processEveGroups()
processEveCategories()
@@ -476,6 +522,7 @@ def update_db():
eos.db.gamedata_session.flush()
processReqSkills(eveTypesData)
processReplacements(eveTypesData, eveGroupsData, dogmaTypeAttributesData, dogmaTypeEffectsData)
processImplantSets(eveTypesData)
# Add schema version to prevent further updates
metadata_schema_version = eos.gamedata.MetaData()

View File

@@ -30,7 +30,8 @@ added_files = [
import_these = [
'numpy.core._dtype_ctypes', # https://github.com/pyinstaller/pyinstaller/issues/3982
'sqlalchemy.ext.baked' # windows build doesn't launch without if when using sqlalchemy 1.3.x
'sqlalchemy.ext.baked', # windows build doesn't launch without if when using sqlalchemy 1.3.x
'pkg_resources.py2_warn' # issue 2156
]
icon = os.path.join(os.getcwd(), "dist_assets", "mac", "pyfa.icns")

View File

@@ -29,7 +29,8 @@ added_files = [
import_these = [
'numpy.core._dtype_ctypes', # https://github.com/pyinstaller/pyinstaller/issues/3982
'sqlalchemy.ext.baked' # windows build doesn't launch without if when using sqlalchemy 1.3.x
'sqlalchemy.ext.baked', # windows build doesn't launch without if when using sqlalchemy 1.3.x
'pkg_resources.py2_warn' # issue 2156
]
# Walk directories that do dynamic importing

View File

@@ -17,24 +17,37 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
import re
import threading
from sqlalchemy import MetaData, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import MetaData, create_engine, event
from sqlalchemy.orm import sessionmaker, scoped_session
from . import migration
from eos import config
from logbook import Logger
pyfalog = Logger(__name__)
pyfalog.info("Initializing database")
pyfalog.info("Gamedata connection: {0}", config.gamedata_connectionstring)
pyfalog.info("Saveddata connection: {0}", config.saveddata_connectionstring)
class ReadOnlyException(Exception):
pass
def re_fn(expr, item):
try:
reg = re.compile(expr, re.IGNORECASE)
except (SystemExit, KeyboardInterrupt):
raise
except:
return False
return reg.search(item) is not None
pyfalog.debug('Initializing gamedata')
gamedata_connectionstring = config.gamedata_connectionstring
if callable(gamedata_connectionstring):
@@ -42,9 +55,26 @@ if callable(gamedata_connectionstring):
else:
gamedata_engine = create_engine(gamedata_connectionstring, echo=config.debug)
@event.listens_for(gamedata_engine, 'connect')
def create_functions(dbapi_connection, connection_record):
dbapi_connection.create_function('regexp', 2, re_fn)
gamedata_meta = MetaData()
gamedata_meta.bind = gamedata_engine
gamedata_session = sessionmaker(bind=gamedata_engine, autoflush=False, expire_on_commit=False)()
GamedataSession = scoped_session(sessionmaker(bind=gamedata_engine, autoflush=False, expire_on_commit=False))
gamedata_session = GamedataSession()
gamedata_sessions = {threading.get_ident(): gamedata_session}
def get_gamedata_session():
thread_id = threading.get_ident()
if thread_id not in gamedata_sessions:
gamedata_sessions[thread_id] = GamedataSession()
return gamedata_sessions[thread_id]
pyfalog.debug('Getting gamedata version')
# This should be moved elsewhere, maybe as an actual query. Current, without try-except, it breaks when making a new
@@ -84,7 +114,7 @@ sd_lock = threading.RLock()
pyfalog.debug('Importing gamedata DB scheme')
# Import all the definitions for all our database stuff
# noinspection PyPep8
from eos.db.gamedata import alphaClones, attribute, category, effect, group, item, marketGroup, metaData, metaGroup, queries, traits, unit, dynamicAttributes
from eos.db.gamedata import alphaClones, attribute, category, effect, group, item, marketGroup, metaData, metaGroup, queries, traits, unit, dynamicAttributes, implantSet
pyfalog.debug('Importing saveddata DB scheme')
# noinspection PyPep8
from eos.db.saveddata import booster, cargo, character, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, \

View File

@@ -1,2 +1,2 @@
__all__ = ["attribute", "category", "effect", "group", "metaData", "dynamicAttributes",
"item", "marketGroup", "metaGroup", "unit", "alphaClones"]
"item", "marketGroup", "metaGroup", "unit", "alphaClones", "implantSet"]

View File

@@ -0,0 +1,33 @@
# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from sqlalchemy import Column, String, Integer, Table
from sqlalchemy.orm import mapper, synonym
from eos.db import gamedata_meta
from eos.gamedata import ImplantSet
implant_set_table = Table("implantsets", gamedata_meta,
Column("setID", Integer, primary_key=True),
Column("setName", String),
Column("gradeName", String),
Column("implants", String))
mapper(ImplantSet, implant_set_table,
properties={"ID": synonym("setID")})

View File

@@ -33,9 +33,6 @@ items_table = Table("invtypes", gamedata_meta,
Column("description", String),
Column("raceID", Integer),
Column("factionID", Integer),
Column("volume", Float),
Column("mass", Float),
Column("capacity", Float),
Column("published", Boolean),
Column("marketGroupID", Integer, ForeignKey("invmarketgroups.marketGroupID")),
Column("iconID", Integer),

View File

@@ -22,11 +22,11 @@ from sqlalchemy.orm import aliased, exc, join
from sqlalchemy.sql import and_, or_, select
import eos.config
from eos.db import gamedata_session
from eos.db import get_gamedata_session
from eos.db.gamedata.item import items_table
from eos.db.gamedata.group import groups_table
from eos.db.util import processEager, processWhere
from eos.gamedata import AlphaClone, Attribute, AttributeInfo, Category, DynamicItem, Group, Item, MarketGroup, MetaData, MetaGroup
from eos.gamedata import AlphaClone, Attribute, AttributeInfo, Category, DynamicItem, Group, Item, MarketGroup, MetaData, MetaGroup, ImplantSet
cache = {}
configVal = getattr(eos.config, "gamedataCache", None)
@@ -64,7 +64,7 @@ else:
return deco
def sqlizeString(line):
def sqlizeNormalString(line):
# Escape backslashes first, as they will be as escape symbol in queries
# Then escape percent and underscore signs
# Finally, replace generic wildcards with sql-style wildcards
@@ -79,29 +79,39 @@ itemNameMap = {}
def getItem(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
item = gamedata_session.query(Item).get(lookfor)
item = get_gamedata_session().query(Item).get(lookfor)
else:
item = gamedata_session.query(Item).options(*processEager(eager)).filter(Item.ID == lookfor).first()
item = get_gamedata_session().query(Item).options(*processEager(eager)).filter(Item.ID == lookfor).first()
elif isinstance(lookfor, str):
if lookfor in itemNameMap:
id = itemNameMap[lookfor]
if eager is None:
item = gamedata_session.query(Item).get(id)
item = get_gamedata_session().query(Item).get(id)
else:
item = gamedata_session.query(Item).options(*processEager(eager)).filter(Item.ID == id).first()
item = get_gamedata_session().query(Item).options(*processEager(eager)).filter(Item.ID == id).first()
else:
# Item names are unique, so we can use first() instead of one()
item = gamedata_session.query(Item).options(*processEager(eager)).filter(Item.name == lookfor).first()
item = get_gamedata_session().query(Item).options(*processEager(eager)).filter(Item.name == lookfor).first()
if item is not None:
itemNameMap[lookfor] = item.ID
else:
raise TypeError("Need integer or string as argument")
return item
@cachedQuery(1, "itemIDs")
def getItems(itemIDs, eager=None):
if not isinstance(itemIDs, (tuple, list, set)) or not all(isinstance(t, int) for t in itemIDs):
raise TypeError("Need iterable of integers as argument")
if eager is None:
items = get_gamedata_session().query(Item).filter(Item.ID.in_(itemIDs)).all()
else:
items = get_gamedata_session().query(Item).options(*processEager(eager)).filter(Item.ID.in_(itemIDs)).all()
return items
def getMutaplasmid(lookfor, eager=None):
if isinstance(lookfor, int):
item = gamedata_session.query(DynamicItem).filter(DynamicItem.ID == lookfor).first()
item = get_gamedata_session().query(DynamicItem).filter(DynamicItem.ID == lookfor).first()
else:
raise TypeError("Need integer as argument")
return item
@@ -109,7 +119,7 @@ def getMutaplasmid(lookfor, eager=None):
def getItemWithBaseItemAttribute(lookfor, baseItemID, eager=None):
# A lot of this is described in more detail in #1597
item = gamedata_session.query(Item).get(lookfor)
item = get_gamedata_session().query(Item).get(lookfor)
base = getItem(baseItemID)
# we have to load all attributes for this object, otherwise we'll lose access to them when we expunge.
@@ -125,7 +135,7 @@ def getItemWithBaseItemAttribute(lookfor, baseItemID, eager=None):
# Expunge the item form the session. This is required to have different Abyssal / Base combinations loaded in memory.
# Without expunging it, once one Abyssal Web is created, SQLAlchmey will use it for all others. We don't want this,
# we want to generate a completely new object to work with
gamedata_session.expunge(item)
get_gamedata_session().expunge(item)
return item
@@ -148,7 +158,7 @@ def getItems(lookfor, eager=None):
if len(toGet) > 0:
# Get items that aren't currently cached, and store them in the cache
items = gamedata_session.query(Item).filter(Item.ID.in_(toGet)).all()
items = get_gamedata_session().query(Item).filter(Item.ID.in_(toGet)).all()
for item in items:
cache[(item.ID, None)] = item
results += items
@@ -162,9 +172,9 @@ def getItems(lookfor, eager=None):
def getAlphaClone(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
item = gamedata_session.query(AlphaClone).get(lookfor)
item = get_gamedata_session().query(AlphaClone).get(lookfor)
else:
item = gamedata_session.query(AlphaClone).options(*processEager(eager)).filter(AlphaClone.ID == lookfor).first()
item = get_gamedata_session().query(AlphaClone).options(*processEager(eager)).filter(AlphaClone.ID == lookfor).first()
else:
raise TypeError("Need integer as argument")
return item
@@ -172,7 +182,7 @@ def getAlphaClone(lookfor, eager=None):
def getAlphaCloneList(eager=None):
eager = processEager(eager)
clones = gamedata_session.query(AlphaClone).options(*eager).all()
clones = get_gamedata_session().query(AlphaClone).options(*eager).all()
return clones
@@ -183,19 +193,19 @@ groupNameMap = {}
def getGroup(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
group = gamedata_session.query(Group).get(lookfor)
group = get_gamedata_session().query(Group).get(lookfor)
else:
group = gamedata_session.query(Group).options(*processEager(eager)).filter(Group.ID == lookfor).first()
group = get_gamedata_session().query(Group).options(*processEager(eager)).filter(Group.ID == lookfor).first()
elif isinstance(lookfor, str):
if lookfor in groupNameMap:
id = groupNameMap[lookfor]
if eager is None:
group = gamedata_session.query(Group).get(id)
group = get_gamedata_session().query(Group).get(id)
else:
group = gamedata_session.query(Group).options(*processEager(eager)).filter(Group.ID == id).first()
group = get_gamedata_session().query(Group).options(*processEager(eager)).filter(Group.ID == id).first()
else:
# Group names are unique, so we can use first() instead of one()
group = gamedata_session.query(Group).options(*processEager(eager)).filter(Group.name == lookfor).first()
group = get_gamedata_session().query(Group).options(*processEager(eager)).filter(Group.name == lookfor).first()
if group is not None:
groupNameMap[lookfor] = group.ID
else:
@@ -210,21 +220,21 @@ categoryNameMap = {}
def getCategory(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
category = gamedata_session.query(Category).get(lookfor)
category = get_gamedata_session().query(Category).get(lookfor)
else:
category = gamedata_session.query(Category).options(*processEager(eager)).filter(
category = get_gamedata_session().query(Category).options(*processEager(eager)).filter(
Category.ID == lookfor).first()
elif isinstance(lookfor, str):
if lookfor in categoryNameMap:
id = categoryNameMap[lookfor]
if eager is None:
category = gamedata_session.query(Category).get(id)
category = get_gamedata_session().query(Category).get(id)
else:
category = gamedata_session.query(Category).options(*processEager(eager)).filter(
category = get_gamedata_session().query(Category).options(*processEager(eager)).filter(
Category.ID == id).first()
else:
# Category names are unique, so we can use first() instead of one()
category = gamedata_session.query(Category).options(*processEager(eager)).filter(
category = get_gamedata_session().query(Category).options(*processEager(eager)).filter(
Category.name == lookfor).first()
if category is not None:
categoryNameMap[lookfor] = category.ID
@@ -240,21 +250,21 @@ metaGroupNameMap = {}
def getMetaGroup(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
metaGroup = gamedata_session.query(MetaGroup).get(lookfor)
metaGroup = get_gamedata_session().query(MetaGroup).get(lookfor)
else:
metaGroup = gamedata_session.query(MetaGroup).options(*processEager(eager)).filter(
metaGroup = get_gamedata_session().query(MetaGroup).options(*processEager(eager)).filter(
MetaGroup.ID == lookfor).first()
elif isinstance(lookfor, str):
if lookfor in metaGroupNameMap:
id = metaGroupNameMap[lookfor]
if eager is None:
metaGroup = gamedata_session.query(MetaGroup).get(id)
metaGroup = get_gamedata_session().query(MetaGroup).get(id)
else:
metaGroup = gamedata_session.query(MetaGroup).options(*processEager(eager)).filter(
metaGroup = get_gamedata_session().query(MetaGroup).options(*processEager(eager)).filter(
MetaGroup.ID == id).first()
else:
# MetaGroup names are unique, so we can use first() instead of one()
metaGroup = gamedata_session.query(MetaGroup).options(*processEager(eager)).filter(
metaGroup = get_gamedata_session().query(MetaGroup).options(*processEager(eager)).filter(
MetaGroup.name == lookfor).first()
if metaGroup is not None:
metaGroupNameMap[lookfor] = metaGroup.ID
@@ -264,16 +274,16 @@ def getMetaGroup(lookfor, eager=None):
def getMetaGroups():
return gamedata_session.query(MetaGroup).all()
return get_gamedata_session().query(MetaGroup).all()
@cachedQuery(1, "lookfor")
def getMarketGroup(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
marketGroup = gamedata_session.query(MarketGroup).get(lookfor)
marketGroup = get_gamedata_session().query(MarketGroup).get(lookfor)
else:
marketGroup = gamedata_session.query(MarketGroup).options(*processEager(eager)).filter(
marketGroup = get_gamedata_session().query(MarketGroup).options(*processEager(eager)).filter(
MarketGroup.ID == lookfor).first()
else:
raise TypeError("Need integer as argument")
@@ -285,7 +295,7 @@ def getMarketTreeNodeIds(rootNodeIds):
addedIds = set(rootNodeIds)
while addedIds:
allIds.update(addedIds)
addedIds = {mg.ID for mg in gamedata_session.query(MarketGroup).filter(MarketGroup.parentGroupID.in_(addedIds))}
addedIds = {mg.ID for mg in get_gamedata_session().query(MarketGroup).filter(MarketGroup.parentGroupID.in_(addedIds))}
return allIds
@@ -299,7 +309,7 @@ def getItemsByCategory(filter, where=None, eager=None):
raise TypeError("Need integer or string as argument")
filter = processWhere(filter, where)
return gamedata_session.query(Item).options(*processEager(eager)).join(Item.group, Group.category).filter(
return get_gamedata_session().query(Item).options(*processEager(eager)).join(Item.group, Group.category).filter(
filter).all()
@@ -314,9 +324,9 @@ def searchItems(nameLike, where=None, join=None, eager=None):
if not hasattr(join, "__iter__"):
join = (join,)
items = gamedata_session.query(Item).options(*processEager(eager)).join(*join)
items = get_gamedata_session().query(Item).options(*processEager(eager)).join(*join)
for token in nameLike.split(' '):
token_safe = "%{0}%".format(sqlizeString(token))
token_safe = "%{0}%".format(sqlizeNormalString(token))
if where is not None:
items = items.filter(and_(Item.name.like(token_safe, escape="\\"), where))
else:
@@ -325,14 +335,35 @@ def searchItems(nameLike, where=None, join=None, eager=None):
return items
@cachedQuery(3, "tokens", "where", "join")
def searchItemsRegex(tokens, where=None, join=None, eager=None):
if not isinstance(tokens, (tuple, list)) or not all(isinstance(t, str) for t in tokens):
raise TypeError("Need tuple or list of strings as argument")
if join is None:
join = tuple()
if not hasattr(join, "__iter__"):
join = (join,)
items = get_gamedata_session().query(Item).options(*processEager(eager)).join(*join)
for token in tokens:
if where is not None:
items = items.filter(and_(Item.name.op('regexp')(token), where))
else:
items = items.filter(Item.name.op('regexp')(token))
items = items.limit(100).all()
return items
@cachedQuery(3, "where", "nameLike", "join")
def searchSkills(nameLike, where=None, eager=None):
if not isinstance(nameLike, str):
raise TypeError("Need string as argument")
items = gamedata_session.query(Item).options(*processEager(eager)).join(Item.group, Group.category)
items = get_gamedata_session().query(Item).options(*processEager(eager)).join(Item.group, Group.category)
for token in nameLike.split(' '):
token_safe = "%{0}%".format(sqlizeString(token))
token_safe = "%{0}%".format(sqlizeNormalString(token))
if where is not None:
items = items.filter(and_(Item.name.like(token_safe, escape="\\"), Category.ID == 16, where))
else:
@@ -352,7 +383,7 @@ def getVariations(itemids, groupIDs=None, where=None, eager=None):
itemfilter = or_(*(items_table.c.variationParentTypeID == itemid for itemid in itemids))
filter = processWhere(itemfilter, where)
vars = gamedata_session.query(Item).options(*processEager(eager)).filter(filter).all()
vars = get_gamedata_session().query(Item).options(*processEager(eager)).filter(filter).all()
if vars:
return vars
@@ -360,7 +391,7 @@ def getVariations(itemids, groupIDs=None, where=None, eager=None):
itemfilter = or_(*(groups_table.c.groupID == groupID for groupID in groupIDs))
filter = processWhere(itemfilter, where)
joinon = items_table.c.groupID == groups_table.c.groupID
vars = gamedata_session.query(Item).options(*processEager(eager)).join((groups_table, joinon)).filter(
vars = get_gamedata_session().query(Item).options(*processEager(eager)).join((groups_table, joinon)).filter(
filter).all()
return vars
@@ -375,7 +406,7 @@ def getAttributeInfo(attr, eager=None):
else:
raise TypeError("Need integer or string as argument")
try:
result = gamedata_session.query(AttributeInfo).options(*processEager(eager)).filter(filter).one()
result = get_gamedata_session().query(AttributeInfo).options(*processEager(eager)).filter(filter).one()
except exc.NoResultFound:
result = None
return result
@@ -384,7 +415,7 @@ def getAttributeInfo(attr, eager=None):
@cachedQuery(1, "field")
def getMetaData(field):
if isinstance(field, str):
data = gamedata_session.query(MetaData).get(field)
data = get_gamedata_session().query(MetaData).get(field)
else:
raise TypeError("Need string as argument")
return data
@@ -403,12 +434,12 @@ def directAttributeRequest(itemIDs, attrIDs):
and_(Attribute.attributeID.in_(attrIDs), Item.typeID.in_(itemIDs)),
from_obj=[join(Attribute, Item)])
result = gamedata_session.execute(q).fetchall()
result = get_gamedata_session().execute(q).fetchall()
return result
def getAbyssalTypes():
return set([r.resultingTypeID for r in gamedata_session.query(DynamicItem.resultingTypeID).distinct()])
return set([r.resultingTypeID for r in get_gamedata_session().query(DynamicItem.resultingTypeID).distinct()])
@cachedQuery(1, "itemID")
@@ -416,11 +447,17 @@ def getDynamicItem(itemID, eager=None):
try:
if isinstance(itemID, int):
if eager is None:
result = gamedata_session.query(DynamicItem).filter(DynamicItem.ID == itemID).one()
result = get_gamedata_session().query(DynamicItem).filter(DynamicItem.ID == itemID).one()
else:
result = gamedata_session.query(DynamicItem).options(*processEager(eager)).filter(DynamicItem.ID == itemID).one()
result = get_gamedata_session().query(DynamicItem).options(*processEager(eager)).filter(DynamicItem.ID == itemID).one()
else:
raise TypeError("Need integer as argument")
except exc.NoResultFound:
result = None
return result
@cachedQuery(1, "lookfor")
def getAllImplantSets():
implantSets = get_gamedata_session().query(ImplantSet).all()
return implantSets

View File

@@ -6,16 +6,16 @@ Allow use of floats in damage pattern values
tmpTable = """
CREATE TABLE "damagePatternsTemp" (
"ID" INTEGER NOT NULL,
"name" VARCHAR,
"emAmount" FLOAT,
"thermalAmount" FLOAT,
"kineticAmount" FLOAT,
"explosiveAmount" FLOAT,
"ownerID" INTEGER,
"created" DATETIME,
"modified" DATETIME,
PRIMARY KEY ("ID"),
"ID" INTEGER NOT NULL,
"name" VARCHAR,
"emAmount" FLOAT,
"thermalAmount" FLOAT,
"kineticAmount" FLOAT,
"explosiveAmount" FLOAT,
"ownerID" INTEGER,
"created" DATETIME,
"modified" DATETIME,
PRIMARY KEY ("ID"),
FOREIGN KEY("ownerID") REFERENCES users ("ID")
)
"""

View File

@@ -0,0 +1,44 @@
"""
Migration 37
- Capacitor Booster tiericide
"""
CONVERSIONS = {
4959: ( # 'Seed' Micro Capacitor Booster I
4957, # Micro Brief Capacitor Overcharge I
4961, # Micro Tapered Capacitor Infusion I
4955, # Micro F-RX Prototype Capacitor Boost
3556, # Micro Capacitor Booster I
3558, # Micro Capacitor Booster II
15774, # Ammatar Navy Micro Capacitor Booster
14180, # Dark Blood Micro Capacitor Booster
14182, # True Sansha Micro Capacitor Booster
15782, # Imperial Navy Micro Capacitor Booster
),
5011: ( # Small F-RX Compact Capacitor Booster
5009, # Small Brief Capacitor Overcharge I
5013, # Small Tapered Capacitor Infusion I
5007, # Small F-RX Prototype Capacitor Boost
),
4833: ( # Medium F-RX Compact Capacitor Booster
4831, # Medium Brief Capacitor Overcharge I
4835, # Medium Tapered Capacitor Infusion I
4829, # Medium F-RX Prototype Capacitor Boost
),
5051: ( # Heavy F-RX Compact Capacitor Booster
5049, # Heavy Brief Capacitor Overcharge I
5053, # Heavy Tapered Capacitor Infusion I
5047, # Heavy F-RX Prototype Capacitor Boost
)
}
def upgrade(saveddata_engine):
# Convert modules
for replacement_item, list in CONVERSIONS.items():
for retired_item in list:
saveddata_engine.execute('UPDATE "modules" SET "itemID" = ? WHERE "itemID" = ?',
(replacement_item, retired_item))
saveddata_engine.execute('UPDATE "cargo" SET "itemID" = ? WHERE "itemID" = ?',
(replacement_item, retired_item))

View File

@@ -0,0 +1,42 @@
"""
Migration 38
- Armor hardener tiericide
"""
CONVERSIONS = {
16357: ( # Experimental Enduring EM Armor Hardener I
16353, # Upgraded Armor EM Hardener I
),
16365: ( # Experimental Enduring Explosive Armor Hardener I
16361, # Upgraded Armor Explosive Hardener I
),
16373: ( # Experimental Enduring Kinetic Armor Hardener I
16369, # Upgraded Armor Kinetic Hardener I
),
16381: ( # Experimental Enduring Thermal Armor Hardener I
16377, # Upgraded Armor Thermal Hardener I
),
16359: ( # Prototype Compact EM Armor Hardener I
16355, # Limited Armor EM Hardener I
),
16367: ( # Prototype Compact Explosive Armor Hardener I
16363, # Limited Armor Explosive Hardener I
),
16375: ( # Prototype Compact Kinetic Armor Hardener I
16371, # Limited Armor Kinetic Hardener I
),
16383: ( # Prototype Compact Thermal Armor Hardener I
16379, # Limited Armor Thermal Hardener I
)
}
def upgrade(saveddata_engine):
# Convert modules
for replacement_item, list in CONVERSIONS.items():
for retired_item in list:
saveddata_engine.execute('UPDATE "modules" SET "itemID" = ? WHERE "itemID" = ?',
(replacement_item, retired_item))
saveddata_engine.execute('UPDATE "cargo" SET "itemID" = ? WHERE "itemID" = ?',
(replacement_item, retired_item))

View File

@@ -0,0 +1,34 @@
"""
Migration 39
- Shield amplifier tiericide
- CCP getting rid of DB TDs due to exploits
"""
CONVERSIONS = {
1798: ( # 'Basic' EM Shield Amplifier
9562, # Supplemental EM Ward Amplifier
),
1804: ( # 'Basic' Explosive Shield Amplifier
9574, # Supplemental Explosive Deflection Amplifier
),
1802: ( # 'Basic' Kinetic Shield Amplifier
9570, # Supplemental Kinetic Deflection Amplifier
),
1800: ( # 'Basic' Thermal Shield Amplifier
9566, # Supplemental Thermal Dissipation Amplifier
),
22933: ( # 'Investor' Tracking Disruptor I
32416, # Dark Blood Tracking Disruptor
)
}
def upgrade(saveddata_engine):
# Convert modules
for replacement_item, list in CONVERSIONS.items():
for retired_item in list:
saveddata_engine.execute('UPDATE "modules" SET "itemID" = ? WHERE "itemID" = ?',
(replacement_item, retired_item))
saveddata_engine.execute('UPDATE "cargo" SET "itemID" = ? WHERE "itemID" = ?',
(replacement_item, retired_item))

View File

@@ -0,0 +1,18 @@
"""
Migration 40
Imports all item conversions since Migration 28 and runs them against module.baseItemID. This column seems to have been
forgotten about since it's been added.
"""
from .upgrade36 import CONVERSIONS as u36
from .upgrade37 import CONVERSIONS as u37
from .upgrade38 import CONVERSIONS as u38
from .upgrade39 import CONVERSIONS as u39
def upgrade(saveddata_engine):
for conversions in [u36, u37, u38, u39]:
for replacement_item, list in conversions.items():
for retired_item in list:
saveddata_engine.execute('UPDATE "modules" SET "baseItemID" = ? WHERE "baseItemID" = ?',
(replacement_item, retired_item))

View File

@@ -0,0 +1,50 @@
"""
Migration 41
- Resistance plating tiericide
"""
CONVERSIONS = {
16345: ( # Upgraded Layered Coating I
16347, # Limited Layered Plating I
16349, # 'Scarab' Layered Plating I
16351, # 'Grail' Layered Plating I
),
16305: ( # Upgraded Multispectrum Coating I
16307, # Limited Adaptive Nano Plating I
16309, # 'Collateral' Adaptive Nano Plating I
16311, # 'Refuge' Adaptive Nano Plating I
),
16329: ( # Upgraded EM Coating I
16331, # Limited EM Plating I
16333, # 'Contour' EM Plating I
16335, # 'Spiegel' EM Plating I
),
16321: ( # Upgraded Explosive Coating I
16323, # Limited Explosive Plating I
16325, # Experimental Explosive Plating I
16319, # 'Aegis' Explosive Plating I
),
16313: ( # Upgraded Kinetic Coating I
16315, # Limited Kinetic Plating I
16317, # Experimental Kinetic Plating I
16327, # 'Element' Kinetic Plating I
),
16337: ( # Upgraded Thermal Coating I
16339, # Limited Thermal Plating I
16341, # Experimental Thermal Plating I
16343, # Prototype Thermal Plating I
)
}
def upgrade(saveddata_engine):
# Convert modules
for replacement_item, list in CONVERSIONS.items():
for retired_item in list:
saveddata_engine.execute('UPDATE "modules" SET "itemID" = ? WHERE "itemID" = ?',
(replacement_item, retired_item))
saveddata_engine.execute('UPDATE "modules" SET "baseItemID" = ? WHERE "baseItemID" = ?',
(replacement_item, retired_item))
saveddata_engine.execute('UPDATE "cargo" SET "itemID" = ? WHERE "itemID" = ?',
(replacement_item, retired_item))

View File

@@ -0,0 +1,50 @@
"""
Migration 42
- Resistance membrane tiericide
"""
CONVERSIONS = {
16391: ( # Compact Multispectrum Energized Membrane
16389, # Experimental Energized Adaptive Nano Membrane I
16387, # Limited Energized Adaptive Nano Membrane I
16385, # Upgraded Energized Adaptive Nano Membrane I
),
16423: ( # Compact Layered Energized Membrane
16421, # Experimental Energized Armor Layering Membrane I
16419, # Limited Energized Armor Layering Membrane I
16417, # Upgraded Energized Armor Layering Membrane I
),
16415: ( # Compact EM Energized Membrane
16413, # Experimental Energized EM Membrane I
16411, # Limited Energized EM Membrane I
16409, # Upgraded Energized EM Membrane I
),
16407: ( # Compact Explosive Energized Membrane
16405, # Experimental Energized Explosive Membrane I
16403, # Limited Energized Explosive Membrane I
16401, # Upgraded Energized Explosive Membrane I
),
16399: ( # Compact Kinetic Energized Membrane
16397, # Experimental Energized Kinetic Membrane I
16395, # Limited Energized Kinetic Membrane I
16393, # Upgraded Energized Kinetic Membrane I
),
16431: ( # Compact Thermal Energized Membrane
16429, # Experimental Energized Thermal Membrane I
16427, # Limited Energized Thermal Membrane I
16425, # Upgraded Energized Thermal Membrane I
)
}
def upgrade(saveddata_engine):
# Convert modules
for replacement_item, list in CONVERSIONS.items():
for retired_item in list:
saveddata_engine.execute('UPDATE "modules" SET "itemID" = ? WHERE "itemID" = ?',
(replacement_item, retired_item))
saveddata_engine.execute('UPDATE "modules" SET "baseItemID" = ? WHERE "baseItemID" = ?',
(replacement_item, retired_item))
saveddata_engine.execute('UPDATE "cargo" SET "itemID" = ? WHERE "itemID" = ?',
(replacement_item, retired_item))

View File

@@ -470,7 +470,7 @@ def searchFits(nameLike, where=None, eager=None):
filter = processWhere(Fit.name.like(nameLike, escape="\\"), where)
eager = processEager(eager)
with sd_lock:
fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).all())
fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).limit(100).all())
return fits

File diff suppressed because it is too large Load Diff

View File

@@ -146,6 +146,12 @@ class Effect(EqBase):
return self.__effectDef is not None
@property
def dealsDamage(self):
if not self.__generated:
self.__generateHandler()
return self.__dealsDamage
def isType(self, type):
"""
Check if this effect is of the passed type
@@ -167,6 +173,7 @@ class Effect(EqBase):
self.__handler = getattr(effectDef, "handler", eos.effects.BaseEffect.handler)
self.__runTime = getattr(effectDef, "runTime", "normal")
self.__activeByDefault = getattr(effectDef, "activeByDefault", True)
self.__dealsDamage = effectDef.dealsDamage
effectType = getattr(effectDef, "type", None)
effectType = effectType if isinstance(effectType, tuple) or effectType is None else (effectType,)
self.__type = effectType
@@ -175,6 +182,7 @@ class Effect(EqBase):
self.__handler = eos.effects.DummyEffect.handler
self.__runTime = "normal"
self.__activeByDefault = True
self.__dealsDamage = False
self.__type = None
pyfalog.debug("ImportError generating handler: {0}", e)
except AttributeError as e:
@@ -182,6 +190,7 @@ class Effect(EqBase):
self.__handler = eos.effects.DummyEffect.handler
self.__runTime = "normal"
self.__activeByDefault = True
self.__dealsDamage = False
self.__type = None
pyfalog.error("AttributeError generating handler: {0}", e)
except (KeyboardInterrupt, SystemExit):
@@ -190,6 +199,7 @@ class Effect(EqBase):
self.__handler = eos.effects.DummyEffect.handler
self.__runTime = "normal"
self.__activeByDefault = True
self.__dealsDamage = False
self.__type = None
pyfalog.critical("Exception generating handler:")
pyfalog.critical(e)
@@ -209,40 +219,13 @@ class Effect(EqBase):
class Item(EqBase):
MOVE_ATTRS = (4, # Mass
38, # Capacity
161) # Volume
MOVE_ATTR_INFO = None
ABYSSAL_TYPES = None
@classmethod
def getMoveAttrInfo(cls):
info = getattr(cls, "MOVE_ATTR_INFO", None)
if info is None:
cls.MOVE_ATTR_INFO = info = []
for id in cls.MOVE_ATTRS:
info.append(eos.db.getAttributeInfo(id))
return info
def moveAttrs(self):
self.__moved = True
for info in self.getMoveAttrInfo():
val = getattr(self, info.name, 0)
if val != 0:
attr = Attribute()
attr.info = info
attr.value = val
self.__attributes[info.name] = attr
@reconstructor
def init(self):
self.__race = None
self.__requiredSkills = None
self.__requiredFor = None
self.__moved = False
self.__offensive = None
self.__assistive = None
self.__overrides = None
@@ -264,9 +247,6 @@ class Item(EqBase):
@property
def attributes(self):
if not self.__moved:
self.moveAttrs()
return self.__attributes
@property
@@ -363,7 +343,11 @@ class Item(EqBase):
if self.__race is None:
try:
if self.category.categoryName == 'Structure':
if (
self.category.categoryName == 'Structure' or
# Here until CCP puts their shit together
self.name in ("Thunderchild", "Stormbringer", "Skybreaker")
):
self.__race = "upwell"
else:
self.__race = self.factionMap[self.factionID]
@@ -723,5 +707,15 @@ class Unit(EqBase):
return value
class Traits(EqBase):
pass
class ImplantSet(EqBase):
@property
def fullName(self):
if not self.gradeName:
return self.setName
return '{} {}'.format(self.gradeName, self.setName)

View File

@@ -21,6 +21,9 @@ from logbook import Logger
from sqlalchemy.orm import reconstructor
from eos.utils.round import roundToPrec
pyfalog = Logger(__name__)
@@ -56,9 +59,8 @@ class BoosterSideEffect:
@property
def name(self):
return "{0}% {1}".format(
self.booster.getModifiedItemAttr(self.attr),
self.__effect.getattr('displayName') or self.__effect.name,
)
roundToPrec(self.booster.getModifiedItemAttr(self.attr), 5),
self.__effect.getattr('displayName') or self.__effect.name)
@property
def attr(self):

View File

@@ -364,3 +364,11 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if self.item.groupID in fitDroneGroupLimits:
return True
return False
def canDealDamage(self, ignoreState=False):
if self.item is None:
return False
for effect in self.item.effects.values():
if effect.dealsDamage and (ignoreState or self.amountActive > 0):
return True
return False

View File

@@ -441,3 +441,15 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return False
return True
def canDealDamage(self, ignoreState=False, ignoreAbilityState=False):
if self.item is None:
return False
if not self.active and not ignoreState:
return False
for ability in self.abilities:
if not ability.active and not ignoreAbilityState:
continue
if ability.effect.dealsDamage:
return True
return False

View File

@@ -21,7 +21,7 @@ import datetime
import time
from copy import deepcopy
from itertools import chain
from math import log, sqrt
from math import floor, log, sqrt
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
@@ -39,6 +39,7 @@ from eos.saveddata.damagePattern import DamagePattern
from eos.saveddata.module import Module
from eos.saveddata.ship import Ship
from eos.saveddata.targetProfile import TargetProfile
from eos.utils.float import floatUnerr
from eos.utils.stats import DmgTypes, RRTypes
@@ -378,8 +379,9 @@ class Fit:
@property
def maxTargets(self):
return min(self.extraAttributes["maxTargetsLockedFromSkills"],
self.ship.getModifiedItemAttr("maxLockedTargets"))
maxTargets = min(self.extraAttributes["maxTargetsLockedFromSkills"],
self.ship.getModifiedItemAttr("maxLockedTargets"))
return floor(floatUnerr(maxTargets))
@property
def maxTargetRange(self):
@@ -576,11 +578,15 @@ class Fit:
if warfareBuffID == 11: # Shield Burst: Active Shielding: Repair Duration/Capacitor
self.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill("Shield Operation") or mod.item.requiresSkill(
"Shield Emission Systems"), "capacitorNeed", value)
lambda mod: mod.item.requiresSkill("Shield Operation") or
mod.item.requiresSkill("Shield Emission Systems") or
mod.item.requiresSkill("Capital Shield Emission Systems"),
"capacitorNeed", value)
self.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill("Shield Operation") or mod.item.requiresSkill(
"Shield Emission Systems"), "duration", value)
lambda mod: mod.item.requiresSkill("Shield Operation") or
mod.item.requiresSkill("Shield Emission Systems") or
mod.item.requiresSkill("Capital Shield Emission Systems"),
"duration", value)
if warfareBuffID == 12: # Shield Burst: Shield Extension: Shield HP
self.ship.boostItemAttr("shieldCapacity", value, stackingPenalties=True)
@@ -590,12 +596,16 @@ class Fit:
self.ship.boostItemAttr("armor%sDamageResonance" % damageType, value, stackingPenalties=True)
if warfareBuffID == 14: # Armor Burst: Rapid Repair: Repair Duration/Capacitor
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or
mod.item.requiresSkill("Repair Systems"),
"capacitorNeed", value)
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or
mod.item.requiresSkill("Repair Systems"),
"duration", value)
self.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or
mod.item.requiresSkill("Repair Systems") or
mod.item.requiresSkill("Capital Remote Armor Repair Systems"),
"capacitorNeed", value)
self.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or
mod.item.requiresSkill("Repair Systems") or
mod.item.requiresSkill("Capital Remote Armor Repair Systems"),
"duration", value)
if warfareBuffID == 15: # Armor Burst: Armor Reinforcement: Armor HP
self.ship.boostItemAttr("armorHP", value, stackingPenalties=True)
@@ -742,6 +752,8 @@ class Fit:
if warfareBuffID == 79: # AOE_Beacon_bioluminescence_cloud
self.ship.boostItemAttr("signatureRadius", value, stackingPenalties=True)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"signatureRadius", value, stackingPenalties=True)
if warfareBuffID == 80: # AOE_Beacon_caustic_cloud_local_repair
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Repair Systems"),
@@ -764,7 +776,11 @@ class Fit:
if warfareBuffID == 90: # Weather_electric_storm_EM_resistance_penalty
for tankType in ("shield", "armor"):
self.ship.boostItemAttr("{}EmDamageResonance".format(tankType), value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"{}EmDamageResonance".format(tankType), value)
self.ship.boostItemAttr("emDamageResonance", value) # for hull
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"emDamageResonance", value) #for hull
if warfareBuffID == 92: # Weather_electric_storm_capacitor_recharge_bonus
self.ship.boostItemAttr("rechargeRate", value, stackingPenalties=True)
@@ -772,32 +788,54 @@ class Fit:
if warfareBuffID == 93: # Weather_xenon_gas_explosive_resistance_penalty
for tankType in ("shield", "armor"):
self.ship.boostItemAttr("{}ExplosiveDamageResonance".format(tankType), value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"{}ExplosiveDamageResonance".format(tankType), value)
self.ship.boostItemAttr("explosiveDamageResonance", value) # for hull
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"explosiveDamageResonance", value) # for hull
if warfareBuffID == 94: # Weather_xenon_gas_shield_hp_bonus
self.ship.boostItemAttr("shieldCapacity", value) # for hull
self.ship.boostItemAttr("shieldCapacity", value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"shieldCapacity", value)
if warfareBuffID == 95: # Weather_infernal_thermal_resistance_penalty
for tankType in ("shield", "armor"):
self.ship.boostItemAttr("{}ThermalDamageResonance".format(tankType), value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"{}ThermalDamageResonance".format(tankType), value)
self.ship.boostItemAttr("thermalDamageResonance", value) # for hull
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"thermalDamageResonance", value) # for hull
if warfareBuffID == 96: # Weather_infernal_armor_hp_bonus
self.ship.boostItemAttr("armorHP", value) # for hull
self.ship.boostItemAttr("armorHP", value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"armorHP", value)
if warfareBuffID == 97: # Weather_darkness_turret_range_penalty
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"),
"maxRange", value, stackingPenalties=True)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"maxRange", value, stackingPenalties=True)
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"),
"falloff", value, stackingPenalties=True)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"falloff", value, stackingPenalties=True)
if warfareBuffID == 98: # Weather_darkness_velocity_bonus
self.ship.boostItemAttr("maxVelocity", value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"maxVelocity", value)
if warfareBuffID == 99: # Weather_caustic_toxin_kinetic_resistance_penalty
for tankType in ("shield", "armor"):
self.ship.boostItemAttr("{}KineticDamageResonance".format(tankType), value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"{}KineticDamageResonance".format(tankType), value)
self.ship.boostItemAttr("kineticDamageResonance", value) # for hull
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"kineticDamageResonance", value) # for hull
if warfareBuffID == 100: # Weather_caustic_toxin_scan_resolution_bonus
self.ship.boostItemAttr("scanResolution", value, stackingPenalties=True)
@@ -1018,6 +1056,16 @@ class Fit:
if mod.isEmpty:
del self.modules[i]
def clearTail(self):
tailPositions = {}
for mod in reversed(self.modules):
if not mod.isEmpty:
break
tailPositions[self.modules.index(mod)] = mod.slot
for pos in sorted(tailPositions, reverse=True):
self.modules.remove(self.modules[pos])
return tailPositions
@property
def modCount(self):
x = 0
@@ -1127,7 +1175,7 @@ class Fit:
def droneBayUsed(self):
amount = 0
for d in self.drones:
amount += d.item.volume * d.amount
amount += d.item.attributes['volume'].value * d.amount
return amount
@@ -1135,7 +1183,7 @@ class Fit:
def fighterBayUsed(self):
amount = 0
for f in self.fighters:
amount += f.item.volume * f.amount
amount += f.item.attributes['volume'].value * f.amount
return amount

View File

@@ -64,7 +64,9 @@ ProjectedSystem = {
class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
"""An instance of this class represents a module together with its charge and modified attributes"""
MINING_ATTRIBUTES = ("miningAmount",)
SYSTEM_GROUPS = ("Effect Beacon", "MassiveEnvironments", "Abyssal Hazards", "Non-Interactable Object")
SYSTEM_GROUPS = (
"Effect Beacon", "MassiveEnvironments", "Abyssal Hazards",
"Non-Interactable Object", "Destructible Effect Beacon")
def __init__(self, item, baseItem=None, mutaplasmid=None):
"""Initialize a module from the program"""
@@ -214,8 +216,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if charge is None:
charges = 0
else:
chargeVolume = charge.volume
containerCapacity = self.item.capacity
chargeVolume = charge.attributes['volume'].value
containerCapacity = self.item.attributes['capacity'].value
if chargeVolume is None or containerCapacity is None:
charges = 0
else:
@@ -461,6 +463,20 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return True
return False
def canDealDamage(self, ignoreState=False):
if self.isEmpty:
return False
for effect in self.item.effects.values():
if effect.dealsDamage and (
ignoreState or
effect.isType('offline') or
(effect.isType('passive') and self.state >= FittingModuleState.ONLINE) or
(effect.isType('active') and self.state >= FittingModuleState.ACTIVE) or
(effect.isType('overheat') and self.state >= FittingModuleState.OVERHEATED)
):
return True
return False
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)}
@@ -696,7 +712,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
# Check this only if we're told to do so
if hardpointLimit:
if fit.getHardpointsFree(self.hardpoint) < 1:
if fit.getHardpointsFree(self.hardpoint) < (1 if self.owner != fit else 0):
return False
return True
@@ -712,6 +728,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return False
elif state == FittingModuleState.OVERHEATED and not self.item.isType("overheat"):
return False
# Some destructible effect beacons contain active effects, hardcap those at online state
elif state > FittingModuleState.ONLINE and self.slot == FittingSlot.SYSTEM:
return False
else:
return True
@@ -778,8 +797,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
# Check sizes, if 'charge size > module volume' it won't fit
if charge is None:
return True
chargeVolume = charge.volume
moduleCapacity = self.item.capacity
chargeVolume = charge.attributes['volume'].value
moduleCapacity = self.item.attributes['capacity'].value
if chargeVolume is not None and moduleCapacity is not None and chargeVolume > moduleCapacity:
return False
@@ -1037,7 +1056,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
elif click == "ctrl":
state = FittingModuleState.OFFLINE
else:
state = transitionMap[currState]
try:
state = transitionMap[currState]
except KeyError:
state = min(transitionMap)
# If passive module tries to transition into online and fails,
# put it to passive instead
if not mod.isValidState(state) and currState == FittingModuleState.ONLINE:

View File

@@ -81,6 +81,8 @@ class Mutator(EqBase):
@validates("value")
def validator(self, key, val):
""" Validates values as properly falling within the range of the modules' Mutaplasmid """
if self.baseValue == 0:
return 0
mod = val / self.baseValue
if self.minMod <= mod <= self.maxMod:

View File

@@ -86,7 +86,7 @@ BUILTINS = OrderedDict([
# Source: ticket #2067
(-52, ('[NPC][Invasion]Invading Precursor Entities', 0.422, 0.367, 0.453, 0.411)),
(-53, ('[NPC][Invasion]Retaliating Amarr Entities', 0.360, 0.310, 0.441, 0.602)),
(-54, ('[NPC][Invasion]Retaliating Caldari Entities', 0.287, 0.610, 0.487, 0.401)),
(-54, ('[NPC][Invasion]Retaliating Caldari Entities', 0.303, 0.610, 0.487, 0.401)),
(-55, ('[NPC][Invasion]Retaliating Gallente Entities', 0.383, 0.414, 0.578, 0.513)),
(-56, ('[NPC][Invasion]Retaliating Minmatar Entities', 0.620, 0.422, 0.355, 0.399)),
(-57, ('[NPC][Abyssal][Dark Matter All Tiers]Drones', 0.439, 0.522, 0.529, 0.435)),

27
eos/utils/round.py Normal file
View File

@@ -0,0 +1,27 @@
import math
def roundToPrec(val, prec, nsValue=None):
"""
nsValue: custom value which should be used to determine normalization shift
"""
# We're not rounding integers anyway
# Also make sure that we do not ask to calculate logarithm of zero
if int(val) == val:
return int(val)
roundFactor = int(prec - math.floor(math.log10(abs(val if nsValue is None else nsValue))) - 1)
# But we don't want to round integers
if roundFactor < 0:
roundFactor = 0
# Do actual rounding
val = round(val, roundFactor)
# Make sure numbers with .0 part designating float don't get through
if int(val) == val:
val = int(val)
return val
def roundDec(val, prec):
if int(val) == val:
return int(val)
return round(val, prec)

View File

@@ -26,11 +26,12 @@ VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle'
class YDef:
def __init__(self, handle, unit, label, selectorLabel=None):
def __init__(self, handle, unit, label, selectorLabel=None, hidden=False):
self.handle = handle
self.unit = unit
self.label = label
self._selectorLabel = selectorLabel
self.hidden = hidden
@property
def selectorLabel(self):
@@ -53,12 +54,13 @@ class YDef:
class XDef:
def __init__(self, handle, unit, label, mainInput, selectorLabel=None):
def __init__(self, handle, unit, label, mainInput, selectorLabel=None, hidden=False):
self.handle = handle
self.unit = unit
self.label = label
self.mainInput = mainInput
self._selectorLabel = selectorLabel
self.hidden = hidden
@property
def selectorLabel(self):

View File

@@ -37,7 +37,14 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
if mod.hardpoint == FittingHardpoint.TURRET:
if "ChainLightning" in mod.item.effects:
if inLockRange:
applicationMap[mod] = getVortonMult(
mod=mod,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
elif mod.hardpoint == FittingHardpoint.TURRET:
if inLockRange:
applicationMap[mod] = getTurretMult(
mod=mod,
@@ -56,7 +63,6 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
if inLockRange or (mod.charge is not None and 'fofMissileLaunching' in mod.charge.effects):
applicationMap[mod] = getLauncherMult(
mod=mod,
src=src,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
@@ -151,7 +157,21 @@ def getTurretMult(mod, src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngl
return mult
def getLauncherMult(mod, src, distance, tgtSpeed, tgtSigRadius):
def getVortonMult(mod, distance, tgtSpeed, tgtSigRadius):
rangeFactor = calculateRangeFactor(
mod.getModifiedItemAttr('maxRange'),
0,
distance)
applicationFactor = _calcMissileFactor(
atkEr=mod.getModifiedItemAttr('aoeCloudSize'),
atkEv=mod.getModifiedItemAttr('aoeVelocity'),
atkDrf=mod.getModifiedItemAttr('aoeDamageReductionFactor'),
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
return rangeFactor * applicationFactor
def getLauncherMult(mod, distance, tgtSpeed, tgtSigRadius):
missileMaxRangeData = mod.missileMaxRangeData
if missileMaxRangeData is None:
return 0

View File

@@ -31,8 +31,8 @@ from .getter import (
class FitDamageStatsGraph(FitGraph):
def __init__(self):
super().__init__()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._timeCache = TimeCache()
self._projectedCache = ProjectedDataCache()

View File

@@ -23,24 +23,6 @@ import math
from graphs.data.base import SmoothPointGetter
class Time2SpeedGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxSpeed': src.getMaxVelocity(),
'mass': src.item.ship.getModifiedItemAttr('mass'),
'agility': src.item.ship.getModifiedItemAttr('agility')}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
maxSpeed = commonData['maxSpeed']
mass = commonData['mass']
agility = commonData['agility']
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
speed = maxSpeed * (1 - math.exp((-time * 1000000) / (agility * mass)))
return speed
class Time2DistanceGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
@@ -60,3 +42,62 @@ class Time2DistanceGetter(SmoothPointGetter):
distance_0 = maxSpeed * 0 + (maxSpeed * agility * mass * math.exp((-0 * 1000000) / (agility * mass)) / 1000000)
distance = distance_t - distance_0
return distance
class Time2SpeedGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxSpeed': src.getMaxVelocity(),
'mass': src.item.ship.getModifiedItemAttr('mass'),
'agility': src.item.ship.getModifiedItemAttr('agility')}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
maxSpeed = commonData['maxSpeed']
mass = commonData['mass']
agility = commonData['agility']
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
speed = maxSpeed * (1 - math.exp((-time * 1000000) / (agility * mass)))
return speed
class Time2MomentumGetter(Time2SpeedGetter):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
mass = commonData['mass']
speed = Time2SpeedGetter._calculatePoint(
self, x=x, miscParams=miscParams,
src=src, tgt=tgt, commonData=commonData)
momentum = speed * mass
return momentum
class Time2BumpSpeedGetter(Time2SpeedGetter):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
# S. Santorine, Ship Motion in EVE-Online, p3, Collisions & Bumping section
# https://docs.google.com/document/d/1rwVWjTvzVdPEFETf0vwm649AFb4bgRBaNLpRPaoB03o
# Internally, Santorine's formulas are using millions of kilograms, so we normalize to them here
bumperMass = commonData['mass'] / 10 ** 6
bumperSpeed = Time2SpeedGetter._calculatePoint(
self, x=x, miscParams=miscParams,
src=src, tgt=tgt, commonData=commonData)
tgtMass = miscParams['tgtMass'] / 10 ** 6
tgtSpeed = (2 * bumperSpeed * bumperMass) / (bumperMass + tgtMass)
return tgtSpeed
class Time2BumpDistanceGetter(Time2BumpSpeedGetter):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
# S. Santorine, Ship Motion in EVE-Online, p3, Collisions & Bumping section
# https://docs.google.com/document/d/1rwVWjTvzVdPEFETf0vwm649AFb4bgRBaNLpRPaoB03o
# Internally, Santorine's formulas are using millions of kilograms, so we normalize to them here
tgtMass = miscParams['tgtMass'] / 10 ** 6
tgtInertia = miscParams['tgtInertia']
tgtSpeed = Time2BumpSpeedGetter._calculatePoint(
self, x=x, miscParams=miscParams,
src=src, tgt=tgt, commonData=commonData)
tgtDistance = tgtSpeed * tgtMass * tgtInertia
return tgtDistance

View File

@@ -19,7 +19,7 @@
from graphs.data.base import FitGraph, XDef, YDef, Input
from .getter import Time2SpeedGetter, Time2DistanceGetter
from .getter import Time2SpeedGetter, Time2DistanceGetter, Time2MomentumGetter, Time2BumpSpeedGetter, Time2BumpDistanceGetter
class FitMobilityGraph(FitGraph):
@@ -30,12 +30,26 @@ class FitMobilityGraph(FitGraph):
xDefs = [XDef(handle='time', unit='s', label='Time', mainInput=('time', 's'))]
yDefs = [
YDef(handle='speed', unit='m/s', label='Speed'),
YDef(handle='distance', unit='km', label='Distance')]
inputs = [Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=10, defaultRange=(0, 30))]
YDef(handle='distance', unit='km', label='Distance'),
YDef(handle='momentum', unit='Gkg⋅m/s', label='Momentum'),
YDef(handle='bumpSpeed', unit='m/s', label='Target speed', selectorLabel='Bump speed'),
YDef(handle='bumpDistance', unit='km', label='Target distance traveled', selectorLabel='Bump distance')]
inputs = [
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=10, defaultRange=(0, 30)),
# Default values in target fields correspond to a random carrier/fax
Input(handle='tgtMass', unit='Mkg', label='Target mass', iconID=76, defaultValue=1300, defaultRange=(100, 2500), conditions=[(None, ('bumpSpeed', 'm/s')), (None, ('bumpDistance', 'km'))], secondaryTooltip='Defined in millions of kilograms'),
Input(handle='tgtInertia', unit=None, label='Target inertia factor', iconID=1401, defaultValue=0.015, defaultRange=(0.03, 0.1), conditions=[(None, ('bumpDistance', 'km'))], secondaryTooltip='Inertia Modifier attribute value of the target ship')]
srcExtraCols = ('Speed', 'Agility')
# Calculation stuff
_normalizers = {('tgtMass', 'Mkg'): lambda v, src, tgt: None if v is None else v * 10 ** 6}
_getters = {
('time', 'speed'): Time2SpeedGetter,
('time', 'distance'): Time2DistanceGetter}
_denormalizers = {('distance', 'km'): lambda v, src, tgt: v / 1000}
('time', 'distance'): Time2DistanceGetter,
('time', 'momentum'): Time2MomentumGetter,
('time', 'bumpSpeed'): Time2BumpSpeedGetter,
('time', 'bumpDistance'): Time2BumpDistanceGetter}
_denormalizers = {
('distance', 'km'): lambda v, src, tgt: v / 1000,
('momentum', 'Gkg⋅m/s'): lambda v, src, tgt: v / 10 ** 9,
('bumpDistance', 'km'): lambda v, src, tgt: v / 1000}

View File

@@ -26,8 +26,8 @@ from .getter import Distance2RpsGetter, Distance2RepAmountGetter, Time2RpsGetter
class FitRemoteRepsGraph(FitGraph):
def __init__(self):
super().__init__()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._timeCache = TimeCache()
def _clearInternalCache(self, reason, extraData):

View File

@@ -27,6 +27,10 @@ from .getter import (
class FitShieldRegenGraph(FitGraph):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.isEffective = gui.mainFrame.MainFrame.getInstance().statsPane.nameViewMap['resistancesViewFull'].showEffective
# UI stuff
internalName = 'shieldRegenGraph'
name = 'Shield Regeneration'
@@ -73,7 +77,3 @@ class FitShieldRegenGraph(FitGraph):
('shieldAmount', '%'): lambda v, src, tgt: v * 100 / src.item.ship.getModifiedItemAttr('shieldCapacity'),
('shieldAmount', 'EHP'): lambda v, src, tgt: src.item.damagePattern.effectivify(src.item, v, 'shield'),
('shieldRegen', 'EHP/s'): lambda v, src, tgt: src.item.damagePattern.effectivify(src.item, v, 'shield')}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.isEffective = gui.mainFrame.MainFrame.getInstance().statsPane.nameViewMap['resistancesViewFull'].showEffective

View File

@@ -26,8 +26,8 @@ from .getter import AU_METERS, Distance2TimeGetter
class FitWarpTimeGraph(FitGraph):
def __init__(self):
super().__init__()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._subspeedCache = SubwarpSpeedCache()
def _clearInternalCache(self, reason, extraData):

View File

@@ -235,7 +235,7 @@ class GraphControlPanel(wx.Panel):
fieldTextBox = FloatBox(self, self._storedConsts.get((inputDef.handle, inputDef.unit), inputDef.defaultValue))
fieldTextBox.Bind(wx.EVT_TEXT, self.OnNonMainInputChanged)
fieldTextBox.SetToolTip(wx.ToolTip(tooltipText))
fieldSizer.Add(fieldTextBox, 0, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
fieldSizer.Add(fieldTextBox, 0, wx.EXPAND | wx.RIGHT, 5)
fieldIcon = None
if inputDef.iconID is not None:
icon = BitmapLoader.getBitmap(inputDef.iconID, 'icons')
@@ -295,12 +295,16 @@ class GraphControlPanel(wx.Panel):
self.ySubSelection.Clear()
for yDef in view.yDefs:
if yDef.hidden and not self.graphFrame.includeHidden:
continue
self.ySubSelection.Append(self.formatLabel(yDef, selector=True), yDef)
self.ySubSelection.Enable(len(view.yDefs) > 1)
self.ySubSelection.SetSelection(selectedY)
self.xSubSelection.Clear()
for xDef in view.xDefs:
if xDef.hidden and not self.graphFrame.includeHidden:
continue
self.xSubSelection.Append(self.formatLabel(xDef, selector=True), xDef)
self.xSubSelection.Enable(len(view.xDefs) > 1)
self.xSubSelection.SetSelection(selectedX)

View File

@@ -27,7 +27,7 @@ import gui.globalEvents as GE
import gui.mainFrame
from graphs.data.base import FitGraph
from graphs.events import RESIST_MODE_CHANGED
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from service.const import GraphCacheCleanupReason
from service.settings import GraphSettings
@@ -50,6 +50,7 @@ class GraphFrame(AuxiliaryFrame):
super().__init__(parent, title='Graphs', size=(520, 390), resizeable=True)
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.includeHidden = includeHidden
self.SetIcon(wx.Icon(BitmapLoader.getBitmap('graphs_small', 'gui')))
@@ -74,7 +75,7 @@ class GraphFrame(AuxiliaryFrame):
# Setup - graph selector
for view in FitGraph.views:
if view.hidden and not includeHidden:
if view.hidden and not self.includeHidden:
continue
self.graphSelection.Append(view.name, view())
self.graphSelection.SetSelection(0)

View File

@@ -22,12 +22,14 @@
import wx
class AuxiliaryFrame(wx.Frame):
class AuxiliaryMixin:
_instance = None
def __init__(self, parent, id=None, title=None, pos=None, size=None, style=None, name=None, resizeable=False):
baseStyle = wx.FRAME_NO_TASKBAR | wx.FRAME_FLOAT_ON_PARENT | wx.CAPTION | wx.CLOSE_BOX | wx.SYSTEM_MENU
baseStyle = wx.FRAME_NO_TASKBAR | wx.CAPTION | wx.CLOSE_BOX | wx.SYSTEM_MENU
if parent is not None:
baseStyle = baseStyle | wx.FRAME_FLOAT_ON_PARENT
if resizeable:
baseStyle = baseStyle | wx.RESIZE_BORDER | wx.MAXIMIZE_BOX
kwargs = {
@@ -53,14 +55,26 @@ class AuxiliaryFrame(wx.Frame):
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
@classmethod
def openOne(cls, parent, *args, **kwargs):
def openOne(cls, parent, *args, forceReopen=False, **kwargs):
"""If window is open and alive - raise it, open otherwise"""
if not cls._instance:
if not cls._instance or forceReopen:
if cls._instance:
cls._instance.Close()
frame = cls(parent, *args, **kwargs)
cls._instance = frame
frame.Show()
else:
cls._instance.Raise()
return cls._instance
def OnSuppressedAction(self, event):
return
class AuxiliaryFrame(AuxiliaryMixin, wx.Frame):
pass
class AuxiliaryDialog(AuxiliaryMixin, wx.Dialog):
pass

View File

@@ -24,6 +24,7 @@ import gui.display as d
import gui.fitCommands as cmd
import gui.globalEvents as GE
from gui.contextMenu import ContextMenu
from gui.builtinMarketBrowser.events import ITEM_SELECTED, ItemSelected
from gui.utils.staticHelpers import DragDropHelper
from service.fit import Fit
from service.market import Market
@@ -58,6 +59,7 @@ class CargoView(d.Display):
self.lastFitId = None
self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged)
self.mainFrame.Bind(ITEM_SELECTED, self.addItem)
self.Bind(wx.EVT_LEFT_DCLICK, self.onLeftDoubleClick)
self.Bind(wx.EVT_KEY_UP, self.kbEvent)
@@ -66,6 +68,31 @@ class CargoView(d.Display):
self.Bind(wx.EVT_CONTEXT_MENU, self.spawnMenu)
def addItem(self, event):
item = Market.getInstance().getItem(event.itemID, eager='group')
if item is None or not item.isCharge:
event.Skip()
return
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
if not fit:
event.Skip()
return
modifiers = wx.GetMouseState().GetModifiers()
amount = 1
if modifiers == wx.MOD_CONTROL:
amount = 10
elif modifiers == wx.MOD_ALT:
amount = 100
elif modifiers == wx.MOD_CONTROL | wx.MOD_ALT:
amount = 1000
self.mainFrame.command.Submit(cmd.GuiAddCargoCommand(
fitID=fitID, itemID=item.ID, amount=amount))
self.mainFrame.additionsPane.select('Cargo')
event.Skip()
def handleListDrag(self, x, y, data):
"""
Handles dragging of items from various pyfa displays which support it

View File

@@ -36,6 +36,11 @@ import gui.fitCommands as cmd
from gui.fitCommands.helpers import droneStackLimit
DRONE_ORDER = ('Light Scout Drones', 'Medium Scout Drones',
'Heavy Attack Drones', 'Sentry Drones', 'Combat Utility Drones',
'Electronic Warfare Drones', 'Logistic Drones', 'Mining Drones', 'Salvage Drones')
class DroneViewDrop(wx.DropTarget):
def __init__(self, dropFn, *args, **kwargs):
super(DroneViewDrop, self).__init__(*args, **kwargs)
@@ -186,17 +191,13 @@ class DroneView(Display):
self.mainFrame.command.Submit(cmd.GuiMergeLocalDroneStacksCommand(
fitID=fitID, srcPosition=srcPosition, dstPosition=dstPosition))
DRONE_ORDER = ('Light Scout Drones', 'Medium Scout Drones',
'Heavy Attack Drones', 'Sentry Drones', 'Combat Utility Drones',
'Electronic Warfare Drones', 'Logistic Drones', 'Mining Drones', 'Salvage Drones')
def droneKey(self, drone):
@staticmethod
def droneKey(drone):
sMkt = Market.getInstance()
groupName = sMkt.getMarketGroupByItem(drone.item).name
return (self.DRONE_ORDER.index(groupName),
drone.item.name)
return (DRONE_ORDER.index(groupName), drone.item.name)
def fitChanged(self, event):
event.Skip()

View File

@@ -34,6 +34,9 @@ from service.fit import Fit
from service.market import Market
FIGHTER_ORDER = ('Light Fighter', 'Heavy Fighter', 'Support Fighter')
class FighterViewDrop(wx.DropTarget):
def __init__(self, dropFn, *args, **kwargs):
super(FighterViewDrop, self).__init__(*args, **kwargs)
@@ -250,11 +253,10 @@ class FighterDisplay(d.Display):
def _merge(src, dst):
return
FIGHTER_ORDER = ('Light Fighter', 'Heavy Fighter', 'Support Fighter')
def fighterKey(self, fighter):
@staticmethod
def fighterKey(fighter):
groupName = Market.getInstance().getGroupByItem(fighter.item).name
orderPos = self.FIGHTER_ORDER.index(groupName)
orderPos = FIGHTER_ORDER.index(groupName)
# Sort support fighters by name, ignore their abilities
if groupName == 'Support Fighter':
abilityEffectIDs = ()

View File

@@ -316,7 +316,7 @@ class ImplantDisplay(d.Display):
implants.append(implant)
return implants
def addImplantSet(self, impSet):
def addImplants(self, implants):
self.mainFrame.command.Submit(cmd.GuiAddImplantSetCommand(
fitID=self.mainFrame.getActiveFit(),
itemIDs=[i.itemID for i in impSet.implants]))
itemIDs=[i.itemID for i in implants]))

View File

@@ -90,8 +90,6 @@ class ProjectedView(d.Display):
self.Bind(wx.EVT_LEFT_DCLICK, self.onLeftDoubleClick)
self.Bind(wx.EVT_KEY_UP, self.kbEvent)
self.droneView = gui.builtinAdditionPanes.droneView.DroneView
self.Bind(wx.EVT_CONTEXT_MENU, self.spawnMenu)
self.SetDropTarget(ProjectedViewDrop(self.handleListDrag))
@@ -119,12 +117,12 @@ class ProjectedView(d.Display):
fitID=fitID, itemID=fit.modules[int(data[1])].itemID))
elif data[0] == 'market':
itemID = int(data[1])
category = Market.getInstance().getItem(itemID, eager=('group.category')).category.name
if category == 'Module':
item = Market.getInstance().getItem(itemID)
if item.isModule:
self.mainFrame.command.Submit(cmd.GuiAddProjectedModuleCommand(fitID=fitID, itemID=itemID))
elif category == 'Drone':
elif item.isDrone:
self.mainFrame.command.Submit(cmd.GuiAddProjectedDroneCommand(fitID=fitID, itemID=itemID))
elif category == 'Fighter':
elif item.isFighter:
self.mainFrame.command.Submit(cmd.GuiAddProjectedFighterCommand(fitID=fitID, itemID=itemID))
def kbEvent(self, event):
@@ -162,7 +160,7 @@ class ProjectedView(d.Display):
if item.marketGroup is None:
item = item.metaGroup.parent
return (self.droneView.DRONE_ORDER.index(item.marketGroup.name),
return (gui.builtinAdditionPanes.droneView.DRONE_ORDER.index(item.marketGroup.name),
drone.item.name)
@staticmethod

View File

@@ -6,6 +6,7 @@ from gui.builtinContextMenus import fitAddCurrentlyOpen
from gui.builtinContextMenus import envEffectAdd
from gui.builtinContextMenus import commandFitAdd
from gui.builtinContextMenus.targetProfile import adder
from gui.builtinContextMenus import graphFitAmmoPicker
# Often-used item manipulations
from gui.builtinContextMenus import shipModeChange
from gui.builtinContextMenus import moduleAmmoChange

View File

@@ -1,4 +1,5 @@
import re
from collections import OrderedDict
from itertools import chain
# noinspection PyPackageRequirements
@@ -10,6 +11,28 @@ from gui.contextMenu import ContextMenuUnconditional
from service.market import Market
class Group:
def __init__(self):
self.groups = OrderedDict()
self.items = []
def sort(self):
self.groups = OrderedDict((k, self.groups[k]) for k in sorted(self.groups))
for group in self.groups.values():
group.sort()
self.items.sort(key=lambda e: e.shortName)
class Entry:
def __init__(self, itemID, name, shortName):
self.itemID = itemID
self.name = name
self.shortName = shortName
class AddEnvironmentEffect(ContextMenuUnconditional):
# CCP doesn't currently provide a mapping between the general Environment, and the specific environment effect
@@ -32,107 +55,78 @@ class AddEnvironmentEffect(ContextMenuUnconditional):
def getText(self, callingWindow, itmContext):
return "Add Environmental Effect"
def _addGroup(self, parentMenu, name):
id = ContextMenuUnconditional.nextID()
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleSelection, menuItem)
return menuItem
def _addEffect(self, parentMenu, typeID, name):
id = ContextMenuUnconditional.nextID()
self.idmap[id] = typeID
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleSelection, menuItem)
return menuItem
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
msw = True if "wxMSW" in wx.PlatformInfo else False
# Wormholes
self.idmap = {}
sub = wx.Menu()
data = self.getData()
msw = "wxMSW" in wx.PlatformInfo
wormhole_item = wx.MenuItem(sub, wx.ID_ANY, "Wormhole")
wormhole_menu = wx.Menu()
wormhole_item.SetSubMenu(wormhole_menu)
sub.Append(wormhole_item)
grouped_data, flat_data = self.getEffectBeacons()
self.buildMenu(grouped_data, flat_data, wormhole_menu, rootMenu, msw)
# Incursions
grouped_data, flat_data = self.getEffectBeacons(incursions=True)
self.buildMenu(grouped_data, flat_data, sub, rootMenu, msw)
# Abyssal Weather
abyssal_item = wx.MenuItem(sub, wx.ID_ANY, "Abyssal Weather")
abyssal_menu = wx.Menu()
abyssal_item.SetSubMenu(abyssal_menu)
sub.Append(abyssal_item)
grouped_data, flat_data = self.getAbyssalWeather()
self.buildMenu(grouped_data, flat_data, abyssal_menu, rootMenu, msw)
# Localized Weather
local_item = wx.MenuItem(sub, wx.ID_ANY, "Localized")
local_menu = wx.Menu()
local_item.SetSubMenu(local_menu)
sub.Append(local_item)
grouped_data, flat_data = self.getLocalizedEnvironments()
self.buildMenu(grouped_data, flat_data, local_menu, rootMenu, msw)
def makeMenu(data, parentMenu):
menu = wx.Menu()
for group_name in data.groups:
menuItem = self._addGroup(rootMenu if msw else parentMenu, group_name)
subMenu = makeMenu(data.groups[group_name], menu)
menuItem.SetSubMenu(subMenu)
menu.Append(menuItem)
for entry in data.items:
menuItem = self._addEffect(rootMenu if msw else parentMenu, entry.itemID, entry.shortName)
menu.Append(menuItem)
menu.Bind(wx.EVT_MENU, self.handleSelection)
return menu
sub = makeMenu(data, rootMenu)
return sub
def handleSelection(self, event):
# Skip events ids that aren't mapped
swObj, swName = self.idmap.get(event.Id, (False, False))
if not swObj and not swName:
swObj = self.idmap.get(event.Id, False)
if not swObj:
event.Skip()
return
fitID = self.mainFrame.getActiveFit()
self.mainFrame.command.Submit(cmd.GuiAddProjectedModuleCommand(fitID, swObj.ID))
self.mainFrame.command.Submit(cmd.GuiAddProjectedModuleCommand(fitID, swObj))
def buildMenu(self, grouped_data, flat_data, local_menu, rootMenu, msw):
def getData(self):
data = Group()
data.groups['Metaliminal Storm'] = self.getEffectBeacons(
'Electrical', 'Exotic', 'Gamma', 'Plasma',
extra_garbage=('Metaliminal', 'Storm', 'Matter', 'Ray', 'Firestorm'))
data.groups['Wormhole'] = self.getEffectBeacons(
'Black Hole', 'Cataclysmic Variable', 'Magnetar',
'Pulsar', 'Red Giant', 'Wolf Rayet')
data.groups['Abyssal Weather'] = self.getAbyssalWeather()
data.groups['Sansha Incursion'] = self.getEffectBeacons('Sansha Incursion')
data.groups['Triglavian Invasion'] = self.getEffectBeacons('Triglavian Invasion')
data.groups['Triglavian Invasion'].groups['Destructible Beacons'] = self.getDestructibleBeacons()
return data
def processFlat(data, root, sub):
for swData in sorted(data, key=lambda tpl: tpl[2]):
wxid = ContextMenuUnconditional.nextID()
swObj, swName, swClass = swData
self.idmap[wxid] = (swObj, swName)
subItem = wx.MenuItem(sub, wxid, swClass)
if msw:
root.Bind(wx.EVT_MENU, self.handleSelection, subItem)
else:
sub.Bind(wx.EVT_MENU, self.handleSelection, subItem)
sub.Append(subItem)
for swType in sorted(grouped_data):
subItem = wx.MenuItem(local_menu, wx.ID_ANY, swType)
grandSub = wx.Menu()
subItem.SetSubMenu(grandSub)
local_menu.Append(subItem)
processFlat(grouped_data[swType], rootMenu, grandSub)
processFlat(flat_data, rootMenu, local_menu)
def getEffectBeacons(self, incursions=False):
def getEffectBeacons(self, *groups, extra_garbage=()):
"""
Get dictionary with wormhole system-wide effects
Get dictionary with system-wide effects
"""
compacted = len(groups) <= 1
sMkt = Market.getInstance()
# todo: rework this
# Container for system-wide effects
grouped = {}
# Expressions for matching when detecting effects we're looking for
if incursions:
validgroups = ("Sansha Incursion",
"Triglavian Invasion")
else:
validgroups = ("Black Hole",
"Cataclysmic Variable",
"Magnetar",
"Pulsar",
"Red Giant",
"Wolf Rayet")
data = Group()
# Stuff we don't want to see in names
garbages = ("System Effects", "Effects")
garbages = ["System Effects", "Effects"]
garbages.extend(extra_garbage)
# Get group with all the system-wide beacons
grp = sMkt.getGroup("Effect Beacon")
@@ -140,7 +134,7 @@ class AddEnvironmentEffect(ContextMenuUnconditional):
# Cycle through them
for beacon in sMkt.getItemsByGroup(grp):
# Check if it belongs to any valid group
for group in validgroups:
for group in groups:
# Check beginning of the name only
if re.search(group, beacon.name):
# Get full beacon name
@@ -159,64 +153,65 @@ class AddEnvironmentEffect(ContextMenuUnconditional):
groupname = re.sub(garbage, "", groupname)
groupname = re.sub(" {2,}", " ", groupname).strip()
# Add stuff to dictionary
if groupname not in grouped:
grouped[groupname] = set()
grouped[groupname].add((beacon, beaconname, shortname))
if compacted:
container = data.items
else:
container = data.groups.setdefault(groupname, Group()).items
container.append(Entry(beacon.ID, beaconname, shortname))
# Break loop on 1st result
break
return grouped, ()
data.sort()
return data
def getAbyssalWeather(self):
sMkt = Market.getInstance()
data = Group()
environments = {x.ID: x for x in sMkt.getGroup("Abyssal Environment").items}
items = chain(sMkt.getGroup("MassiveEnvironments").items, sMkt.getGroup("Non-Interactable Object").items)
grouped = {}
flat = set()
items = chain(
sMkt.getGroup("MassiveEnvironments").items,
sMkt.getGroup("Non-Interactable Object").items)
for beacon in items:
if not beacon.isType('projected'):
continue
type = self.__class__.abyssal_mapping.get(beacon.name[0:-2], None)
type = environments.get(type, None)
if type is None:
continue
if type.name not in grouped:
grouped[type.name] = set()
subdata = data.groups.setdefault(type.name, Group())
display_name = "{} {}".format(type.name, beacon.name[-1:])
grouped[type.name].add((beacon, display_name, display_name))
subdata.items.append(Entry(beacon.ID, display_name, display_name))
data.sort()
# Localized abyssal hazards
items = sMkt.getGroup("Abyssal Hazards").items
if items:
subdata = data.groups.setdefault('Localized', Group())
for beacon in sMkt.getGroup("Abyssal Hazards").items:
if not beacon.isType('projected'):
continue
# Localized effects, currently, have a name like "(size) (type) Cloud"
# Until this inevitably changes, do a simple split
name_parts = beacon.name.split(" ")
key = name_parts[1].strip()
subsubdata = subdata.groups.setdefault(key, Group())
subsubdata.items.append(Entry(beacon.ID, beacon.name, beacon.name))
subdata.sort()
# PVP weather
flat.add((sMkt.getItem(49766), 'PvP Weather', 'PvP Weather'))
data.items.append(Entry(49766, 'PvP Weather', 'PvP Weather'))
return grouped, flat
return data
def getLocalizedEnvironments(self):
def getDestructibleBeacons(self):
data = Group()
sMkt = Market.getInstance()
grp = sMkt.getGroup("Abyssal Hazards")
grouped = dict()
for beacon in grp.items:
if not beacon.isType('projected'):
for item in sMkt.getItemsByGroup(sMkt.getGroup('Destructible Effect Beacon')):
if not item.isType('projected'):
continue
# Localized effects, currently, have a name like "(size) (type) Cloud"
# Until this inevitably changes, do a simple split
name_parts = beacon.name.split(" ")
key = name_parts[1].strip()
if key not in grouped:
grouped[key] = set()
grouped[key].add((beacon, beacon.name, beacon.name))
return grouped, ()
data.items.append(Entry(item.ID, item.name, item.name))
data.sort()
return data
AddEnvironmentEffect.register()

View File

@@ -0,0 +1,241 @@
# noinspection PyPackageRequirements
import wx
import gui.mainFrame
from gui.auxWindow import AuxiliaryDialog
from gui.contextMenu import ContextMenuSingle
from service.ammo import Ammo
from service.market import Market
class GraphFitAmmoPicker(ContextMenuSingle):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext, mainItem):
if srcContext != 'graphFitList':
return False
if mainItem is None or not mainItem.isFit:
return False
if callingWindow.graphFrame.getView().internalName != 'dmgStatsGraph':
return False
return True
def getText(self, callingWindow, itmContext, mainItem):
return 'Plot with Different Ammo...'
def activate(self, callingWindow, fullContext, mainItem, i):
AmmoPickerFrame.openOne(callingWindow, mainItem.item, forceReopen=True)
# GraphFitAmmoPicker.register()
class AmmoPickerFrame(AuxiliaryDialog):
def __init__(self, parent, fit):
super().__init__(parent, title='Choose Different Ammo', style=wx.DEFAULT_DIALOG_STYLE, resizeable=True)
padding = 5
mainSizer = wx.BoxSizer(wx.VERTICAL)
contents = AmmoPickerContents(self, fit)
mainSizer.Add(contents, 1, wx.EXPAND | wx.ALL, padding)
buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
if buttonSizer:
mainSizer.Add(buttonSizer, 0, wx.EXPAND | wx.ALL, padding)
self.SetSizer(mainSizer)
self.Layout()
contW, contH = contents.GetVirtualSize()
bestW = contW + padding * 2
bestH = contH + padding * 2
if buttonSizer:
# Yeah right... whatever
buttW, buttH = buttonSizer.GetSize()
bestW = max(bestW, buttW + padding * 2)
bestH += buttH + padding * 2
bestW = min(1000, bestW)
bestH = min(700, bestH)
self.SetSize(bestW, bestH)
self.SetMinSize(wx.Size(int(bestW * 0.7), int(bestH * 0.7)))
self.CenterOnParent()
self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent)
def kbEvent(self, event):
if event.GetKeyCode() == wx.WXK_ESCAPE and event.GetModifiers() == wx.MOD_NONE:
self.Close()
return
event.Skip()
class AmmoPickerContents(wx.ScrolledCanvas):
indent = 15
def __init__(self, parent, fit):
wx.ScrolledCanvas.__init__(self, parent)
self.SetScrollRate(0, 15)
mods = self.getMods(fit)
drones = self.getDrones(fit)
fighters = self.getFighters(fit)
self.rbLabelMap = {}
self.rbCheckboxMap = {}
mainSizer = wx.BoxSizer(wx.VERTICAL)
moduleSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(moduleSizer, 0, wx.ALL, 0)
self.droneSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(self.droneSizer, 0, wx.ALL, 0)
fighterSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(fighterSizer, 0, wx.ALL, 0)
firstRadio = True
for modInfo, modAmmo in mods:
text = '\n'.join('{}x {}'.format(amount, item.name) for item, amount in modInfo)
modRb = self.addRadioButton(moduleSizer, text, firstRadio)
firstRadio = False
# Get actual module, as ammo getters need it
mod = next((m for m in fit.modules if m.itemID == next(iter(modInfo))[0].ID), None)
_, ammoTree = Ammo.getInstance().getModuleStructuredAmmo(mod)
if len(ammoTree) == 1:
for ammoCatName, ammos in ammoTree.items():
for ammo in ammos:
self.addCheckbox(moduleSizer, ammo.name, modRb, indentLvl=1)
else:
for ammoCatName, ammos in ammoTree.items():
if len(ammos) == 1:
ammo = next(iter(ammos))
self.addCheckbox(moduleSizer, ammo.name, modRb, indentLvl=1)
else:
self.addLabel(moduleSizer, '{}:'.format(ammoCatName), modRb, indentLvl=1)
for ammo in ammos:
self.addCheckbox(moduleSizer, ammo.name, modRb, indentLvl=2)
if drones:
droneRb = self.addRadioButton(self.droneSizer, 'Drones', firstRadio)
from gui.builtinAdditionPanes.droneView import DroneView
for drone in sorted(drones, key=DroneView.droneKey):
self.addCheckbox(self.droneSizer, '{}x {}'.format(drone.amount, drone.item.name), droneRb, indentLvl=1)
addBtn = wx.Button(self, wx.ID_ANY, '+', style=wx.BU_EXACTFIT)
addBtn.Bind(wx.EVT_BUTTON, self.OnDroneGroupAdd)
mainSizer.Add(addBtn, 0, wx.LEFT, self.indent)
if fighters:
fighterRb = self.addRadioButton(fighterSizer, 'Fighters', firstRadio)
from gui.builtinAdditionPanes.fighterView import FighterDisplay
for fighter in sorted(fighters, key=FighterDisplay.fighterKey):
self.addCheckbox(fighterSizer, '{}x {}'.format(fighter.amount, fighter.item.name), fighterRb, indentLvl=1)
self.SetSizer(mainSizer)
self.refreshStatus()
def addRadioButton(self, sizer, text, firstRadio=False):
if firstRadio:
rb = wx.RadioButton(self, wx.ID_ANY, text, style=wx.RB_GROUP)
rb.SetValue(True)
else:
rb = wx.RadioButton(self, wx.ID_ANY, text)
rb.SetValue(False)
rb.Bind(wx.EVT_RADIOBUTTON, self.rbSelected)
sizer.Add(rb, 0, wx.EXPAND | wx.ALL, 0)
return rb
def addCheckbox(self, sizer, text, currentRb, indentLvl=0):
cb = wx.CheckBox(self, -1, text)
sizer.Add(cb, 0, wx.EXPAND | wx.LEFT, self.indent * indentLvl)
if currentRb is not None:
self.rbCheckboxMap.setdefault(currentRb, []).append(cb)
def addLabel(self, sizer, text, currentRb, indentLvl=0):
text = text[0].capitalize() + text[1:]
label = wx.StaticText(self, wx.ID_ANY, text)
sizer.Add(label, 0, wx.EXPAND | wx.LEFT, self.indent * indentLvl)
if currentRb is not None:
self.rbLabelMap.setdefault(currentRb, []).append(label)
def getMods(self, fit):
sMkt = Market.getInstance()
sAmmo = Ammo.getInstance()
loadableChargesCache = {}
# Modules, format: {frozenset(ammo): {item: count}}
modsPrelim = {}
if fit is not None:
for mod in fit.modules:
if not mod.canDealDamage():
continue
typeID = mod.item.ID
if typeID not in loadableChargesCache:
loadableChargesCache[typeID] = sAmmo.getModuleFlatAmmo(mod)
charges = loadableChargesCache[typeID]
# We're not interested in modules which contain no charges
if charges:
data = modsPrelim.setdefault(frozenset(charges), {})
if mod.item not in data:
data[mod.item] = 0
data[mod.item] += 1
# Format: [([(item, count), ...], frozenset(ammo)), ...]
modsFinal = []
for charges, itemCounts in modsPrelim.items():
modsFinal.append((
# Sort items within group
sorted(itemCounts.items(), key=lambda i: sMkt.itemSort(i[0], reverseMktGrp=True), reverse=True),
charges))
# Sort item groups
modsFinal.sort(key=lambda i: sMkt.itemSort(i[0][0][0], reverseMktGrp=True), reverse=True)
return modsFinal
def getDrones(self, fit):
drones = []
if fit is not None:
for drone in fit.drones:
if drone.item is None:
continue
# Drones are our "ammo", so we want to pick even those which are inactive
if drone.canDealDamage(ignoreState=True):
drones.append(drone)
continue
if {'remoteWebifierEntity', 'remoteTargetPaintEntity'}.intersection(drone.item.effects):
drones.append(drone)
continue
return drones
def getFighters(self, fit):
fighters = []
if fit is not None:
for fighter in fit.fighters:
if fighter.item is None:
continue
# Fighters are our "ammo" as well
if fighter.canDealDamage(ignoreState=True):
fighters.append(fighter)
continue
for ability in fighter.abilities:
if not ability.active:
continue
if ability.effect.name == 'fighterAbilityStasisWebifier':
fighters.append(fighter)
break
return fighters
def OnDroneGroupAdd(self, event):
event.Skip()
sizer = wx.BoxSizer(wx.HORIZONTAL)
label = wx.StaticText()
self.droneSizer.Add(sizer, 0, wx.EXPAND | wx.LEFT, self.indent)
def refreshStatus(self):
for map in (self.rbLabelMap, self.rbCheckboxMap):
for rb, items in map.items():
for item in items:
item.Enable(rb.GetValue())
def rbSelected(self, event):
event.Skip()
self.refreshStatus()

View File

@@ -2,17 +2,19 @@
import wx
from gui.contextMenu import ContextMenuUnconditional
from service.implantSet import ImplantSets as s_ImplantSets
from service.market import Market
from service.implantSet import ImplantSets as UserImplantSets
from service.precalcImplantSet import PrecalcedImplantSets
class ImplantSetApply(ContextMenuUnconditional):
def display(self, callingWindow, srcContext):
sIS = s_ImplantSets.getInstance()
implantSets = sIS.getImplantSetList()
self.userImplantSets = UserImplantSets.getInstance().getImplantSetList()
self.structedImplantSets = PrecalcedImplantSets.getStructuredSets()
if len(implantSets) == 0:
if len(self.userImplantSets) == 0 and len(self.structedImplantSets) == 0:
return False
return srcContext in ("implantItemMisc", "implantEditor")
@@ -20,34 +22,87 @@ class ImplantSetApply(ContextMenuUnconditional):
def getText(self, callingWindow, context):
return "Apply Implant Set"
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
m = wx.Menu()
bindmenu = rootMenu if "wxMSW" in wx.PlatformInfo else m
def _addSeparator(self, m, text):
id_ = ContextMenuUnconditional.nextID()
m.Append(id_, '%s' % text)
m.Enable(id_, False)
sIS = s_ImplantSets.getInstance()
implantSets = sIS.getImplantSetList()
def _addSet(self, parentMenu, profile, name):
id = ContextMenuUnconditional.nextID()
self.eventSetMap[id] = profile
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleSelection, menuItem)
return menuItem
def _addCategory(self, parentMenu, name):
id = ContextMenuUnconditional.nextID()
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleSelection, menuItem)
return menuItem
def _gradeSorter(self, item):
order = ['low-grade', 'mid-grade', 'high-grade']
try:
pos = order.index(item.lower())
except IndexError:
pos = len(order)
return pos, item
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
msw = "wxMSW" in wx.PlatformInfo
menu_lvl1 = wx.Menu()
self.context = context
self.callingWindow = callingWindow
self.idmap = {}
self.eventSetMap = {}
for set in sorted(implantSets, key=lambda i: i.name):
id = ContextMenuUnconditional.nextID()
mitem = wx.MenuItem(rootMenu, id, set.name)
bindmenu.Bind(wx.EVT_MENU, self.handleSelection, mitem)
self.idmap[id] = set
m.Append(mitem)
# Auto-generated sets
for setName in sorted(self.structedImplantSets):
setData = self.structedImplantSets[setName]
if len(setData) == 1:
for implantIDs in setData.values():
menuitem_lvl1 = self._addSet(rootMenu, implantIDs, setName)
menu_lvl1.Append(menuitem_lvl1)
else:
menuitem_lvl1 = self._addCategory(rootMenu, setName)
menu_lvl2 = wx.Menu()
for gradeName in sorted(setData, key=self._gradeSorter):
implantIDs = setData[gradeName]
menuitem_lvl2 = self._addSet(rootMenu if msw else menu_lvl1, implantIDs, gradeName)
menu_lvl2.Append(menuitem_lvl2)
menu_lvl2.Bind(wx.EVT_MENU, self.handleSelection)
menuitem_lvl1.SetSubMenu(menu_lvl2)
menu_lvl1.Append(menuitem_lvl1)
return m
# Separator
if self.userImplantSets and self.structedImplantSets:
menu_lvl1.AppendSeparator()
# Saved sets
if self.userImplantSets:
menuitem_lvl1 = self._addCategory(rootMenu, 'Saved Sets')
menu_lvl2 = wx.Menu()
for implantSet in sorted(self.userImplantSets, key=lambda i: i.name):
menuitem_lvl2 = self._addSet(rootMenu if msw else menu_lvl1, implantSet, implantSet.name)
menu_lvl2.Append(menuitem_lvl2)
menu_lvl2.Bind(wx.EVT_MENU, self.handleSelection)
menuitem_lvl1.SetSubMenu(menu_lvl2)
menu_lvl1.Append(menuitem_lvl1)
menu_lvl1.Bind(wx.EVT_MENU, self.handleSelection)
return menu_lvl1
def handleSelection(self, event):
impSet = self.idmap.get(event.Id, None)
impSet = self.eventSetMap.get(event.Id, None)
if impSet is None:
event.Skip()
return
self.callingWindow.addImplantSet(impSet)
if isinstance(impSet, str):
implants = PrecalcedImplantSets.stringToImplants(impSet)
else:
implants = impSet.implants
self.callingWindow.addImplants(implants)
ImplantSetApply.register()

View File

@@ -3,33 +3,28 @@ import wx
import gui.fitCommands as cmd
import gui.mainFrame
from eos.const import FittingHardpoint
from eos.saveddata.module import Module
from gui.bitmap_loader import BitmapLoader
from gui.contextMenu import ContextMenuCombined
from gui.fitCommands.helpers import getSimilarModPositions
from service.ammo import Ammo
from service.fit import Fit
from service.market import Market
class ChangeModuleAmmo(ContextMenuCombined):
DAMAGE_TYPES = ("em", "explosive", "kinetic", "thermal")
MISSILE_ORDER = ("em", "thermal", "kinetic", "explosive", "mixed")
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
# Format: {type ID: set(loadable, charges)}
self.loadableCharges = {}
self.loadableChargesCache = {}
def display(self, callingWindow, srcContext, mainItem, selection):
if srcContext not in ("fittingModule", "projectedModule"):
if srcContext not in ('fittingModule', 'projectedModule'):
return False
if self.mainFrame.getActiveFit() is None:
return False
self.mainCharges = self.getChargesForMod(mainItem)
self.mainCharges = self._getAmmo(mainItem)
if not self.mainCharges:
return False
@@ -39,186 +34,81 @@ class ChangeModuleAmmo(ContextMenuCombined):
return True
def getText(self, callingWindow, itmContext, mainItem, selection):
return "Charge"
return 'Charge'
def getChargesForMod(self, mod):
sMkt = Market.getInstance()
if mod is None or mod.isEmpty:
def _getAmmo(self, mod):
if mod.itemID is None:
return set()
typeID = mod.item.ID
if typeID in self.loadableCharges:
return self.loadableCharges[typeID]
chargeSet = self.loadableCharges.setdefault(typeID, set())
# Do not try to grab it for modes which can also be passed as part of selection
if isinstance(mod, Module):
for charge in mod.getValidCharges():
if sMkt.getPublicityByItem(charge):
chargeSet.add(charge)
return chargeSet
if mod.itemID not in self.loadableChargesCache:
self.loadableChargesCache[mod.itemID] = Ammo.getInstance().getModuleFlatAmmo(mod)
return self.loadableChargesCache[mod.itemID]
def turretSorter(self, charge):
damage = 0
range_ = (self.module.item.getAttribute("maxRange")) * \
(charge.getAttribute("weaponRangeMultiplier") or 1)
falloff = (self.module.item.getAttribute("falloff") or 0) * \
(charge.getAttribute("fallofMultiplier") or 1)
for type_ in self.DAMAGE_TYPES:
d = charge.getAttribute("%sDamage" % type_)
if d > 0:
damage += d
# Take optimal and falloff as range factor
rangeFactor = range_ + falloff
return - rangeFactor, charge.name.rsplit()[-2:], damage, charge.name
def missileSorter(self, charge):
# Get charge damage type and total damage
chargeDamageType, totalDamage = self.damageInfo(charge)
# Find its position in sort list
position = self.MISSILE_ORDER.index(chargeDamageType)
return position, totalDamage, charge.name
def damageInfo(self, charge):
# Set up data storage for missile damage stuff
damageMap = {}
totalDamage = 0
# Fill them with the data about charge
for damageType in self.DAMAGE_TYPES:
currentDamage = charge.getAttribute("{0}Damage".format(damageType)) or 0
damageMap[damageType] = currentDamage
totalDamage += currentDamage
# Detect type of ammo
chargeDamageType = None
for damageType in damageMap:
# If all damage belongs to certain type purely, set appropriate
# ammoType
if damageMap[damageType] == totalDamage:
chargeDamageType = damageType
break
# Else consider ammo as mixed damage
if chargeDamageType is None:
chargeDamageType = "mixed"
return chargeDamageType, totalDamage
@staticmethod
def numericConverter(string):
return int(string) if string.isdigit() else string
def nameSorter(self, charge):
parts = charge.name.split(" ")
return list(map(self.numericConverter, parts))
def addCharge(self, menu, charge):
def _addCharge(self, menu, charge):
id_ = ContextMenuCombined.nextID()
name = charge.name if charge is not None else "Empty"
self.chargeIds[id_] = charge
name = charge.name if charge is not None else 'Empty'
self.chargeEventMap[id_] = charge
item = wx.MenuItem(menu, id_, name)
menu.Bind(wx.EVT_MENU, self.handleAmmoSwitch, item)
item.charge = charge
if charge is not None and charge.iconID is not None:
bitmap = BitmapLoader.getBitmap(charge.iconID, "icons")
bitmap = BitmapLoader.getBitmap(charge.iconID, 'icons')
if bitmap is not None:
item.SetBitmap(bitmap)
return item
@staticmethod
def addSeperator(m, text):
def _addSeparator(m, text):
id_ = ContextMenuCombined.nextID()
m.Append(id_, '%s' % text)
m.Enable(id_, False)
def getSubMenu(self, callingWindow, context, mainItem, selection, rootMenu, i, pitem):
msw = True if "wxMSW" in wx.PlatformInfo else False
m = wx.Menu()
self.chargeIds = {}
hardpoint = self.module.hardpoint
moduleName = self.module.item.name
# Make sure we do not consider mining turrets as combat turrets
if hardpoint == FittingHardpoint.TURRET and self.module.getModifiedItemAttr("miningAmount", None) is None:
self.addSeperator(m, "Long Range")
items = []
range_ = None
nameBase = None
sub = None
chargesSorted = sorted(self.mainCharges, key=self.turretSorter)
for charge in chargesSorted:
if "civilian" in charge.name.lower():
continue
currBase = charge.name.rsplit()[-2:]
currRange = charge.getAttribute("weaponRangeMultiplier")
if nameBase is None or range_ != currRange or nameBase != currBase:
if sub is not None:
self.addSeperator(sub, "More Damage")
sub = None
base = charge
nameBase = currBase
range_ = currRange
item = self.addCharge(rootMenu if msw else m, charge)
items.append(item)
msw = True if 'wxMSW' in wx.PlatformInfo else False
menu = wx.Menu()
self.chargeEventMap = {}
modType, chargeDict = Ammo.getInstance().getModuleStructuredAmmo(self.module, ammo=self.mainCharges)
if modType == 'ddTurret':
self._addSeparator(menu, 'Long Range')
menuItems = []
for charges in chargeDict.values():
if len(charges) == 1:
menuItems.append(self._addCharge(rootMenu if msw else menu, charges[0]))
else:
if sub is None and item and base:
sub = wx.Menu()
sub.Bind(wx.EVT_MENU, self.handleAmmoSwitch)
self.addSeperator(sub, "Less Damage")
item.SetSubMenu(sub)
sub.Append(self.addCharge(rootMenu if msw else sub, base))
sub.Append(self.addCharge(rootMenu if msw else sub, charge))
if sub is not None:
self.addSeperator(sub, "More Damage")
for item in items:
m.Append(item)
self.addSeperator(m, "Short Range")
elif hardpoint == FittingHardpoint.MISSILE and moduleName != 'Festival Launcher':
type_ = None
sub = None
defender = None
chargesSorted = sorted(self.mainCharges, key=self.missileSorter)
for charge in chargesSorted:
currType = self.damageInfo(charge)[0]
if currType != type_ or type_ is None:
if sub is not None:
self.addSeperator(sub, "More Damage")
type_ = currType
item = wx.MenuItem(m, wx.ID_ANY, type_.capitalize())
bitmap = BitmapLoader.getBitmap("%s_small" % type_, "gui")
if bitmap is not None:
item.SetBitmap(bitmap)
sub = wx.Menu()
sub.Bind(wx.EVT_MENU, self.handleAmmoSwitch)
self.addSeperator(sub, "Less Damage")
item.SetSubMenu(sub)
m.Append(item)
if charge.name not in ("Light Defender Missile I", "Heavy Defender Missile I"):
sub.Append(self.addCharge(rootMenu if msw else sub, charge))
else:
defender = charge
if defender is not None:
m.Append(self.addCharge(rootMenu if msw else m, defender))
if sub is not None:
self.addSeperator(sub, "More Damage")
else:
chargesSorted = sorted(self.mainCharges, key=self.nameSorter)
for charge in chargesSorted:
m.Append(self.addCharge(rootMenu if msw else m, charge))
m.Append(self.addCharge(rootMenu if msw else m, None))
return m
baseCharge = charges[0]
menuItem = self._addCharge(rootMenu if msw else menu, baseCharge)
menuItems.append(menuItem)
subMenu = wx.Menu()
subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch)
menuItem.SetSubMenu(subMenu)
self._addSeparator(subMenu, 'Less Damage')
for charge in charges:
subMenu.Append(self._addCharge(rootMenu if msw else subMenu, charge))
self._addSeparator(subMenu, 'More Damage')
for menuItem in menuItems:
menu.Append(menuItem)
self._addSeparator(menu, 'Short Range')
elif modType == 'ddMissile':
menuItems = []
for chargeCatName, charges in chargeDict.items():
menuItem = wx.MenuItem(menu, wx.ID_ANY, chargeCatName.capitalize())
menuItems.append(menuItem)
subMenu = wx.Menu()
subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch)
menuItem.SetSubMenu(subMenu)
self._addSeparator(subMenu, 'Less Damage')
for charge in charges:
subMenu.Append(self._addCharge(rootMenu if msw else subMenu, charge))
self._addSeparator(subMenu, 'More Damage')
for menuItem in menuItems:
menu.Append(menuItem)
elif modType == 'general':
for charge in chargeDict['general']:
menu.Append(self._addCharge(rootMenu if msw else menu, charge))
menu.Append(self._addCharge(rootMenu if msw else menu, None))
return menu
def handleAmmoSwitch(self, event):
charge = self.chargeIds.get(event.Id, False)
charge = self.chargeEventMap.get(event.Id, False)
if charge is False:
event.Skip()
return
@@ -254,7 +144,7 @@ class ChangeModuleAmmo(ContextMenuCombined):
positions = []
for position, mod in enumerate(modContainer):
if mod in self.selection:
modCharges = self.getChargesForMod(mod)
modCharges = self._getAmmo(mod)
if modCharges.issubset(self.mainCharges):
positions.append(position)
self.mainFrame.command.Submit(command(

View File

@@ -26,7 +26,7 @@ class TargetProfileAdder(ContextMenuUnconditional):
return 'Add Target Profile'
def handleProfileAdd(self, event):
profile = self.profileEventMap.get(event.Id, False)
profile = self.eventProfileMap.get(event.Id, False)
if profile is False:
event.Skip()
return
@@ -34,7 +34,7 @@ class TargetProfileAdder(ContextMenuUnconditional):
def _addProfile(self, parentMenu, profile, name):
id = ContextMenuUnconditional.nextID()
self.profileEventMap[id] = profile
self.eventProfileMap[id] = profile
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleProfileAdd, menuItem)
return menuItem
@@ -51,7 +51,7 @@ class TargetProfileAdder(ContextMenuUnconditional):
profiles = list(chain(sTR.getBuiltinTargetProfileList(), sTR.getUserTargetProfileList()))
profiles.sort(key=lambda p: smartSort(p.fullName))
self.profileEventMap = {}
self.eventProfileMap = {}
items = (OrderedDict(), OrderedDict())
for profile in profiles:
container = items

View File

@@ -126,10 +126,13 @@ class AttributeSlider(wx.Panel):
def SetValue(self, value, post_event=True, affect_modified_flag=True):
self.ctrl.SetValue(value)
invert_factor = -1 if self.inverse else 1
if value >= self.base_value:
slider_percentage = (value - self.base_value) / (self.UserMaxValue - self.base_value) * 100 * invert_factor
else:
slider_percentage = (value - self.base_value) / (self.base_value - self.UserMinValue) * 100 * invert_factor
try:
if value >= self.base_value:
slider_percentage = (value - self.base_value) / (self.UserMaxValue - self.base_value) * 100 * invert_factor
else:
slider_percentage = (value - self.base_value) / (self.base_value - self.UserMinValue) * 100 * invert_factor
except ZeroDivisionError:
slider_percentage = 0
self.slider.SetValue(slider_percentage)
if post_event:
wx.PostEvent(self, ValueChanged(self, None, value, None, slider_percentage, affect_modified_flag=affect_modified_flag))

View File

@@ -35,7 +35,7 @@ class ItemDescription(wx.Panel):
self.Layout()
self.description.Bind(wx.EVT_CONTEXT_MENU, self.onPopupMenu)
self.description.Bind(wx.EVT_KEY_DOWN, self.onKeyDown)
self.description.Bind(wx.EVT_KEY_UP, self.onKeyUp)
self.popupMenu = wx.Menu()
copyItem = wx.MenuItem(self.popupMenu, 1, 'Copy')
@@ -50,7 +50,7 @@ class ItemDescription(wx.Panel):
if selectedMenuItem == 1: # Copy was chosen
self.copySelectionToClipboard()
def onKeyDown(self, event):
def onKeyUp(self, event):
keyCode = event.GetKeyCode()
# Ctrl + C
if keyCode == 67 and event.ControlDown():

View File

@@ -93,6 +93,8 @@ class ItemMutatorList(wx.ScrolledWindow):
first = True
for m in sorted(mod.mutators.values(), key=lambda x: x.attribute.displayName):
if m.baseValue == 0:
continue
if not first:
sizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, wx.ALL | wx.EXPAND, 5)
first = False

View File

@@ -14,7 +14,7 @@ class ItemTraits(wx.Panel):
self.traits.SetPage(item.traits.traitText)
self.traits.Bind(wx.EVT_CONTEXT_MENU, self.onPopupMenu)
self.traits.Bind(wx.EVT_KEY_DOWN, self.onKeyDown)
self.traits.Bind(wx.EVT_KEY_UP, self.onKeyUp)
mainSizer.Add(self.traits, 1, wx.ALL | wx.EXPAND, 0)
self.Layout()
@@ -32,7 +32,7 @@ class ItemTraits(wx.Panel):
if selectedMenuItem == 1: # Copy was chosen
self.copySelectionToClipboard()
def onKeyDown(self, event):
def onKeyUp(self, event):
keyCode = event.GetKeyCode()
# Ctrl + C
if keyCode == 67 and event.ControlDown():

View File

@@ -1,15 +1,16 @@
import wx
from logbook import Logger
from eos.saveddata.module import Module
import gui.builtinMarketBrowser.pfSearchBox as SBox
from config import slotColourMap
from eos.saveddata.module import Module
from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES
from gui.contextMenu import ContextMenu
from gui.display import Display
from gui.utils.staticHelpers import DragDropHelper
from service.attribute import Attribute
from service.fit import Fit
from config import slotColourMap
from service.market import Market
pyfalog = Logger(__name__)
@@ -170,8 +171,8 @@ class ItemView(Display):
def scheduleSearch(self, event=None):
self.searchTimer.Stop() # Cancel any pending timers
search = self.marketBrowser.search.GetLineText(0)
# Make sure we do not count wildcard as search symbol
realsearch = search.replace("*", "")
# Make sure we do not count wildcards as search symbol
realsearch = search.replace('*', '').replace('?', '')
# Re-select market group if search query has zero length
if len(realsearch) == 0:
self.selectionMade('search')
@@ -193,30 +194,15 @@ class ItemView(Display):
self.setToggles()
self.filterItemStore()
def populateSearch(self, items):
def populateSearch(self, itemIDs):
# If we're no longer searching, dump the results
if self.marketBrowser.mode != 'search':
return
items = Market.getItems(itemIDs)
self.updateItemStore(items)
self.setToggles()
self.filterItemStore()
def itemSort(self, item):
sMkt = self.sMkt
catname = sMkt.getCategoryByItem(item).name
try:
mktgrpid = sMkt.getMarketGroupByItem(item).ID
except AttributeError:
mktgrpid = -1
pyfalog.warning("unable to find market group for {}".format(item.name))
parentname = sMkt.getParentItemByItem(item).name
# Get position of market group
metagrpid = sMkt.getMetaGroupIdByItem(item)
metatab = sMkt.META_MAP_REVERSE_INDICES.get(metagrpid)
metalvl = item.metaLevel or 0
return catname, mktgrpid, parentname, metatab, metalvl, item.name
def contextMenu(self, event):
clickedPos = self.getRowByAbs(event.Position)
self.ensureSelection(clickedPos)
@@ -239,7 +225,7 @@ class ItemView(Display):
self.unselectAll()
# Perform sorting, using item's meta levels besides other stuff
if self.marketBrowser.mode != 'recent':
items.sort(key=self.itemSort)
items.sort(key=self.sMkt.itemSort)
# Mark current item list as active
self.active = items
# Show them
@@ -249,12 +235,10 @@ class ItemView(Display):
if len(items) > 1:
# Re-sort stuff
if self.marketBrowser.mode != 'recent':
items.sort(key=self.itemSort)
items.sort(key=self.sMkt.itemSort)
for i, item in enumerate(items[:9]):
# set shortcut info for first 9 modules
item.marketShortcut = i + 1
Display.refresh(self, items)
def columnBackground(self, colItem, item):

View File

@@ -60,7 +60,8 @@ class MarketTree(wx.TreeCtrl):
# If market should have items but it doesn't, do not show it
if sMkt.marketGroupValidityCheck(childMktGrp) is False:
continue
iconId = self.addImage(sMkt.getIconByMarketGroup(childMktGrp))
icon = sMkt.getIconByMarketGroup(childMktGrp)
iconId = -1 if icon is None else self.addImage(icon)
try:
childId = self.AppendItem(root, childMktGrp.name, iconId, data=childMktGrp.ID)
except (KeyboardInterrupt, SystemExit):

View File

@@ -86,7 +86,7 @@ class PFSearchBox(wx.Window):
def OnKeyPress(self, event):
if event.RawControlDown() and event.GetKeyCode() == wx.WXK_BACK:
HandleCtrlBackspace(self.EditBox)
HandleCtrlBackspace(self.EditBox)
else:
event.Skip()

View File

@@ -35,31 +35,31 @@ class PFGeneralPref(PreferenceView):
# Database path
self.stSetUserPath = wx.StaticText(panel, wx.ID_ANY, "pyfa User Path:", wx.DefaultPosition, wx.DefaultSize, 0)
self.stSetUserPath.Wrap(-1)
mainSizer.Add(self.stSetUserPath, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
mainSizer.Add(self.stSetUserPath, 0, wx.ALL, 5)
self.inputUserPath = wx.TextCtrl(panel, wx.ID_ANY, config.savePath, wx.DefaultPosition, wx.DefaultSize, 0)
self.inputUserPath.SetEditable(False)
self.inputUserPath.SetBackgroundColour((200, 200, 200))
mainSizer.Add(self.inputUserPath, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5)
mainSizer.Add(self.inputUserPath, 0, wx.ALL | wx.EXPAND, 5)
# Save DB
self.stFitDB = wx.StaticText(panel, wx.ID_ANY, "Fitting Database:", wx.DefaultPosition, wx.DefaultSize, 0)
self.stFitDB.Wrap(-1)
mainSizer.Add(self.stFitDB, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
mainSizer.Add(self.stFitDB, 0, wx.ALL, 5)
self.inputFitDB = wx.TextCtrl(panel, wx.ID_ANY, config.saveDB, wx.DefaultPosition, wx.DefaultSize, 0)
self.inputFitDB.SetEditable(False)
self.inputFitDB.SetBackgroundColour((200, 200, 200))
mainSizer.Add(self.inputFitDB, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5)
mainSizer.Add(self.inputFitDB, 0, wx.ALL | wx.EXPAND, 5)
# Game Data DB
self.stGameDB = wx.StaticText(panel, wx.ID_ANY, "Game Database:", wx.DefaultPosition, wx.DefaultSize, 0)
self.stGameDB.Wrap(-1)
mainSizer.Add(self.stGameDB, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
mainSizer.Add(self.stGameDB, 0, wx.ALL, 5)
self.inputGameDB = wx.TextCtrl(panel, wx.ID_ANY, config.gameDB, wx.DefaultPosition, wx.DefaultSize, 0)
self.inputGameDB.SetEditable(False)
self.inputGameDB.SetBackgroundColour((200, 200, 200))
mainSizer.Add(self.inputGameDB, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5)
mainSizer.Add(self.inputGameDB, 0, wx.ALL | wx.EXPAND, 5)
self.cbsaveInRoot.SetValue(config.saveInRoot)
self.cbsaveInRoot.Bind(wx.EVT_CHECKBOX, self.onCBsaveInRoot)

View File

@@ -25,7 +25,7 @@ class PFGeneralPref(PreferenceView):
self.stTitle.Wrap(-1)
self.stTitle.SetFont(wx.Font(12, 70, 90, 90, False, wx.EmptyString))
mainSizer.Add(self.stTitle, 0, wx.EXPAND | wx.ALL, 5)
helpCursor = wx.Cursor(wx.CURSOR_QUESTION_ARROW)
self.m_staticline1 = wx.StaticLine(panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL)

View File

@@ -50,7 +50,7 @@ class PFHTMLExportPref(PreferenceView):
self.fileSelectButton = wx.Button(panel, -1, "Set export destination", pos=(0, 0))
self.fileSelectButton.Bind(wx.EVT_BUTTON, self.selectHTMLExportFilePath)
mainSizer.Add(self.fileSelectButton, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
mainSizer.Add(self.fileSelectButton, 0, wx.ALL, 5)
self.stDesc4 = wx.StaticText(panel, wx.ID_ANY, self.desc4, wx.DefaultPosition, wx.DefaultSize, 0)
self.stDesc4.Wrap(dlgWidth - 50)

View File

@@ -36,20 +36,20 @@ class PFGeneralPref(PreferenceView):
# Database path
self.stLogPath = wx.StaticText(panel, wx.ID_ANY, "Log file location:", wx.DefaultPosition, wx.DefaultSize, 0)
self.stLogPath.Wrap(-1)
mainSizer.Add(self.stLogPath, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
mainSizer.Add(self.stLogPath, 0, wx.ALL, 5)
self.inputLogPath = wx.TextCtrl(panel, wx.ID_ANY, config.logPath, wx.DefaultPosition, wx.DefaultSize, 0)
self.inputLogPath.SetEditable(False)
self.inputLogPath.SetBackgroundColour((200, 200, 200))
mainSizer.Add(self.inputLogPath, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5)
mainSizer.Add(self.inputLogPath, 0, wx.ALL | wx.EXPAND, 5)
import requests
self.certPath = wx.StaticText(panel, wx.ID_ANY, "Cert Path:", wx.DefaultPosition, wx.DefaultSize, 0)
self.certPath .Wrap(-1)
mainSizer.Add(self.certPath, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
mainSizer.Add(self.certPath, 0, wx.ALL, 5)
self.certPathCtrl = wx.TextCtrl(panel, wx.ID_ANY, requests.certs.where(), wx.DefaultPosition, wx.DefaultSize, 0)
self.certPathCtrl.SetEditable(False)
self.certPathCtrl.SetBackgroundColour((200, 200, 200))
mainSizer.Add(self.certPathCtrl, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5)
mainSizer.Add(self.certPathCtrl, 0, wx.ALL | wx.EXPAND, 5)
# Debug Logging
self.cbdebugLogging = wx.CheckBox(panel, wx.ID_ANY, "Debug Logging Enabled", wx.DefaultPosition, wx.DefaultSize, 0)
@@ -57,7 +57,7 @@ class PFGeneralPref(PreferenceView):
self.stDumpLogs = wx.StaticText(panel, wx.ID_ANY, "Pressing this button will cause all logs in memory to write to the log file:",
wx.DefaultPosition, wx.DefaultSize, 0)
mainSizer.Add(self.stDumpLogs, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
mainSizer.Add(self.stDumpLogs, 0, wx.ALL, 5)
self.btnDumpLogs = wx.Button(panel, wx.ID_ANY, "Dump All Logs", wx.DefaultPosition, wx.DefaultSize, 0)
self.btnDumpLogs.Bind(wx.EVT_BUTTON, OnDumpLogs)
mainSizer.Add(self.btnDumpLogs, 0, wx.ALIGN_LEFT, 5)

View File

@@ -135,7 +135,7 @@ class PFNetworkPref(PreferenceView):
self.stPSAutoDetected = wx.StaticText(panel, wx.ID_ANY, "Auto-detected: ", wx.DefaultPosition, wx.DefaultSize,
0)
self.stPSAutoDetected.Wrap(-1)
mainSizer.Add(self.stPSAutoDetected, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
mainSizer.Add(self.stPSAutoDetected, 0, wx.ALL, 5)
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer.AddStretchSpacer()

View File

@@ -86,8 +86,8 @@ class NavigationPanel(SFItem.SFBrowserItem):
def OnScheduleSearch(self, event):
search = self.BrowserSearchBox.GetValue()
# Make sure we do not count wildcard as search symbol
realsearch = search.replace("*", "")
# Make sure we do not count wildcards as search symbol
realsearch = search.replace('*', '').replace('?', '')
minChars = 1 if isStringCjk(realsearch) else 3
if len(realsearch) >= minChars:
self.lastSearch = search

View File

@@ -51,8 +51,7 @@ class FirepowerViewFull(StatsView):
self.headerPanel = headerPanel
hsizer = self.headerPanel.Parent.GetHeaderContentSizer()
self.stEff = wx.StaticText(self.headerPanel, wx.ID_ANY, "( Effective )")
hsizer.Add(self.stEff)
# self.headerPanel.GetParent().AddToggleItem(self.stEff)
hsizer.Insert(0, self.stEff)
panel = "full"
@@ -130,9 +129,12 @@ class FirepowerViewFull(StatsView):
self.panel.GetSizer().Layout()
# Remove effective label
hsizer = self.headerPanel.GetSizer()
hsizer.Hide(self.stEff)
# self.stEff.Destroy()
hsizer = self.headerPanel.Parent.GetHeaderContentSizer()
for i, c in enumerate(hsizer.Children):
if c.GetWindow() is self.stEff:
hsizer.Remove(i)
self.stEff.Destroy()
break
# Get the new view
view = StatsView.getView("miningyieldViewFull")(self.parent)

View File

@@ -123,6 +123,15 @@ class Miscellanea(ViewColumn):
text = ' | '.join(i[0] for i in info)
tooltip = ' and '.join(i[1] for i in info).capitalize()
return text, tooltip
elif itemGroup == "Vorton Projector":
cloudSize = stuff.getModifiedItemAttr("aoeCloudSize")
aoeVelocity = stuff.getModifiedItemAttr("aoeVelocity")
if not cloudSize or not aoeVelocity:
return "", None
text = "{0}{1} | {2}{3}".format(formatAmount(cloudSize, 3, 0, 3), "m",
formatAmount(aoeVelocity, 3, 0, 3), "m/s")
tooltip = "Explosion radius and explosion velocity"
return text, tooltip
elif itemCategory == "Subsystem":
slots = ("hi", "med", "low")
info = []
@@ -133,7 +142,7 @@ class Miscellanea(ViewColumn):
return "+ " + ", ".join(info), "Slot Modifiers"
elif (
itemGroup in ("Energy Neutralizer", "Structure Energy Neutralizer") or
(itemGroup == "Structure Burst Projector" and "doomsdayAOENeut" in item.effects)
(itemGroup in ("Structure Burst Projector", "Burst Projectors") and "doomsdayAOENeut" in item.effects)
):
neutAmount = stuff.getModifiedItemAttr("energyNeutralizerAmount")
cycleParams = stuff.getCycleParameters()
@@ -182,7 +191,7 @@ class Miscellanea(ViewColumn):
return text, tooltip
elif (
itemGroup in ("Stasis Web", "Stasis Webifying Drone", "Structure Stasis Webifier") or
(itemGroup == "Structure Burst Projector" and "doomsdayAOEWeb" in item.effects)
(itemGroup in ("Structure Burst Projector", "Burst Projectors") and "doomsdayAOEWeb" in item.effects)
):
speedFactor = stuff.getModifiedItemAttr("speedFactor")
if not speedFactor:
@@ -193,7 +202,7 @@ class Miscellanea(ViewColumn):
elif (
itemGroup == "Target Painter" or
(itemGroup == "Structure Disruption Battery" and "structureModuleEffectTargetPainter" in item.effects) or
(itemGroup == "Structure Burst Projector" and "doomsdayAOEPaint" in item.effects)
(itemGroup in ("Structure Burst Projector", "Burst Projectors") and "doomsdayAOEPaint" in item.effects)
):
sigRadBonus = stuff.getModifiedItemAttr("signatureRadiusBonus")
if not sigRadBonus:
@@ -204,7 +213,7 @@ class Miscellanea(ViewColumn):
elif (
itemGroup == "Sensor Dampener" or
(itemGroup == "Structure Disruption Battery" and "structureModuleEffectRemoteSensorDampener" in item.effects) or
(itemGroup == "Structure Burst Projector" and "doomsdayAOEDamp" in item.effects)
(itemGroup in ("Structure Burst Projector", "Burst Projectors") and "doomsdayAOEDamp" in item.effects)
):
lockRangeBonus = stuff.getModifiedItemAttr("maxTargetRangeBonus")
scanResBonus = stuff.getModifiedItemAttr("scanResolutionBonus")
@@ -226,7 +235,7 @@ class Miscellanea(ViewColumn):
return text, tooltip
elif (
itemGroup in ("Weapon Disruptor", "Structure Disruption Battery") or
(itemGroup == "Structure Burst Projector" and "doomsdayAOETrack" in item.effects)
(itemGroup in ("Structure Burst Projector", "Burst Projectors") and "doomsdayAOETrack" in item.effects)
):
# Weapon disruption now covers both tracking and guidance (missile) disruptors
# First get the attributes for tracking disruptors
@@ -279,7 +288,8 @@ class Miscellanea(ViewColumn):
"Heat Sink",
"Ballistic Control system",
"Structure Weapon Upgrade",
"Entropic Radiation Sink"
"Entropic Radiation Sink",
"Vorton Projector Upgrade"
):
attrMap = {
"Gyrostabilizer": ("damageMultiplier", "speedMultiplier", "Projectile weapon"),
@@ -287,7 +297,8 @@ class Miscellanea(ViewColumn):
"Heat Sink": ("damageMultiplier", "speedMultiplier", "Energy weapon"),
"Ballistic Control system": ("missileDamageMultiplierBonus", "speedMultiplier", "Missile"),
"Structure Weapon Upgrade": ("missileDamageMultiplierBonus", "speedMultiplier", "Missile"),
"Entropic Radiation Sink": ("damageMultiplier", "speedMultiplier", "Precursor weapon")}
"Entropic Radiation Sink": ("damageMultiplier", "speedMultiplier", "Precursor weapon"),
"Vorton Projector Upgrade": ("damageMultiplier", "speedMultiplier", "Vorton projector")}
dmgAttr, rofAttr, weaponName = attrMap[itemGroup]
dmg = stuff.getModifiedItemAttr(dmgAttr)
rof = stuff.getModifiedItemAttr(rofAttr)
@@ -311,8 +322,8 @@ class Miscellanea(ViewColumn):
tooltip = "Drone DPS boost"
return text, tooltip
elif (
itemGroup in ("ECM", "Burst Jammer", "Burst Projectors", "Structure ECM Battery") or
(itemGroup == "Structure Burst Projector" and "doomsdayAOEECM" in item.effects)
itemGroup in ("ECM", "Burst Jammer", "Structure ECM Battery") or
(itemGroup in ("Structure Burst Projector", "Burst Projectors") and "doomsdayAOEECM" in item.effects)
):
grav = stuff.getModifiedItemAttr("scanGravimetricStrengthBonus")
ladar = stuff.getModifiedItemAttr("scanLadarStrengthBonus")
@@ -608,6 +619,18 @@ class Miscellanea(ViewColumn):
shield_hp = stuff.getModifiedItemAttr("shieldBonus", 0)
hp = max(stuff_hp, armor_hp * cycles, capacitor_hp * cycles, shield_hp * cycles, 0)
nonChargedMap = {
"Ancillary Remote Armor Repairer": ("armor", "Armor repaired per second"),
"Ancillary Remote Shield Booster": ("shield", "Shield transferred per second")}
if not cycles and itemGroup in nonChargedMap:
rps = stuff.getRemoteReps(ignoreState=True)
rps = getattr(rps, nonChargedMap[itemGroup][0])
if not rps:
return "", None
text = "{0}/s".format(formatAmount(rps, 3, 0, 3, forceSign=True))
tooltip = nonChargedMap[itemGroup][1]
return text, tooltip
if not hp or not cycleTime or not cycles:
return "", None
@@ -668,6 +691,13 @@ class Miscellanea(ViewColumn):
formatAmount(itemArmorResistanceShiftHardenerExp, 3, 0, 3),
)
return text, tooltip
elif itemGroup in ("Cargo Scanner", "Ship Scanner", "Survey Scanner"):
duration = stuff.getModifiedItemAttr("duration")
if not duration:
return "", None
text = "{}s".format(formatAmount(duration / 1000, 3, 0, 0))
tooltip = "Scan duration"
return text, tooltip
elif stuff.charge is not None:
chargeGroup = stuff.charge.group.name
if chargeGroup.endswith("Rocket") or chargeGroup.endswith("Missile") or chargeGroup.endswith("Torpedo"):

View File

@@ -166,7 +166,7 @@ class FittingView(d.Display):
self.hoveredRow = None
self.hoveredColumn = None
self.Bind(wx.EVT_KEY_DOWN, self.kbEvent)
self.Bind(wx.EVT_KEY_UP, self.kbEvent)
self.Bind(wx.EVT_LEFT_DOWN, self.click)
self.Bind(wx.EVT_RIGHT_DOWN, self.click)
self.Bind(wx.EVT_MIDDLE_DOWN, self.click)
@@ -377,23 +377,7 @@ class FittingView(d.Display):
event.Skip()
return
batchOp = wx.GetMouseState().GetModifiers() == wx.MOD_ALT and getattr(event, 'allowBatch', None) is not False
# If we've selected ammo, then apply to the selected module(s)
if item.isCharge:
positions = []
fit = Fit.getInstance().getFit(fitID)
if batchOp:
for position, mod in enumerate(fit.modules):
if isinstance(mod, Module) and not mod.isEmpty:
positions.append(position)
else:
for mod in self.getSelectedMods():
if mod.isEmpty or mod not in fit.modules:
continue
positions.append(fit.modules.index(mod))
if len(positions) > 0:
self.mainFrame.command.Submit(cmd.GuiChangeLocalModuleChargesCommand(
fitID=fitID, positions=positions, chargeItemID=itemID))
elif (item.isModule and not batchOp) or item.isSubsystem:
if (item.isModule and not batchOp) or item.isSubsystem:
self.mainFrame.command.Submit(cmd.GuiAddLocalModuleCommand(fitID=fitID, itemID=itemID))
elif item.isModule and batchOp:
self.mainFrame.command.Submit(cmd.GuiFillWithNewLocalModulesCommand(fitID=fitID, itemID=itemID))
@@ -578,7 +562,10 @@ class FittingView(d.Display):
if sFit.serviceFittingOptions["rackSlots"]:
# flag to know when to add blanks, based on previous slot
slotDivider = None if sFit.serviceFittingOptions["rackLabels"] else self.mods[0].slot
if sFit.serviceFittingOptions["rackLabels"] or len(self.mods) == 0:
slotDivider = None
else:
slotDivider = self.mods[0].slot
# first loop finds where slot dividers must go before modifying self.mods
for i, mod in enumerate(self.mods):

View File

@@ -302,8 +302,8 @@ class ItemView(d.Display):
sMkt = Market.getInstance()
search = self.searchBox.GetLineText(0)
# Make sure we do not count wildcard as search symbol
realsearch = search.replace("*", "")
# Make sure we do not count wildcards as search symbol
realsearch = search.replace('*', '').replace('?', '')
# Show nothing if query is too short
if len(realsearch) < 3:
self.clearSearch()
@@ -311,12 +311,12 @@ class ItemView(d.Display):
sMkt.searchItems(search, self.populateSearch, 'implants')
def populateSearch(self, items):
def populateSearch(self, itemIDs):
if not self.IsShown():
self.parent.availableImplantsTree.Hide()
self.Show()
self.parent.Layout()
items = Market.getItems(itemIDs)
items = [i for i in items if i.group.name != 'Booster']
self.items = sorted(list(items), key=lambda i: i.name)

View File

@@ -34,7 +34,7 @@ from wx.lib.agw.floatspin import FloatSpin
import config
import gui.globalEvents as GE
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from gui.builtinViews.entityEditor import BaseValidator, EntityEditor, TextEntryValidatedDialog
from gui.builtinViews.implantEditor import BaseImplantEditorView
@@ -312,6 +312,7 @@ class CharacterEditor(AuxiliaryFrame):
class SkillTreeView(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
style=wx.TAB_TRAVERSAL)
@@ -402,7 +403,7 @@ class SkillTreeView(wx.Panel):
setattr(self, "{}Btn".format(name.lower()), btn)
btn.Enable(True)
btn.SetToolTip("%s skills %s clipboard" % (name, direction))
bSizerButtons.Add(btn, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_RIGHT | wx.ALL, 5)
bSizerButtons.Add(btn, 0, wx.ALL, 5)
btn.Bind(wx.EVT_BUTTON, getattr(self, "{}Skills".format(name.lower())))
pmainSizer.Add(bSizerButtons, 0, wx.EXPAND, 5)
@@ -611,12 +612,16 @@ class SkillTreeView(wx.Panel):
def spawnMenu(self, event):
item = event.GetItem()
itemData = self.skillTreeListCtrl.GetItemData(item)
if itemData is None:
return
self.skillTreeListCtrl.Select(item)
thing = self.skillTreeListCtrl.GetFirstChild(item).IsOk()
if thing:
return
id = self.skillTreeListCtrl.GetItemData(item)[1]
id = itemData[1]
eveItem = Market.getInstance().getItem(id)
srcContext = "skillItem"
@@ -729,12 +734,12 @@ class ImplantEditorView(BaseImplantEditorView):
sChar.removeImplant(char.ID, implant)
def addImplantSet(self, impSet):
def addImplants(self, implants):
charEditor = self.Parent.Parent
char = charEditor.entityEditor.getActiveEntity()
sChar = Character.getInstance()
for implant in impSet.implants:
for implant in implants:
sChar.addImplant(char.ID, implant.item.ID)
wx.PostEvent(charEditor, GE.CharChanged())
@@ -833,7 +838,7 @@ class APIView(wx.Panel):
def fetchSkills(self, evt):
sChar = Character.getInstance()
char = self.charEditor.entityEditor.getActiveEntity()
sChar.apiFetch(char.ID, self.__fetchCallback)
sChar.apiFetch(char.ID, APIView.fetchCallback)
def addCharacter(self, event):
sEsi = Esi.getInstance()
@@ -899,7 +904,8 @@ class APIView(wx.Panel):
if event is not None:
event.Skip()
def __fetchCallback(self, e=None):
@staticmethod
def fetchCallback(e=None):
if e:
pyfalog.warn("Error fetching skill information for character for __fetchCallback")
exc_type, exc_value, exc_trace = e

View File

@@ -402,6 +402,10 @@ class _TabRenderer:
width = max(width, self.min_width)
height = max(height, self.min_height)
cur_width, cur_height = self.tab_size
if (width == cur_width) and (height == cur_height):
return
self.tab_size = (width, height)
self.InitTab()
@@ -975,9 +979,6 @@ class _TabsContainer(wx.Panel):
sel_tab = self.tabs.index(tab)
self.Parent.SetSelection(sel_tab)
wx.PostEvent(self.Parent, PageChanged(self.tabs.index(old_sel_tab),
self.tabs.index(tab)))
return True
return False

View File

@@ -177,7 +177,7 @@ class CopySelectDialog(wx.Dialog):
def exportEsi(self, options, callback):
fit = getFit(self.mainFrame.getActiveFit())
Port.exportESI(fit, callback)
Port.exportESI(fit, True, callback)
def exportXml(self, options, callback):
fit = getFit(self.mainFrame.getActiveFit())

View File

@@ -26,7 +26,7 @@ import wx
from logbook import Logger
import eos.db
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.builtinShipBrowser.events import FitSelected

View File

@@ -26,7 +26,7 @@ import wx
from logbook import Logger
import config
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from service.prereqsCheck import version_block
@@ -106,7 +106,7 @@ class ErrorFrame(AuxiliaryFrame):
self.errorTextCtrl = wx.TextCtrl(self, wx.ID_ANY, version + version_block.strip(), wx.DefaultPosition,
(-1, 400), wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2 | wx.TE_DONTWRAP)
self.errorTextCtrl.SetFont(wx.Font(8, wx.FONTFAMILY_TELETYPE, wx.NORMAL, wx.NORMAL))
mainSizer.Add(self.errorTextCtrl, 0, wx.EXPAND | wx.ALL | wx.ALIGN_CENTER, 5)
mainSizer.Add(self.errorTextCtrl, 0, wx.EXPAND | wx.ALL, 5)
self.errorTextCtrl.AppendText("\n")
self.errorTextCtrl.Layout()

View File

@@ -5,16 +5,20 @@ import requests
import wx
from logbook import Logger
import config
import gui.globalEvents as GE
from eos.db import getItem
from eos.saveddata.cargo import Cargo
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.display import Display
from gui.characterEditor import APIView
from service.character import Character
from service.esi import Esi
from service.esiAccess import APIException
from service.fit import Fit
from service.port import Port
from service.port.esi import ESIExportException
from service.settings import EsiSettings
pyfalog = Logger(__name__)
@@ -204,7 +208,7 @@ class ExportToEve(AuxiliaryFrame):
def __init__(self, parent):
super().__init__(
parent, id=wx.ID_ANY, title="Export fit to EVE", pos=wx.DefaultPosition,
size=wx.Size(400, 120) if "wxGTK" in wx.PlatformInfo else wx.Size(350, 100), resizeable=True)
size=wx.Size(400, 140) if "wxGTK" in wx.PlatformInfo else wx.Size(350, 115), resizeable=True)
self.mainFrame = parent
@@ -221,6 +225,11 @@ class ExportToEve(AuxiliaryFrame):
mainSizer.Add(hSizer, 0, wx.EXPAND, 5)
self.exportChargesCb = wx.CheckBox(self, wx.ID_ANY, 'Export Loaded Charges', wx.DefaultPosition, wx.DefaultSize, 0)
self.exportChargesCb.SetValue(EsiSettings.getInstance().get('exportCharges'))
self.exportChargesCb.Bind(wx.EVT_CHECKBOX, self.OnChargeExportChange)
mainSizer.Add(self.exportChargesCb, 0, 0, 5)
self.exportBtn.Bind(wx.EVT_BUTTON, self.exportFitting)
self.statusbar = wx.StatusBar(self)
@@ -236,6 +245,10 @@ class ExportToEve(AuxiliaryFrame):
self.Center(wx.BOTH)
def OnChargeExportChange(self, event):
EsiSettings.getInstance().set('exportCharges', self.exportChargesCb.GetValue())
event.Skip()
def updateCharList(self):
sEsi = Esi.getInstance()
chars = sEsi.getSsoCharacters()
@@ -271,8 +284,9 @@ class ExportToEve(AuxiliaryFrame):
sEsi = Esi.getInstance()
sFit = Fit.getInstance()
exportCharges = self.exportChargesCb.GetValue()
try:
data = sPort.exportESI(sFit.getFit(fitID))
data = sPort.exportESI(sFit.getFit(fitID), exportCharges)
except ESIExportException as e:
msg = str(e)
if not msg:
@@ -335,7 +349,7 @@ class SsoCharacterMgmt(AuxiliaryFrame):
self.addBtn = wx.Button(self, wx.ID_ANY, "Add Character", wx.DefaultPosition, wx.DefaultSize, 0)
btnSizer.Add(self.addBtn, 0, wx.ALL | wx.EXPAND, 5)
self.deleteBtn = wx.Button(self, wx.ID_ANY, "Revoke Character", wx.DefaultPosition, wx.DefaultSize, 0)
self.deleteBtn = wx.Button(self, wx.ID_ANY, "Remove Character", wx.DefaultPosition, wx.DefaultSize, 0)
btnSizer.Add(self.deleteBtn, 0, wx.ALL | wx.EXPAND, 5)
mainSizer.Add(btnSizer, 0, wx.EXPAND, 5)
@@ -355,6 +369,16 @@ class SsoCharacterMgmt(AuxiliaryFrame):
def ssoLogin(self, event):
self.popCharList()
sChar = Character.getInstance()
# Update existing pyfa character, if it doesn't exist - create new
char = sChar.getCharacter(event.character.characterName)
newChar = False
if char is None:
char = sChar.new(event.character.characterName)
newChar = True
char.setSsoCharacter(event.character, config.getClientSecret())
sChar.apiFetch(char.ID, APIView.fetchCallback)
wx.PostEvent(self.mainFrame, GE.CharListUpdated())
event.Skip()
def kbEvent(self, event):

View File

@@ -40,9 +40,6 @@ class CalcAddLocalModuleCommand(wx.Command):
position=fit.modules.index(oldMod),
newModInfo=self.newModInfo)
return self.subsystemCmd.Do()
if not newMod.fits(fit):
pyfalog.warning('Module does not fit')
return False
fit.modules.append(newMod)
if newMod not in fit.modules:
pyfalog.warning('Failed to append to list')
@@ -52,6 +49,13 @@ class CalcAddLocalModuleCommand(wx.Command):
# relationship via .owner attribute, which is handled by SQLAlchemy
eos.db.flush()
sFit.recalc(fit)
# fits() sometimes relies on recalculated on-item attributes, such as fax cap
# booster limitation, so we have to check it after recalculating and remove the
# module if the check has failed
if not newMod.fits(fit):
pyfalog.warning('Module does not fit')
self.Undo()
return False
self.savedStateCheckChanges = sFit.checkStates(fit, newMod)
return True
@@ -63,7 +67,7 @@ class CalcAddLocalModuleCommand(wx.Command):
if self.savedPosition is None:
return False
from .localRemove import CalcRemoveLocalModulesCommand
cmd = CalcRemoveLocalModulesCommand(fitID=self.fitID, positions=[self.savedPosition], recalc=False)
cmd = CalcRemoveLocalModulesCommand(fitID=self.fitID, positions=[self.savedPosition], recalc=False, clearTail=True)
if not cmd.Do():
return False
restoreCheckedStates(Fit.getInstance().getFit(self.fitID), self.savedStateCheckChanges)

View File

@@ -3,7 +3,7 @@ from logbook import Logger
import eos.db
from eos.const import FittingSlot
from gui.fitCommands.helpers import ModuleInfo, restoreCheckedStates
from gui.fitCommands.helpers import ModuleInfo, restoreCheckedStates, restoreRemovedDummies
from service.fit import Fit
@@ -12,14 +12,16 @@ pyfalog = Logger(__name__)
class CalcRemoveLocalModulesCommand(wx.Command):
def __init__(self, fitID, positions, recalc=True):
def __init__(self, fitID, positions, recalc=True, clearTail=False):
wx.Command.__init__(self, True, 'Remove Module')
self.fitID = fitID
self.positions = positions
self.recalc = recalc
self.clearTail = clearTail
self.savedSubInfos = None
self.savedModInfos = None
self.savedStateCheckChanges = None
self.savedTail = None
def Do(self):
pyfalog.debug('Doing removal of local modules from positions {} on fit {}'.format(self.positions, self.fitID))
@@ -40,6 +42,9 @@ class CalcRemoveLocalModulesCommand(wx.Command):
if len(self.savedSubInfos) == 0 and len(self.savedModInfos) == 0:
return False
if self.clearTail:
self.savedTail = fit.clearTail()
if self.recalc:
# Need to flush because checkStates sometimes relies on module->fit
# relationship via .owner attribute, which is handled by SQLAlchemy
@@ -76,6 +81,7 @@ class CalcRemoveLocalModulesCommand(wx.Command):
if not any(results):
return False
restoreCheckedStates(fit, self.savedStateCheckChanges)
restoreRemovedDummies(fit, self.savedTail)
return True
@property

View File

@@ -39,7 +39,17 @@ class CalcReplaceLocalModuleCommand(wx.Command):
if newMod.slot != oldMod.slot:
return False
# Dummy it out in case the next bit fails
fit.modules.free(self.position)
fit.modules.replace(self.position, newMod)
if newMod not in fit.modules:
pyfalog.warning('Failed to replace in list')
self.Undo()
return False
if self.recalc:
# Need to flush because checkStates sometimes relies on module->fit
# relationship via .owner attribute, which is handled by SQLAlchemy
eos.db.flush()
sFit.recalc(fit)
self.savedStateCheckChanges = sFit.checkStates(fit, newMod)
if not self.ignoreRestrictions and not newMod.fits(fit):
pyfalog.warning('Module does not fit')
self.Undo()
@@ -52,17 +62,6 @@ class CalcReplaceLocalModuleCommand(wx.Command):
pyfalog.warning('Invalid charge')
self.Undo()
return False
fit.modules.replace(self.position, newMod)
if newMod not in fit.modules:
pyfalog.warning('Failed to replace in list')
self.Undo()
return False
if self.recalc:
# Need to flush because checkStates sometimes relies on module->fit
# relationship via .owner attribute, which is handled by SQLAlchemy
eos.db.flush()
sFit.recalc(fit)
self.savedStateCheckChanges = sFit.checkStates(fit, newMod)
return True
def Undo(self):

View File

@@ -324,7 +324,8 @@ def activeStateLimit(itemIdentity):
'microJumpDrive', 'microJumpPortalDrive', 'emergencyHullEnergizer',
'cynosuralGeneration', 'jumpPortalGeneration', 'jumpPortalGenerationBO',
'cloneJumpAccepting', 'cloakingWarpSafe', 'cloakingPrototype', 'cloaking',
'massEntanglerEffect5', 'electronicAttributeModifyOnline', 'targetPassively'
'massEntanglerEffect5', 'electronicAttributeModifyOnline', 'targetPassively',
'cargoScan', 'shipScan', 'surveyScan'
}.intersection(item.effects):
return FittingModuleState.ONLINE
return FittingModuleState.ACTIVE
@@ -353,6 +354,8 @@ def restoreCheckedStates(fit, stateInfo, ignoreModPoss=()):
def restoreRemovedDummies(fit, dummyInfo):
if dummyInfo is None:
return
# Need this to properly undo the case when removal of subsystems removes dummy slots
for position in sorted(dummyInfo):
slot = dummyInfo[position]

View File

@@ -23,7 +23,7 @@ import wx
import config
import gui.mainFrame
from eos.saveddata.module import Module
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from gui.builtinItemStatsViews.itemAffectedBy import ItemAffectedBy
from gui.builtinItemStatsViews.itemAttributes import ItemParams

View File

@@ -110,8 +110,6 @@ class OpenFitsThread(threading.Thread):
self.start()
def run(self):
time.sleep(0.5) # Give GUI some time to finish drawing
# `startup` tells FitSpawner that we are loading fits are startup, and
# has 3 values:
# False = Set as default in FitSpawner itself, never set here
@@ -610,7 +608,9 @@ class MainFrame(wx.Frame):
(wx.ACCEL_CTRL, wx.WXK_PAGEDOWN, ctabnext),
(wx.ACCEL_CTRL, wx.WXK_PAGEUP, ctabprev),
(wx.ACCEL_CMD, wx.WXK_PAGEDOWN, ctabnext),
(wx.ACCEL_CMD, wx.WXK_PAGEUP, ctabprev)
(wx.ACCEL_CMD, wx.WXK_PAGEUP, ctabprev),
(wx.ACCEL_CMD | wx.ACCEL_SHIFT, ord("Z"), wx.ID_REDO)
]
# Ctrl/Cmd+# for addition pane selection
@@ -678,8 +678,8 @@ class MainFrame(wx.Frame):
def toggleOverrides(self, event):
ModifiedAttributeDict.overrides_enabled = not ModifiedAttributeDict.overrides_enabled
wx.PostEvent(self, GE.FitChanged(fitIDs=(self.getActiveFit(),)))
changedFitIDs = Fit.getInstance().processOverrideToggle()
wx.PostEvent(self, GE.FitChanged(fitIDs=changedFitIDs))
menu = self.GetMenuBar()
menu.SetLabel(menu.toggleOverridesId,
"&Turn Overrides Off" if ModifiedAttributeDict.overrides_enabled else "&Turn Overrides On")

View File

@@ -80,8 +80,8 @@ class MainMenuBar(wx.MenuBar):
fitMenu = wx.Menu()
self.Append(fitMenu, "Fi&t")
fitMenu.Append(wx.ID_UNDO)
fitMenu.Append(wx.ID_REDO)
fitMenu.Append(wx.ID_UNDO, "&Undo\tCTRL+Z", "Undo the most recent action")
fitMenu.Append(wx.ID_REDO, "&Redo\tCTRL+Y", "Redo the most recent undone action")
fitMenu.AppendSeparator()
fitMenu.Append(wx.ID_COPY, "&To Clipboard\tCTRL+C", "Export a fit to the clipboard")

View File

@@ -21,7 +21,7 @@
import wx
from logbook import Logger
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from gui.builtinViews.entityEditor import BaseValidator, EntityEditor
from gui.utils.clipboard import fromClipboard, toClipboard
@@ -67,7 +67,8 @@ class DmgPatternEntityEditor(EntityEditor):
def getEntitiesFromContext(self):
sDP = DamagePattern.getInstance()
choices = sorted(sDP.getUserDamagePatternList(), key=lambda p: p.rawName)
return [c for c in choices if c.rawName != "Selected Ammo"]
choices = [c for c in choices if c.rawName != "Selected Ammo"]
return choices
def DoNew(self, name):
sDP = DamagePattern.getInstance()
@@ -180,9 +181,13 @@ class DmgPatternEditor(AuxiliaryFrame):
setattr(self, name, btn)
btn.Enable(True)
btn.SetToolTip("%s patterns %s clipboard" % (name, direction))
footerSizer.Add(btn, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_RIGHT)
footerSizer.Add(btn, 0)
btn.Bind(wx.EVT_BUTTON, getattr(self, "{}Patterns".format(name.lower())))
if not self.entityEditor.checkEntitiesExist():
self.Close()
return
self.Layout()
bsize = self.GetBestSize()
self.SetSize((-1, bsize.height))
@@ -232,6 +237,11 @@ class DmgPatternEditor(AuxiliaryFrame):
self.entityEditor.btnDelete.Enable()
def patternChanged(self, event=None):
if not self.entityEditor.checkEntitiesExist():
self.Close()
return
p = self.entityEditor.getActiveEntity()
if p is None:

View File

@@ -10,9 +10,10 @@ import gui.builtinMarketBrowser.pfSearchBox as SBox
import gui.display as d
import gui.globalEvents as GE
from eos.db.gamedata.queries import getAttributeInfo, getItem
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from gui.marketBrowser import SearchBox
from service.fit import Fit
from service.market import Market
@@ -170,12 +171,15 @@ class ItemView(d.Display):
d.Display.__init__(self, parent)
self.activeItems = []
self.searchTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.scheduleSearch, self.searchTimer)
self.searchBox = parent.Parent.Parent.searchBox
# Bind search actions
self.searchBox.Bind(SBox.EVT_TEXT_ENTER, self.scheduleSearch)
self.searchBox.Bind(SBox.EVT_SEARCH_BTN, self.scheduleSearch)
self.searchBox.Bind(SBox.EVT_CANCEL_BTN, self.clearSearch)
self.searchBox.Bind(SBox.EVT_TEXT, self.scheduleSearch)
self.searchBox.Bind(SBox.EVT_TEXT, self.delaySearch)
self.update(Market.getInstance().getItemsWithOverrides())
@@ -188,12 +192,17 @@ class ItemView(d.Display):
if updateDisplay:
self.update(Market.getInstance().getItemsWithOverrides())
def delaySearch(self, evt):
sFit = Fit.getInstance()
self.searchTimer.Stop()
self.searchTimer.Start(sFit.serviceFittingOptions["marketSearchDelay"], True)
def scheduleSearch(self, event=None):
sMkt = Market.getInstance()
search = self.searchBox.GetLineText(0)
# Make sure we do not count wildcard as search symbol
realsearch = search.replace("*", "")
# Make sure we do not count wildcards as search symbol
realsearch = search.replace('*', '').replace('?', '')
# Show nothing if query is too short
if len(realsearch) < 3:
self.clearSearch()
@@ -204,21 +213,10 @@ class ItemView(d.Display):
def itemSort(self, item):
sMkt = Market.getInstance()
isFittable = item.group.name in sMkt.FIT_GROUPS or item.category.name in sMkt.FIT_CATEGORIES
catname = sMkt.getCategoryByItem(item).name
try:
mktgrpid = sMkt.getMarketGroupByItem(item).ID
except AttributeError:
mktgrpid = -1
pyfalog.warning("unable to find market group for {}".format(item.name))
parentname = sMkt.getParentItemByItem(item).name
# Get position of market group
metagrpid = sMkt.getMetaGroupIdByItem(item)
metatab = sMkt.META_MAP_REVERSE_INDICES.get(metagrpid)
metalvl = item.metaLevel or 0
return (not isFittable, *sMkt.itemSort(item))
return not isFittable, catname, mktgrpid, parentname, metatab, metalvl, item.name
def populateSearch(self, items):
def populateSearch(self, itemIDs):
items = Market.getItems(itemIDs)
self.update(items)
def populate(self, items):

View File

@@ -254,7 +254,6 @@ class PyGauge(wx.Window):
w = rect.width
else:
w = rect.width * (float(value) / 100)
r = copy.copy(rect)
r.width = w
dc.DrawRectangle(r)
@@ -315,7 +314,8 @@ class PyGauge(wx.Window):
color,
gradient_color
)
dc.DrawBitmap(gradient_bitmap, r.left, r.top)
if gradient_bitmap is not None:
dc.DrawBitmap(gradient_bitmap, r.left, r.top)
# font stuff begins here
dc.SetFont(self.font)

View File

@@ -21,7 +21,7 @@
import wx
from logbook import Logger
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.builtinViews.entityEditor import BaseValidator, EntityEditor
from gui.builtinViews.implantEditor import BaseImplantEditorView
from gui.utils.clipboard import fromClipboard, toClipboard
@@ -159,7 +159,7 @@ class ImplantSetEditor(AuxiliaryFrame):
setattr(self, name, btn)
btn.Enable(True)
btn.SetToolTip("%s implant sets %s clipboard" % (name, direction))
footerSizer.Add(btn, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_RIGHT)
footerSizer.Add(btn, 0)
mainSizer.Add(footerSizer, 0, wx.ALL | wx.EXPAND, 5)

View File

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

View File

@@ -148,7 +148,7 @@ class StatsPane(wx.Panel):
sizer.AddStretchSpacer()
# Add menu
header_menu = wx.StaticText(tp.GetHeaderPanel(), wx.ID_ANY, "\u2630", size=wx.Size((10, -1)))
sizer.Add(header_menu , 0, wx.EXPAND | wx.RIGHT | wx.ALIGN_RIGHT, 5)
sizer.Add(header_menu , 0, wx.EXPAND | wx.RIGHT, 5)
header_menu.Bind(wx.EVT_CONTEXT_MENU, handler)
header_menu.Bind(wx.EVT_LEFT_UP, handler)

View File

@@ -25,12 +25,12 @@ from collections import OrderedDict
import wx
from logbook import Logger
import gui.mainFrame
import gui.globalEvents as GE
from gui.auxFrame import AuxiliaryFrame
import gui.mainFrame
from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from gui.builtinViews.entityEditor import EntityEditor, BaseValidator
from gui.utils.clipboard import toClipboard, fromClipboard
from gui.builtinViews.entityEditor import BaseValidator, EntityEditor
from gui.utils.clipboard import fromClipboard, toClipboard
from gui.utils.inputs import FloatBox, InputValidator, strToFloat
from service.fit import Fit
from service.targetProfile import TargetProfile
@@ -183,7 +183,7 @@ class TargetProfileEditor(AuxiliaryFrame):
ttText, unitText = self.ATTRIBUTES[attr]
bmp = wx.StaticBitmap(self, wx.ID_ANY, BitmapLoader.getBitmap("%s_big" % attr, "gui"))
bmp.SetToolTip(wx.ToolTip(ttText))
miscAttrSizer.Add(bmp, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.LEFT, leftPad)
miscAttrSizer.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)
editBox.SetToolTip(wx.ToolTip(ttText))
@@ -231,7 +231,7 @@ class TargetProfileEditor(AuxiliaryFrame):
setattr(self, name, btn)
btn.Enable(True)
btn.SetToolTip("%s profiles %s clipboard" % (name, direction))
footerSizer.Add(btn, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_RIGHT)
footerSizer.Add(btn, 0)
btn.Bind(wx.EVT_BUTTON, getattr(self, "{}Patterns".format(name.lower())))
if not self.entityEditor.checkEntitiesExist():

View File

@@ -35,6 +35,8 @@ def DrawFilledBitmap(width, height, color):
def DrawGradientBar(width, height, gStart, gEnd, gMid=None, fillRatio=4):
if width == 0 or height == 0:
return None
canvas = wx.Bitmap(width, height)
mdc = wx.MemoryDC()

View File

@@ -1,5 +1,7 @@
import math
from eos.utils.round import roundToPrec, roundDec
def formatAmount(val, prec=3, lowest=0, highest=0, currency=False, forceSign=False, unitName=None):
"""
@@ -97,29 +99,3 @@ def formatAmount(val, prec=3, lowest=0, highest=0, currency=False, forceSign=Fal
else:
result = "{}{} {}{}".format(sign, mantissa, suffix, unitName)
return result
def roundToPrec(val, prec, nsValue=None):
"""
nsValue: custom value which should be used to determine normalization shift
"""
# We're not rounding integers anyway
# Also make sure that we do not ask to calculate logarithm of zero
if int(val) == val:
return int(val)
roundFactor = int(prec - math.floor(math.log10(abs(val if nsValue is None else nsValue))) - 1)
# But we don't want to round integers
if roundFactor < 0:
roundFactor = 0
# Do actual rounding
val = round(val, roundFactor)
# Make sure numbers with .0 part designating float don't get through
if int(val) == val:
val = int(val)
return val
def roundDec(val, prec):
if int(val) == val:
return int(val)
return round(val, prec)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 B

After

Width:  |  Height:  |  Size: 590 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 B

After

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 778 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 732 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 B

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 B

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 740 B

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 897 B

After

Width:  |  Height:  |  Size: 693 B

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