Compare commits

..

476 Commits

Author SHA1 Message Date
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
DarkPhoenix
10583fd506 Update fighter additions tab icon 2020-02-13 17:25:40 +03:00
DarkPhoenix
713694be56 Bump version 2020-02-13 17:00:58 +03:00
DarkPhoenix
f50293cf77 Revert to wxpython 4.0.6 once again (see issue 2136) 2020-02-13 16:57:23 +03:00
DarkPhoenix
7c88fa477f Bump version 2020-02-13 12:20:03 +03:00
DarkPhoenix
f8df540fad Use newer wxpython version once again 2020-02-13 12:19:37 +03:00
DarkPhoenix
61a01805cc Reset locale and remove resolution data from all the images (wxpython phoenix issue #1515) 2020-02-13 12:14:26 +03:00
Metallicow
a93915cf04 trim trailing whitespace 2020-02-12 16:14:12 -06:00
DarkPhoenix
0f74c97fbf Temporarily revert python version to 3.7 2020-02-12 18:31:44 +03:00
DarkPhoenix
29713b69dc Fix the hack 2020-02-12 18:25:17 +03:00
DarkPhoenix
cc3c2cb9c8 Add hack to build wx on windows 2020-02-12 18:22:31 +03:00
DarkPhoenix
b36a3959da Revert wxpython version to 4.0.6 2020-02-12 18:18:18 +03:00
DarkPhoenix
3b26bfc0e9 Add missing entries to migration 2020-02-11 17:02:00 +03:00
DarkPhoenix
c59b621963 Add capacitor transfer tiericide conversion entries 2020-02-11 16:52:15 +03:00
DarkPhoenix
977a8fa329 Update staticdata to 1663997 2020-02-11 16:20:08 +03:00
DarkPhoenix
a5226fee83 Merge branch 'singularity' 2020-02-11 16:08:46 +03:00
DarkPhoenix
e4069a3988 Update AppVeyor deploy oauth key after repo transfer 2020-02-04 00:41:03 +03:00
DarkPhoenix
6527f9e11e Attempt to terminate threads when pyfa is closed 2020-02-03 17:12:23 +03:00
DarkPhoenix
9ddfcc894f Make sure not to catch keyboard interrupts and system exits 2020-02-03 16:12:41 +03:00
DarkPhoenix
f22a4f13e5 Allow pasting non-mutated modules via ctrl-v 2020-02-03 15:54:22 +03:00
DarkPhoenix
fc9337df67 Add more info prints to mac os packaging scripts 2020-02-03 13:37:27 +03:00
DarkPhoenix
98a8332cfa Change the way we update pip for mac os builds 2020-02-03 13:33:26 +03:00
DarkPhoenix
a78eedad48 Remove unused files 2020-02-03 13:30:04 +03:00
DarkPhoenix
85e1a1bd06 Use newer image version for mac builds 2020-02-03 13:25:51 +03:00
DarkPhoenix
25b2c04309 Do not set environment variable as it's not used anymore 2020-02-03 13:23:56 +03:00
DarkPhoenix
54a84d0e42 Do not attempt to install custom python version, just use system default 2020-02-03 13:23:13 +03:00
DarkPhoenix
af0a4d1def Update travis deployment key 2020-02-03 13:11:48 +03:00
DarkPhoenix
97cdde84ce Uninstall not when setup just begins, but right before install begins 2020-02-03 06:17:28 +03:00
DarkPhoenix
a65129b277 Fix function definition order in inno setup script 2020-02-03 06:06:25 +03:00
DarkPhoenix
3b40a49918 Uninstall previous pyfa version unconditionally 2020-02-03 05:59:29 +03:00
DarkPhoenix
d95345b476 Insist on uninstalling old pyfa versions when next one is about to be installed 2020-02-02 23:48:06 +03:00
DarkPhoenix
2ba2d95017 Change clone depth to 400 on appveyor 2020-02-02 14:46:26 +03:00
DarkPhoenix
4d0ffedcc8 Install in 64 bit mode on Windows 2020-02-02 11:07:20 +03:00
DarkPhoenix
84abde4fc5 Some OCD in echoes + testing if build is started after webhook removal 2020-02-02 04:22:41 +03:00
DarkPhoenix
8897f1e4b1 Use my credentials when uploading builds to releases 2020-02-02 04:08:10 +03:00
DarkPhoenix
6f33cacb7a Clean up appveyor file 2020-02-02 03:54:52 +03:00
DarkPhoenix
b1a8c0ad09 Disable appveyor's RDP setup as it's a security hole 2020-02-02 03:40:20 +03:00
DarkPhoenix
13ed635803 We do not care if all PR builds will be built 2020-02-02 03:38:43 +03:00
DarkPhoenix
5a44909ebf Remove appveyor environment preparation which is not needed anymore 2020-02-02 03:13:57 +03:00
DarkPhoenix
6cbb80693d Switch to powershell echo 2020-02-02 03:04:53 +03:00
DarkPhoenix
0b90d254f9 Try another approach in echo'ing 2020-02-02 02:53:18 +03:00
DarkPhoenix
641d36205c Remove quotes from echo commands 2020-02-02 02:47:14 +03:00
DarkPhoenix
6e38b6ea4d Fix path to windows SDK once again 2020-02-02 02:38:26 +03:00
DarkPhoenix
84b4d8fabb Change windows SDK location 2020-02-02 02:03:23 +03:00
DarkPhoenix
c0e1f7e746 Fix mistype 2020-02-01 15:36:33 +03:00
DarkPhoenix
d734c12168 Try to find out when listing windows SDKs fails 2020-02-01 15:34:14 +03:00
DarkPhoenix
340d94eb3e Specify image in apprveyor file 2020-02-01 15:09:47 +03:00
DarkPhoenix
9b0b31648c Add debugging prints to see appveyor's OS version 2020-02-01 02:38:00 +03:00
DarkPhoenix
8346358c37 Fix appveyor script slashes 2020-02-01 02:24:09 +03:00
DarkPhoenix
b935700a12 Bump INNO setup version 2020-02-01 01:56:26 +03:00
DarkPhoenix
2f4a5a4830 Fix improper use of operator "is" when comparing to literal values 2020-01-31 18:16:36 +03:00
DarkPhoenix
e951ce8e9d Bump version 2020-01-31 18:06:28 +03:00
DarkPhoenix
fc6d42f483 Update effects 2020-01-31 17:59:50 +03:00
DarkPhoenix
7df1431ad2 Update staticdata to 1657583 2020-01-31 17:40:51 +03:00
DarkPhoenix
4d4680961e Bump versions of multiple packages 2020-01-31 16:08:24 +03:00
DarkPhoenix
1cae99b812 Remove VC 9.0 tools manifest
We're using newer version now, and we will see later if dependency section in manifest is needed
2020-01-31 16:07:26 +03:00
DarkPhoenix
3727d19311 In attribute editor what is you see is what you get now 2020-01-31 10:32:18 +03:00
DarkPhoenix
c45c093f4f Return search results as list to avoid various issues 2020-01-31 10:04:35 +03:00
DarkPhoenix
b613d3e312 Use market sorting in attribute override editor 2020-01-31 00:37:35 +03:00
DarkPhoenix
4cff9247b0 Always include ships and citadels in override editor output 2020-01-31 00:13:40 +03:00
DarkPhoenix
b9a26ec28d Do not return items from all the categories in overrides editor searches 2020-01-31 00:00:05 +03:00
DarkPhoenix
245f81e888 Actually make appveyor to use 64 bit python 2020-01-30 13:55:30 +03:00
DarkPhoenix
204c1ba9f2 Switch windows build to be 64 bits 2020-01-30 13:45:49 +03:00
DarkPhoenix
a7a4be133c Bump pyinstaller version once again 2020-01-30 13:04:49 +03:00
DarkPhoenix
14f9a46d7a Remove MPL hook 2020-01-30 11:59:04 +03:00
DarkPhoenix
003dd040dc Bump matplotlib version 2020-01-30 11:51:27 +03:00
DarkPhoenix
97cf53217e Bump python version to 3.8 2020-01-30 11:09:11 +03:00
DarkPhoenix
7c1805826d Fix mino and apostle RR effects affecting subcap modules 2020-01-29 16:11:30 +03:00
DarkPhoenix
d6a2b4dfc4 Fix projected system-wide effects menu after multiple renames 2020-01-29 15:58:02 +03:00
DarkPhoenix
d70952520a Update effects 2020-01-29 15:41:55 +03:00
DarkPhoenix
a57867b806 Update static data to 1655541 2020-01-29 15:18:07 +03:00
DarkPhoenix
17d7583cfc Remove overrides which are not needed 2020-01-29 12:54:23 +03:00
DarkPhoenix
55ad95081b Fix alternate tree display in case units did not have display names 2020-01-28 15:16:48 +03:00
DarkPhoenix
da8da1759f Bump version 2020-01-28 14:59:20 +03:00
DarkPhoenix
b4e758b9ee Add new and old but missing conversions 2020-01-28 14:58:54 +03:00
DarkPhoenix
3ba41db699 Update data to 1655072 2020-01-28 14:39:35 +03:00
DarkPhoenix
cfc47cf483 When learning skill in character editor, learn all the prerequisites 2020-01-27 11:44:20 +03:00
DarkPhoenix
38c8be995b Add even more condensed tooltip version (not used yet) 2020-01-24 17:56:07 +03:00
DarkPhoenix
492207700b Force pyinstaller to 3.3 on mac as well 2020-01-21 22:17:29 +03:00
DarkPhoenix
b69adefbf8 Restore matplotlib pyinstaller hook which is still needed in 3.3 2020-01-21 22:00:08 +03:00
DarkPhoenix
361299f51e Bump version 2020-01-21 21:51:02 +03:00
DarkPhoenix
025091c3f6 Roll back pyinstaller to 3.3 for windows builds 2020-01-21 21:50:10 +03:00
DarkPhoenix
a54d70036b Bump version number 2020-01-21 15:32:16 +03:00
DarkPhoenix
9e67b5962c Update the way we update pip on appveyor 2020-01-21 15:31:48 +03:00
Anton Vorobyov
ed0c080163 Merge pull request #2123 from Neugeniko/master
Update Invasion Damage Profiles due to npc changes.
2020-01-21 15:26:53 +03:00
Neugeniko
21389d84fc Update Invasion Damage Profiles due to npc changes.
#2122
2020-01-21 23:17:37 +11:00
DarkPhoenix
0fe54631ad Update staticdata to 1651039 2020-01-21 15:00:21 +03:00
DarkPhoenix
36ea5200f2 Remove pyinstaller from requirements as it's needed just for packaging 2020-01-21 13:33:17 +03:00
DarkPhoenix
d9934160a6 Compatibility with pyinstaller 2020-01-21 13:30:48 +03:00
DarkPhoenix
b9c92c48d3 Bump wx version 2020-01-17 17:15:21 +03:00
DarkPhoenix
6d23df6156 Fix on-effect resistance definition 2020-01-17 16:08:55 +03:00
DarkPhoenix
a913ab72c2 Bump version of pyinstaller to avoid security issues 2020-01-17 11:14:28 +03:00
DarkPhoenix
f6d33a2ac1 Use effect IDs rather than effect amount when deciding if we should rebuild abilities 2020-01-16 19:02:10 +03:00
DarkPhoenix
5a3fec33a7 Bump version 2020-01-16 16:26:19 +03:00
DarkPhoenix
95734aca2c Add fighter AB effect 2020-01-16 16:19:37 +03:00
DarkPhoenix
377254be9d Add nirvana effects 2020-01-16 16:09:17 +03:00
DarkPhoenix
ad9905a5e0 Add shield booster tieritice conversions 2020-01-16 15:46:30 +03:00
DarkPhoenix
6fc5a9699e Add slave as jargon entry for amulets 2020-01-16 15:39:54 +03:00
DarkPhoenix
0ee86f4836 Add slaves to renames dictionary 2020-01-16 15:34:56 +03:00
DarkPhoenix
85593367f7 Remove single-precition workaround and update data to 1646988 2020-01-16 15:19:39 +03:00
DarkPhoenix
465ea61b06 Merge branch 'singularity' 2020-01-16 14:57:44 +03:00
DarkPhoenix
2d5507e951 Force faction structure neut to be variation of t1 item 2020-01-08 00:47:56 +03:00
DarkPhoenix
84e20be153 Add auto targeting system to list of mods which do not get online by default 2020-01-04 19:32:31 +03:00
DarkPhoenix
60080c02b0 Fix red giant ecm bonus 2020-01-02 22:11:16 +03:00
DarkPhoenix
2a0d4179d0 Make script effect passive 2020-01-02 22:03:05 +03:00
DarkPhoenix
6dd55e0cd4 Move sansha incursion and sleeper profile one level up, as there are not too many 2019-12-28 21:48:56 +03:00
Anton Vorobyov
0adc6e0c6f Merge pull request #2113 from Neugeniko/master
Add Abyssal/Invasion Damage and Target Profiles.
2019-12-28 21:45:51 +03:00
Neugeniko
e155356701 Add Invasion & Abyssal Target Profiles 2019-12-29 00:28:48 +11:00
Neugeniko
ef44b50980 Add Abyssal & Invasion Damage Profile 2019-12-29 00:26:39 +11:00
DarkPhoenix
6785d16c87 More mods not activated by default 2019-12-28 04:22:11 +03:00
DarkPhoenix
76c670b7d5 Do not activate several categories of modules on fitting 2019-12-28 03:53:02 +03:00
DarkPhoenix
717303a72f Allow to put separate charges into citadel cargo 2019-12-27 12:39:37 +03:00
DarkPhoenix
e13125b174 Enable sigamp tooltip for citadels 2019-12-27 12:35:51 +03:00
DarkPhoenix
8aa7964b81 Do not show effective cap stats for ships with 100% neut resist (citadels) 2019-12-27 12:27:30 +03:00
DarkPhoenix
5e5f23d296 Show DG standup ECM as variation of t1 module 2019-12-26 12:28:01 +03:00
DarkPhoenix
05837f99ff Change order of structure meta levels 2019-12-26 12:26:25 +03:00
DarkPhoenix
70f620c2c3 Fix unit name processing 2019-12-25 01:33:43 +03:00
DarkPhoenix
10ffb987ea Detect concord race and show appropriate icon 2019-12-23 17:02:34 +03:00
DarkPhoenix
13ed785551 Drop tank stats on factor reload toggle 2019-12-23 13:26:23 +03:00
DarkPhoenix
69d2e38647 Use modified item attributes to show ancillary tank modules reload time 2019-12-23 13:17:08 +03:00
DarkPhoenix
c45abbdbcf Use new table as source for skill requirement data 2019-12-19 20:44:17 +03:00
DarkPhoenix
2655b001a9 Add new skillrequirement file 2019-12-19 19:56:23 +03:00
DarkPhoenix
3c7f4edb1b Fix replacements generation 2019-12-19 16:41:49 +03:00
DarkPhoenix
663cbab401 More updates to the script, and data update as well 2019-12-19 16:24:45 +03:00
DarkPhoenix
897ca9ca43 Update DB script to be able to work with sisi data 2019-12-19 15:56:57 +03:00
DarkPhoenix
b31a071859 Fix crashes related to target profiles / damage patterns 2019-12-17 13:10:41 +03:00
DarkPhoenix
8c2788fd78 Do not show implant set editor if user cancels addition of an implant set 2019-12-16 18:06:03 +03:00
DarkPhoenix
dee8fa400c Allow to override menu visibility settings with control key 2019-12-16 16:29:42 +03:00
DarkPhoenix
2339327473 Do some renames 2019-12-16 13:59:41 +03:00
Anton Vorobyov
f6f9239dd5 Merge pull request #2103 from Gochim/issue/2073
Issue #2073: Added ability to add a list of implants from implant view to a new implant set
2019-12-16 13:55:00 +03:00
DarkPhoenix
cd4c8c8c10 Do not save changes when changes were reverted 2019-12-16 13:42:49 +03:00
DarkPhoenix
c25eda8b64 Do not save mutated values when user did not touch them (and they were altered due to rounding) 2019-12-16 13:11:18 +03:00
Gochim
b6edf0e034 Issue #2073: Fixed a stuck in character editor 2019-12-12 18:03:45 +02:00
Gochim
af4277fc7e Issue #2073: Fixed name of a popup menu item for implant tab 2019-12-12 17:57:03 +02:00
Gochim
acd774abe5 Issue #2073: Fixed Codacy warning 2019-12-12 14:02:31 +02:00
Gochim
9081353634 Issue #2073: Final tweaks on implementation 2019-12-11 07:51:31 +02:00
Gochim
9c7fa37a72 Issue #2073: Added functionality to add all implants in fit implant tab to implant set list 2019-12-10 18:14:13 +02:00
Gochim
d92e11893a Issue #2073: Added mechanism to pass implant list to implant set manager 2019-12-10 08:50:21 +02:00
Gochim
f55ab00bf5 Fixed error message text in EntityEditor 2019-12-10 08:18:01 +02:00
DarkPhoenix
36265aa2a3 Add comment about built-in pattern order 2019-12-07 15:52:10 +03:00
DarkPhoenix
bfedcdbffd Simplify code a little 2019-12-07 15:42:26 +03:00
DarkPhoenix
a5d10c4a76 Change sorting of built-in damage profiles 2019-12-07 15:41:07 +03:00
DarkPhoenix
2962ce1945 Ensure 0%-100% spoolup is sorted properly 2019-12-07 03:07:49 +03:00
DarkPhoenix
0063d2955e Add info on invasion ships 2019-12-07 02:53:48 +03:00
DarkPhoenix
9787a18666 Fix issue with inability to right-click user-defined target profiles in graph window 2019-12-07 02:47:24 +03:00
DarkPhoenix
d8cd3197b5 Show short profile name in graphs 2019-12-07 02:43:53 +03:00
DarkPhoenix
e07c4f65ab Allow to have square brackets in profile name 2019-12-07 02:19:24 +03:00
DarkPhoenix
c3108f773a Use uniform as default resists profile, if no resist profile can be fetched 2019-12-07 01:29:15 +03:00
DarkPhoenix
a400821268 Remove "database defaults" 2019-12-07 01:21:46 +03:00
DarkPhoenix
ca4bac07da Implement switching between user and builtin patterns 2019-12-07 01:17:41 +03:00
DarkPhoenix
0038eacc3f Add database migration to add new columns which are used to refer builting profiles 2019-12-07 00:50:35 +03:00
DarkPhoenix
4bd633a0d4 Process names on pattern objects themselves 2019-12-06 23:24:29 +03:00
DarkPhoenix
ee837f0b04 Add comment about ideal target profile 2019-12-06 20:07:08 +03:00
DarkPhoenix
016e18cc89 Set negative IDs to all hardcoded entities 2019-12-06 20:04:56 +03:00
DarkPhoenix
1d6ac53183 Make target profile adder multi-level 2019-12-06 20:00:53 +03:00
DarkPhoenix
1e32dfa463 Always show target resist switcher 2019-12-06 19:35:20 +03:00
DarkPhoenix
4431753570 Reimplement switcher to use built-ins too and be multi-level 2019-12-06 17:59:44 +03:00
DarkPhoenix
6fdb57318c Expose builtin target profiles to service 2019-12-06 17:14:12 +03:00
DarkPhoenix
649db019de Add builtin fetcher for target profiles 2019-12-06 16:58:58 +03:00
DarkPhoenix
1d528be0ef Add target profile builtins as well 2019-12-06 14:45:46 +03:00
DarkPhoenix
2d6f6df83d Store built-in damage patterns in dictionary rather than in list 2019-12-06 14:25:27 +03:00
DarkPhoenix
7fa998f276 Avoid duplicating names in context menu 2019-12-06 02:59:14 +03:00
DarkPhoenix
6a3157a4c8 Add hardcoded damage patterns to damage pattern definition module 2019-12-06 02:47:11 +03:00
DarkPhoenix
74efa50576 Add multi-level menu support for incoming damage patterns 2019-12-06 02:11:32 +03:00
DarkPhoenix
e99be78c54 Partially rework damage pattern switcher 2019-12-05 20:04:50 +03:00
DarkPhoenix
e386232de1 Gather data on damage patterns into single object 2019-12-05 10:44:26 +03:00
DarkPhoenix
a15896044d Show cap delta detailed info even when fit has no cap use 2019-12-05 10:14:30 +03:00
DarkPhoenix
72c74513ce Add excessive effective cap gain to tooltip 2019-12-04 22:21:25 +03:00
DarkPhoenix
34d6d13cb2 Avoid extra indentation in non-spool version of the tooltip 2019-12-03 20:01:25 +03:00
DarkPhoenix
323bb8e064 Change layout once again 2019-12-03 19:57:40 +03:00
DarkPhoenix
b628f8ef9b Change layout of the tooltip 2019-12-03 19:45:49 +03:00
DarkPhoenix
1900216d36 Show spoolup part before damage distribution 2019-12-03 16:31:21 +03:00
DarkPhoenix
c045ed81c1 Do not show spoolup mark when fit has no spoolup weapons 2019-12-03 16:21:16 +03:00
DarkPhoenix
39edec60e3 Add damage type info to firepower panel tooltips 2019-12-03 16:15:05 +03:00
Gochim
259214e907 Issue #2073: Added popup menu item 2019-12-03 09:23:54 +02:00
DarkPhoenix
2e1c53392d Keep hull energizers online upon addition 2019-12-02 11:41:13 +03:00
DarkPhoenix
7a77d12686 Use built-in curl timeouts instead of coreutils 2019-11-27 16:35:22 +03:00
DarkPhoenix
763a7714ed Use proper timeout on Mac OS 2019-11-27 16:06:31 +03:00
DarkPhoenix
e89f35d654 Do not fail Mac OS build if upload fails 2019-11-27 15:56:57 +03:00
DarkPhoenix
3196751b7a Bump version 2019-11-27 15:11:26 +03:00
DarkPhoenix
f221024974 Update staticdata to 1618828 2019-11-27 15:10:49 +03:00
DarkPhoenix
66cab8a0d4 Bump version 2019-11-26 16:55:15 +03:00
DarkPhoenix
daa49a8cd4 Correctly detect zirnitra as amarr 2019-11-26 16:54:47 +03:00
DarkPhoenix
b8601ff240 Update icons/renders 2019-11-26 16:50:51 +03:00
DarkPhoenix
5df3eeca64 Add zirnitra renders 2019-11-26 16:34:28 +03:00
DarkPhoenix
274e4ac2ca Update effects 2019-11-26 16:27:06 +03:00
DarkPhoenix
f6485251ca Update staticdata to 1617768 2019-11-26 15:00:40 +03:00
DarkPhoenix
c03fb70def Merge branch 'singularity' 2019-11-26 14:55:51 +03:00
DarkPhoenix
8652a2891b Do not attempt to change skill levels of built-in all0/5 chars 2019-11-22 15:46:29 +03:00
DarkPhoenix
a09af0a9eb Propagate keys to upper windows if they were not processed in here 2019-11-22 15:45:01 +03:00
Anton Vorobyov
c7d4b93fba Merge pull request #2094 from RussianCow/level-keyboard-shortcuts
Add keyboard shortcuts to character level editor
2019-11-22 15:01:26 +03:00
Sasha
745c0db530 Add keyboard shortcuts to character level editor 2019-11-21 18:10:56 -08:00
DarkPhoenix
aa56ab8d6c Bump version 2019-11-21 13:01:14 +03:00
DarkPhoenix
043c430221 Update staticdata to 1610407 2019-11-21 13:00:36 +03:00
DarkPhoenix
dc5cc5855e Show range of bursts projectors, taking ship/citadel radius taken into consideration 2019-11-18 15:18:20 +03:00
DarkPhoenix
59d6266e2b Do not crash on exception classes without message attribute 2019-11-17 21:24:41 +03:00
DarkPhoenix
7495ba67f8 Revert change made for debugging 2019-11-17 17:40:07 +03:00
DarkPhoenix
e649683a4d Accept XML headers with extra info 2019-11-17 17:34:49 +03:00
DarkPhoenix
bec58a5772 Do not crash on cargo fetch failures 2019-11-17 17:29:16 +03:00
DarkPhoenix
e8f9ae8a9c Do not use hires assets on wxGTK 2019-11-16 19:22:37 +03:00
DarkPhoenix
679382e220 Do not separate number and unit in range tooltip, for consistency with other fields 2019-11-15 20:19:23 +03:00
DarkPhoenix
7f86782f54 Change range column tooltip for missiles 2019-11-15 13:23:27 +03:00
DarkPhoenix
f80b7d972f Implement hidden flight time bonus 2019-11-15 12:59:57 +03:00
Anton Vorobyov
c0bd489c1b Merge pull request #2085 from Gochim/master
Possible fix for #2084
2019-11-14 12:38:08 +03:00
Gochim
41e4c2107d Fix for #2084 2019-11-14 11:36:59 +02:00
Gochim
cfc95c272a Possible fix for #2084 2019-11-14 10:35:16 +02:00
DarkPhoenix
f778f9ceae Take ship radius in consideration when displaying missile range in range column 2019-11-13 20:51:14 +03:00
DarkPhoenix
7fb6170bcb Implement missile "falloff" support in graphs 2019-11-13 20:28:52 +03:00
DarkPhoenix
fa37428cd3 Update staticdata to 1573560935 2019-11-12 15:39:57 +03:00
DarkPhoenix
2a2d9d3456 Fix circular import issues for tests 2019-11-12 14:59:43 +03:00
Anton Vorobyov
a91efb681f Merge pull request #2079 from Gochim/minor_fixes
Several minor fixes in different parts of code
2019-11-12 00:07:31 +03:00
Gochim
386e05be8f Fixed method naming in stats.py classes 2019-11-11 19:19:31 +02:00
Anton Vorobyov
65e7bf609d Merge pull request #2078 from Gochim/master
Added central place to get damage types and ehp sources. Added tests
2019-11-11 16:40:22 +03:00
Gochim
7a58d97652 Merge remote-tracking branch 'origin/minor_fixes' into minor_fixes
# Conflicts:
#	eos/utils/stats.py
2019-11-10 17:36:47 +02:00
Gochim
c5118da417 Fixed small issue with #2078 by removing unnecessary prints 2019-11-10 17:35:48 +02:00
Gochim
13b505525d Refactored shipstats.py to use common damage profile and hull type names. Reduced code complexity 2019-11-10 17:27:46 +02:00
Gochim
b682dec363 Second fix for #2076 - use Abstract collections from .abc module. Fixes future issue 2019-11-10 17:27:46 +02:00
Gochim
6aa98e2214 Fixed test_aboutData test 2019-11-10 17:27:46 +02:00
Gochim
8fba988222 Added central place to get damage types and ehp sources. Added tests 2019-11-10 17:27:46 +02:00
Gochim
24a82efe50 Refactored shipstats.py to use common damage profile and hull type names. Reduced code complexity 2019-11-10 17:27:07 +02:00
Gochim
8054fa9267 Second fix for #2076 - use Abstract collections from .abc module. Fixes future issue 2019-11-09 23:27:28 +02:00
Gochim
a0e39a3725 Fixed test_aboutData test 2019-11-09 22:53:52 +02:00
Gochim
185d6d0c51 Merge remote-tracking branch 'origin/master' 2019-11-08 08:35:29 +02:00
Gochim
1975e96848 Added central place to get damage types and ehp sources. Added tests 2019-11-08 08:34:45 +02:00
Gochim
ab37d228ea Added central place to get damage types and ehp sources. Added tests 2019-11-08 08:34:22 +02:00
DarkPhoenix
f48483d754 Merge branch 'master' of github.com:pyfa-org/Pyfa 2019-11-07 18:36:10 +03:00
DarkPhoenix
e6cfd33435 Fix #2076 - use collection ABCs from .abc module 2019-11-07 18:35:14 +03:00
Anton Vorobyov
c29126ce1d Merge pull request #2077 from Gochim/master
Added instructions to run tests for the project
2019-11-07 18:34:58 +03:00
Gochim
c52170b731 Fixed path for bash script call 2019-11-07 14:45:29 +02:00
Gochim
6607dd31bf Merge remote-tracking branch 'origin/master' 2019-11-07 14:27:36 +02:00
Gochim
c6c74be38d Added instructions to run tests for the project 2019-11-07 14:27:14 +02:00
Gochim
41c6062ff9 Added instructions to run tests for the project 2019-11-07 14:26:41 +02:00
DarkPhoenix
bbb96a73b7 Add alias file for trig dread-related items 2019-11-06 23:09:28 +03:00
DarkPhoenix
9eb3b9e017 Do not use evepraisal info unless there are orders up 2019-11-04 02:50:55 +03:00
DarkPhoenix
6b3e94729c Update staticdata and bump version 2019-10-31 15:05:14 +03:00
DarkPhoenix
fb48f2b5d4 Do not attempt to add entries to name maps if entity fetch failed 2019-10-30 16:46:34 +03:00
DarkPhoenix
cfffc77777 Change format name 2019-10-30 16:13:15 +03:00
DarkPhoenix
f7089f358d Fix stats export parenthesis 2019-10-30 16:12:33 +03:00
DarkPhoenix
06c4f2ce46 Bump version 2019-10-30 16:09:08 +03:00
Anton Vorobyov
83eb0abd92 Merge pull request #2070 from pyfa-org/json_in_repo
Store text staticdata in repo instead of binary
2019-10-30 16:05:55 +03:00
DarkPhoenix
4199b33c47 Merge branch 'master' into json_in_repo 2019-10-30 15:47:47 +03:00
Anton Vorobyov
23cd4bff5a Merge pull request #2069 from Gochim/master
[Updated] Implemented copying the currently open fit stats to the clipboard.
2019-10-30 15:44:53 +03:00
DarkPhoenix
b65f95fe77 Make sure to avoid doing DB updates avoiding sqlalchemy, as we're re-using the same session for pyfa now 2019-10-30 15:39:21 +03:00
DarkPhoenix
32160c94e1 Add extra metadata field which we use during gamedata DB checks 2019-10-30 15:26:26 +03:00
DarkPhoenix
ac02fba98b Move useless category IDs closer to context 2019-10-30 15:18:08 +03:00
DarkPhoenix
cde0108cba Change logging a little so info about DB being rebuilt is always printed to stdout 2019-10-30 15:16:13 +03:00
DarkPhoenix
39dc7e4a46 Compose DB out of data stored externally 2019-10-30 14:56:51 +03:00
Gochim
9943f784a8 Fixed code auto-checks for pull request 2019-10-30 13:34:54 +02:00
Gochim
88ce45f29e Merge remote-tracking branch 'origin/master' 2019-10-30 11:46:25 +02:00
Gochim
7157e876ca Fixed issue with mainFrame after merging 2019-10-30 11:46:03 +02:00
Gochim
0cf88cf7ca Added stats that were more or less agreed on in [Issue #2065] 2019-10-30 11:39:21 +02:00
Gochim
10dfdc3627 Added UI for new type of copying data about fit to the clipboard 2019-10-30 11:39:21 +02:00
Gochim
76bdefcda6 Fixed wording in contributing.md 2019-10-30 11:39:21 +02:00
Gochim
1c2c8cc5f9 Added UI for new type of copying data about fit to the clipboard 2019-10-30 11:39:21 +02:00
Alexander Maryanovsky
58f853de5b Implemented copying the currently open fit stats to the clipboard. 2019-10-30 11:39:21 +02:00
Gochim
c052297bf7 Added stats that were more or less agreed on in [Issue #2065] 2019-10-30 09:17:38 +02:00
DarkPhoenix
9e78cd1076 Fix drag-n-dropping module from market into specific empty slot 2019-10-28 13:23:40 +03:00
DarkPhoenix
79f4deacea Show hidden graphs on ctrl-alt-g 2019-10-28 12:56:34 +03:00
Gochim
ff42c4c711 Added UI for new type of copying data about fit to the clipboard 2019-10-26 18:36:38 +03:00
DarkPhoenix
02d31d49d8 Implement graph types to pick best ECM burst + damps ship 2019-10-26 00:30:45 +03:00
DarkPhoenix
64f47fcc24 Do not choke on fits for unknown ships 2019-10-25 01:00:32 +03:00
DarkPhoenix
0ceb8acd64 Rename some fields for the hidden graph 2019-10-24 23:15:38 +03:00
DarkPhoenix
78579e2e13 Adjust ECM burst + damp graph 2019-10-24 23:13:20 +03:00
DarkPhoenix
cf4e1d3935 Consistency fixes 2019-10-24 14:09:08 +03:00
DarkPhoenix
d1be0bb680 Add scanres vs locktime on active fit graph as experimental feature 2019-10-24 13:52:34 +03:00
DarkPhoenix
e70ebad3f7 Bump version 2019-10-22 15:37:25 +03:00
DarkPhoenix
4dfe5c3abf Update database and effects to 1589866 2019-10-22 15:36:37 +03:00
Gochim
384d9f4614 Fixed wording in contributing.md 2019-10-21 13:22:00 +03:00
Gochim
47434c68f9 Added UI for new type of copying data about fit to the clipboard 2019-10-20 15:25:06 +03:00
Gochim
af88afb6b5 Merge branch 'export-stats' of https://github.com/m-sasha/PyfaAT into m-sasha-export-stats
# Conflicts:
#	gui/mainFrame.py
#	gui/mainMenuBar.py
2019-10-20 14:48:00 +03:00
Anton Vorobyov
536eb1efa5 Merge pull request #2064 from Gochim/master
Merged some additional info from wiki. Improved on readability
2019-10-17 17:19:10 +03:00
Anton Vorobyov
c4c763089e Merge branch 'master' into master 2019-10-17 17:19:00 +03:00
DarkPhoenix
cdfd4c0d8e Invoke pip the same way as on Windows 2019-10-17 17:01:10 +03:00
DarkPhoenix
f9bb8836e5 Upgrade pip when building mac distribution 2019-10-17 16:59:38 +03:00
Gochim
58b2634c8c Merged info from wiki. Improved on readability 2019-10-17 16:50:40 +03:00
Anton Vorobyov
093ae008ce Merge pull request #2063 from Gochim/master
Re-written in much greater detail instructions how to set up the project
2019-10-17 16:05:31 +03:00
Gochim
5f62fc0cdc Merge remote-tracking branch 'origin/master' 2019-10-16 22:08:33 +03:00
Gochim
e7a4b4ac26 Added section "Setting up the project with PyCharm" 2019-10-16 22:07:10 +03:00
Gochim
66e9944cb5 Added "Setting up the project manually" instructions 2019-10-16 22:07:10 +03:00
DIvanchenko
ad8528c248 Plan for CONTRIBUTING.md. Updated link to CONTRIBUTION.md. 2019-10-16 22:06:19 +03:00
DIvanchenko
07d22cd8e4 Created link to CONTRIBUTION.md. Fixed heading 2019-10-16 22:06:19 +03:00
DarkPhoenix
a6d5922d77 Bump minor version, as database has been updated 2019-10-16 16:33:43 +03:00
Ryan Holmes
958d7bff99 Update .appveyor.yml 2019-10-16 09:27:22 -04:00
Ryan Holmes
7819b80be4 Update .appveyor.yml 2019-10-16 09:25:14 -04:00
Gochim
2ca50a4658 Added section "Setting up the project with PyCharm" 2019-10-15 23:08:21 +03:00
Gochim
09ff4fd128 Added "Setting up the project manually" instructions 2019-10-15 20:17:48 +03:00
DarkPhoenix
3e53863f9e Bump version 2019-10-15 17:09:15 +03:00
DarkPhoenix
63d2289f97 Update database to 1585794 2019-10-15 17:06:42 +03:00
DarkPhoenix
2663ef2e66 Make mutated module import consider old 1mn gravid mutaplasmid name 2019-10-14 18:26:22 +03:00
DIvanchenko
d4bdf47d62 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2019-10-10 18:04:24 +03:00
DIvanchenko
660ee7c4bf Plan for CONTRIBUTING.md. Updated link to CONTRIBUTION.md. 2019-10-10 18:03:28 +03:00
DIvanchenko
1db1f3070b Created link to CONTRIBUTION.md. Fixed heading 2019-10-10 10:50:56 +03:00
DIvanchenko
3dba82c497 Created link to CONTRIBUTION.md. Fixed heading 2019-10-10 10:49:54 +03:00
DarkPhoenix
25e7b7a9f7 Fix RMB in graph fit list 2019-10-09 23:09:09 +03:00
DarkPhoenix
9582212ae0 Bump version 2019-10-09 19:38:25 +03:00
DarkPhoenix
7d2b60c327 Fix addition of drones via alt-dclick 2019-10-09 16:24:21 +03:00
DarkPhoenix
0121a0064e Show unknown meta groups as normal, also add standup faction group to faction tab 2019-10-09 14:38:08 +03:00
DarkPhoenix
2aa96fc819 Do not crash when user cancels setting of projection range 2019-10-09 13:30:01 +03:00
DarkPhoenix
8d81db0a3a Change what kind of strength is displayed for probe launchers 2019-10-09 13:13:40 +03:00
DarkPhoenix
e5ba35fde9 Pass effect in all effect handler calls 2019-10-08 23:41:26 +03:00
DarkPhoenix
885cd32cb0 Fix EFS export after adding projection range support 2019-10-08 23:11:58 +03:00
DarkPhoenix
18d8ed6558 Take modifier key info from event where possible 2019-10-08 20:50:14 +03:00
DarkPhoenix
9618ece4b4 Bump version 2019-10-08 19:50:57 +03:00
DarkPhoenix
a80a77a422 Update effects file 2019-10-08 19:50:38 +03:00
DarkPhoenix
3806be3ddd Add capsule renders 2019-10-08 19:46:15 +03:00
DarkPhoenix
3e803fef30 Actually expose capsule to a shuttle group 2019-10-08 19:37:32 +03:00
DarkPhoenix
12956d435a Update database to 1581357 and do necessary schema changes to support changed source data 2019-10-08 19:28:24 +03:00
DarkPhoenix
a3381007f3 Use numbers in default values instead of strings to avoid type errors 2019-10-07 16:58:11 +03:00
DarkPhoenix
1efe4ee5e5 Enable showing capsules in shuttle group 2019-10-05 10:27:08 +03:00
DarkPhoenix
ec21f93d3c Fix background color of panels behind buttons 2019-10-04 01:25:38 +03:00
DarkPhoenix
f384b32ed6 Remove unused file 2019-10-03 14:30:50 +03:00
DarkPhoenix
22d8f34c75 Fix pref panel titles on linux 2019-10-03 14:29:47 +03:00
DarkPhoenix
6128cd8322 Update once again
Cut everything outside of window
2019-10-03 13:34:42 +03:00
DarkPhoenix
386f403430 Update screenshot 2019-10-03 13:32:36 +03:00
DarkPhoenix
5f7d9aea89 Add amount of text in notes to notes tab title 2019-10-03 13:02:08 +03:00
DarkPhoenix
b367c449a9 Few more readme fixes 2019-10-03 09:56:53 +03:00
DarkPhoenix
26b3dff9d4 Update readme 2019-10-03 09:47:43 +03:00
DarkPhoenix
873a62e3f0 Make error dialog window like any other auxiliary window - do not keep it on top 2019-10-02 22:08:37 +03:00
Anton Vorobyov
d967ab375e Merge pull request #2050 from ZeroPointEnergy/feature/images_from_zip
Support loading images from zip
2019-10-02 19:16:50 +03:00
Andreas Zuber
fcf2d6a72c Support loading images from zip
This commit reenables the ability to load the images from a zip file
instead of a directory structure. It is possible to set the location of
this file via configforced.py
2019-10-02 17:01:14 +02:00
DarkPhoenix
843ced15bf Fix ammo damage ratios 2019-10-02 14:45:29 +03:00
DarkPhoenix
813db9340f Make sure to select right-clicked item on windows when Control is pressed 2019-10-02 13:19:46 +03:00
DarkPhoenix
acbd8a3298 Allow context menu-related batch actions to be triggered on ctrl too
Windows hides context menu when user presses alt
2019-10-02 12:44:27 +03:00
DarkPhoenix
561e22e894 Bump version 2019-10-02 12:11:25 +03:00
DarkPhoenix
05ac0a528a Show extra labels only when there's something inside 2019-10-02 10:56:01 +03:00
DarkPhoenix
c040353f6e Move some common functionality into common space of tab sizing method 2019-10-02 10:37:14 +03:00
DarkPhoenix
f23a8fa0c8 Adjust tab shadows to tab sizes 2019-10-02 10:33:11 +03:00
DarkPhoenix
ba93467646 Implement logic to have tabs of different sizes in additions pane and in fitting pane 2019-10-02 10:16:36 +03:00
DarkPhoenix
00d480860f Change tab outlook when option value is changed 2019-10-01 22:28:24 +03:00
DarkPhoenix
c94384acb8 Show amount of items hidden in additions tabs 2019-10-01 22:22:10 +03:00
DarkPhoenix
0c2c0ac6ef Add modules to session even when they were added via appendIgnoreEmpty 2019-10-01 10:25:31 +03:00
DarkPhoenix
61a33a331e Copy projection range when copying fit 2019-10-01 09:29:10 +03:00
DarkPhoenix
e374a6f2c6 Do not add null drains to not affect cap sim calculations 2019-09-30 17:27:10 +03:00
DarkPhoenix
dbd84dce28 Allow to change projected items' metas regardless of ability of target ship to fit them 2019-09-30 17:24:14 +03:00
DarkPhoenix
9d554f9c68 Make sure to do fit recalculation if it was changed after the last one 2019-09-30 17:20:22 +03:00
DarkPhoenix
576cf56735 Expose chance of jamming to stats pane itself rather than tooltip 2019-09-30 17:06:01 +03:00
DarkPhoenix
e2aaabbc16 Do not let jamming strength exceed 100% 2019-09-30 16:40:48 +03:00
DarkPhoenix
ef226898c0 Add projection range calculation to effects where it makes sense 2019-09-30 14:43:52 +03:00
DarkPhoenix
a0db235e5a Add support for projection range to bunch of effects 2019-09-30 03:17:21 +03:00
DarkPhoenix
5bf05ba775 Allow batch changes of projection range 2019-09-30 02:47:12 +03:00
DarkPhoenix
c073b1fa2a Do not show context menu on system-wide effects 2019-09-30 02:10:11 +03:00
DarkPhoenix
5f58307bf3 Add projection range commands to projected fighters 2019-09-30 02:04:24 +03:00
DarkPhoenix
8741b17a5e Add projection range commands for projected drones 2019-09-30 01:55:16 +03:00
DarkPhoenix
4c1fa09795 Apply drones and fighters from projected fit at range of 0 2019-09-30 01:21:39 +03:00
DarkPhoenix
ce7df2d01f Allow to change projection range for projected modules 2019-09-30 01:18:55 +03:00
DarkPhoenix
b433b0ea7c Change fit projection so that projection range actually counts for the sake of calculations overall
No effect support still
2019-09-30 00:19:31 +03:00
DarkPhoenix
20868d6b44 Add ability to change projection range of fits 2019-09-29 23:41:45 +03:00
DarkPhoenix
33103dbee9 Add column which shows projected item range 2019-09-29 22:16:19 +03:00
DarkPhoenix
2a05ac5a85 Pass projection range parameter to effects 2019-09-29 22:02:10 +03:00
DarkPhoenix
a013828128 Add projectionRange to actual objects built from database 2019-09-29 16:04:44 +03:00
DarkPhoenix
e19510b3d4 Move function which calculates range factor to eos 2019-09-29 16:00:37 +03:00
DarkPhoenix
390f2048f2 Add projection range column to projectable entities 2019-09-29 15:54:45 +03:00
DarkPhoenix
0bb732300e Do not rely on resistance view being available 2019-09-29 11:21:00 +03:00
DarkPhoenix
fd017df561 Add lock range limit support to ewar graph 2019-09-27 20:43:28 +03:00
DarkPhoenix
0ed16b9a6f Add lockrange support to DPS graphs 2019-09-27 20:19:29 +03:00
DarkPhoenix
865978fcc1 Add context menu which controls if graphs ignore drone control range or not, and add support for this option to RR graph 2019-09-27 18:40:33 +03:00
DarkPhoenix
a43f9930de Allow to change meta level of standup fighters 2019-09-23 16:44:26 +03:00
DarkPhoenix
c13cd23d54 Change parent of fit deletion dialog
Try out tip mentioned in https://github.com/wxWidgets/Phoenix/issues/1343
2019-09-23 15:32:16 +03:00
DarkPhoenix
ed1f52a114 Show implant description in tooltip of implant editor 2019-09-23 15:25:58 +03:00
DarkPhoenix
7dd063f04e Add graph setting to ignore drone control range 2019-09-17 13:03:21 +03:00
DarkPhoenix
6e9fc1d1d9 Do not crash on Nones in value 2019-09-11 08:32:13 +03:00
DarkPhoenix
cae0172e48 Bump version 2019-09-10 15:54:23 +03:00
DarkPhoenix
e2b492ee8d Update database and effects to 1564394 2019-09-10 15:53:37 +03:00
DarkPhoenix
545ddc7492 Adjust DB script to changes in phobos 2019-09-10 15:31:02 +03:00
DarkPhoenix
d0b7c58a1d Merge branch 'singularity' 2019-09-10 14:30:51 +03:00
DarkPhoenix
a9ad094422 Fix fax link amount bonus 2019-09-04 15:10:07 +03:00
DarkPhoenix
68154333c2 Merge branch 'master' into singularity 2019-09-02 02:25:25 +03:00
DarkPhoenix
5df2db5879 Bump version 2019-09-02 01:21:32 +03:00
DarkPhoenix
5a34db0d2f Change how fit deletion confirmation dialog is destroyed 2019-09-02 01:14:35 +03:00
DarkPhoenix
6f50be1e7e Fix another set of crashes with manual login 2019-08-30 15:36:34 +03:00
DarkPhoenix
d15fefcf1b Avoid various crashes when working with SslLoginServer dialog 2019-08-30 15:26:58 +03:00
DarkPhoenix
07bf1b400c Fix spool scale mode on the very final cycle with spoolup set 2019-08-30 09:42:26 +03:00
DarkPhoenix
9f975a958e Merge branch 'master' into singularity 2019-08-27 11:34:17 +03:00
DarkPhoenix
c2a240bab0 Fix mutadaptive rep group 2019-08-26 23:12:44 +03:00
DarkPhoenix
40c3bf723f Make drone RR rigs stacking penalized for shield RR bots 2019-08-26 19:43:44 +03:00
DarkPhoenix
7a92ace2db Remove stacking penalty from scorpion ECM strength bonus 2019-08-26 19:33:55 +03:00
DarkPhoenix
500f5b8310 Add redraw delay on fit changes 2019-08-26 14:47:34 +03:00
DarkPhoenix
44830a4de6 Add capacity to container name if it was added to cargo 2019-08-26 14:13:26 +03:00
DarkPhoenix
f3f13e7ba8 Make cargo containers searchable 2019-08-26 13:38:03 +03:00
DarkPhoenix
0269a64ae1 Add maximize button to resizeable windows and make character editor resizeable 2019-08-26 12:59:00 +03:00
DarkPhoenix
5d6cdcbd23 Fix indent 2019-08-26 12:28:55 +03:00
DarkPhoenix
81906a7bd2 Do not store item-specific resistance attrs on effects 2019-08-26 12:27:43 +03:00
DarkPhoenix
b25b038934 Fix on-effect resistance definition 2019-08-26 09:28:48 +03:00
DarkPhoenix
b469fa520e Do not crash on cap boosters only in fit 2019-08-26 09:02:55 +03:00
DarkPhoenix
4f865896c7 Force variations menu to respect meta group overrides 2019-08-26 04:01:23 +03:00
DarkPhoenix
3b50dddef2 Allow to online extra amount of command bursts on various caps 2019-08-26 03:48:17 +03:00
DarkPhoenix
380e9c2e87 Change how rounding on Y ticks happens - now it relies on shown Y range 2019-08-26 03:35:35 +03:00
DarkPhoenix
1c1443c862 Move calculation of normalization shift to separate function 2019-08-26 02:56:11 +03:00
DarkPhoenix
0529baac4a Merge branch 'master' into singularity 2019-08-23 13:44:40 +03:00
DarkPhoenix
7dab220009 Ignore non-active scrams and scrammables 2019-08-23 13:44:15 +03:00
DarkPhoenix
0ea0f8cdf2 Merge branch 'master' into singularity 2019-08-23 13:24:29 +03:00
DarkPhoenix
eebd59413b Apply scrams in DPS graph when projected mods is enabled 2019-08-23 13:19:17 +03:00
DarkPhoenix
f4a635eb43 Implement hybrid extended attribute getter and few scram-related functions for DPS graph 2019-08-23 11:53:13 +03:00
DarkPhoenix
0e57258cc5 Add ability to pass multiple afflictors to no-afflictor getter 2019-08-23 09:13:40 +03:00
DarkPhoenix
67462c3278 Do not crash on attempt to export blank fitting 2019-08-23 09:07:31 +03:00
DarkPhoenix
fce8129fa2 Add support for extended target profile stats export/import 2019-08-23 08:55:13 +03:00
DarkPhoenix
1d76f3ec31 Merge branch 'master' into singularity 2019-08-22 21:32:34 +03:00
DarkPhoenix
707dbeecf8 Fix subsystems giving fitting bonuses to non-med RRs 2019-08-22 21:30:10 +03:00
Alexander Maryanovsky
b2c718d614 Implemented copying the currently open fit stats to the clipboard. 2018-09-08 20:14:34 +03:00
3579 changed files with 3208744 additions and 10551 deletions

View File

@@ -1,124 +1,78 @@
image: Visual Studio 2019
clone_depth: 400
environment:
global:
# SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the
# /E:ON and /V:ON options are not enabled in the batch script intepreter
# See: http://stackoverflow.com/a/13751649/163740
CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_env.cmd"
matrix:
- PYTHON: "C:\\Python36"
PYTHON_VERSION: "3.6.x"
PYTHON_ARCH: "32"
init:
- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
- PYTHON: "C:\\Python37-x64"
# Should be enabled only for build process debugging
# init:
# - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
install:
# If there is a newer build queued for the same PR, cancel this one.
# The AppVeyor 'rollout builds' option is supposed to serve the same
# purpose but it is problematic because it tends to cancel builds pushed
# directly to master instead of just PR builds (or the converse).
# credits: JuliaLang developers.
- ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod `
https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | `
Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { `
throw "There are newer queued builds for this pull request, failing early." }
- ps: echo("OS version:")
- ps: "[System.Environment]::OSVersion.Version"
- ECHO "Filesystem root:"
- ps: "ls \"C:/\""
- ps: echo("Filesystem - root:")
- ps: "ls \"C:\\\""
- ECHO "Filesystem projects root:"
- ps: echo("Filesystem - projects root:")
- ps: "ls \"C:\\projects\\\""
- ECHO "Filesystem pyfa root:"
- ps: "ls \"C:\\projects\\$env:APPVEYOR_PROJECT_SLUG\""
- ps: echo("Filesystem - pyfa root:")
- ps: "ls \"C:\\projects\\$env:APPVEYOR_PROJECT_SLUG\\\""
- ECHO "Installed SDKs:"
- ps: "ls \"C:/Program Files/Microsoft SDKs/Windows\""
- ps: echo("Filesystem - installed SDKs:")
- ps: "ls \"C:\\Program Files (x86)\\Windows Kits\\\""
# Prepend newly installed Python to the PATH of this build (this cannot be
# done from inside the powershell script as it would require to restart
# the parent CMD process).
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
- cmd: "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
- "python --version"
- "python -c \"import struct; print(struct.calcsize('P') * 8)\""
- cmd: "python --version"
- cmd: "python -c \"import struct; print(struct.calcsize('P') * 8)\""
# Upgrade to the latest version of pip to avoid it displaying warnings
# about it being out of date.
- "pip install --disable-pip-version-check --user --upgrade pip"
- cmd: "python -m pip install --upgrade pip"
# Install the build dependencies of the project. If some dependencies contain
# compiled extensions and are not provided as pre-built wheel packages,
# pip will build them from source using the MSVC compiler matching the
# target Python version and architecture
- ECHO "Install pip requirements:"
- "pip install -r requirements.txt"
- "pip install PyInstaller"
- ps: echo("Install pip requirements:")
# This one is needed to build wxpython 4.0.6 on windows
- cmd: "python -m pip install pathlib2"
- cmd: "python -m pip install -r requirements.txt"
- cmd: "python -m pip install PyInstaller==3.6"
before_build:
# directory that will contain the built files
- ps: $env:PYFA_DIST_DIR = "c:\projects\$env:APPVEYOR_PROJECT_SLUG\dist"
- ps: $env:PYFA_VERSION = (python ./scripts/dump_version.py)
- ps: echo("pyfa version ")
- ps: echo ($env:PYFA_VERSION)
- ps: echo("pyfa version $env:PYFA_VERSION")
build_script:
- ECHO "Build pyfa:"
##########
# PyInstaller - create binaries for pyfa
##########
- ps: echo("Build pyfa:")
# Build gamedata DB
- cmd: "python db_update.py"
# Build command for PyInstaller
- "python -m PyInstaller --noupx --clean --windowed --noconsole -y pyfa.spec"
- cmd: "python -m PyInstaller --noupx --clean --windowed --noconsole -y pyfa.spec"
# Copy over manifest (See pyfa-org/pyfa#1622)
- ps: xcopy /y dist_assets\win\pyfa.exe.manifest $env:PYFA_DIST_DIR\pyfa\
# Not really sure if this is needed, but why not
- ps: xcopy /y dist_assets\win\Microsoft.VC90.CRT.manifest $env:PYFA_DIST_DIR\pyfa\
##########
# InnoScript EXE building
# This is in a separate script because I don't feel like copying over the logic to AppVeyor script right now...
##########
- "python dist_assets/win/dist.py"
# InnoScript EXE building. This is in a separate script because I don't feel like copying over the logic to AppVeyor script right now...
- cmd: "python dist_assets/win/dist.py"
- ps: dir $env:PYFA_DIST_DIR/
#- ECHO "Build pyfa (Debug):"
#- copy C:\projects\pyfa\dist_assets\win\pyfa_debug.spec C:\projects\pyfa\pyfa_debug.spec
#- "pyinstaller.exe --clean --noconfirm --windowed --upx-dir=C:\\projects\\pyfa\\scripts\\upx.exe C:\\projects\\pyfa\\pyfa_debug.spec"
build: on
after_build:
- ps: "ls \"./\""
#- ps: "ls \"C:\\projects\\pyfa\\build\\pyfa\\\""
# - ps: "ls \"C:\\projects\\$env:APPVEYOR_PROJECT_SLUG\\build\\exe.win32-2.7\\\""
# Zip
# APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER
#- 7z a build.zip -r C:\projects\pyfa\build\pyfa\*.*
- ps: 7z a "pyfa-$env:PYFA_VERSION-win.zip" -r "$env:PYFA_DIST_DIR\pyfa\*.*"
#- 7z a pyfa_debug.zip -r C:\projects\pyfa\dist\pyfa_debug\*.*
on_success:
# Do nothing right now
- ps: 7z a "pyfa-$env:PYFA_VERSION-win.zip" -r "$env:PYFA_DIST_DIR\pyfa\*"
test_script:
#- tox
#- "py.test --cov=./"
# Run the project tests
# - "%CMD_IN_ENV% python C:/projects/eve-gnosis/setup.py nosetests"
after_test:
# If tests are successful, create binary packages for the project.
# - "%CMD_IN_ENV% python setup.py bdist_wheel"
# - "%CMD_IN_ENV% python setup.py bdist_wininst"
# - "%CMD_IN_ENV% python setup.py bdist_msi"
# - ps: "ls dist"
# Ha... we're just building
artifacts:
# Archive the generated packages in the ci.appveyor.com build report.
- path: pyfa*-win.zip
- path: pyfa*-win.exe
#- path: pyfa_debug.zip
# name: Pyfa_debug
deploy:
tag: $(pyfa_version)
@@ -126,10 +80,9 @@ deploy:
description: 'Release description'
provider: GitHub
auth_token:
secure: BfNHO66ff5hVx2O2ORbl49X0U/5h2V2T0IuRZDwm7fd1HvsVluF0wRCbl29oRp1M
secure: X+U3hOAMTt7HGXCR/LXaGNF6qyhUXetrjz5+xlWiNJQ3XEdzhZZmHK75m0Hm6qre
draft: true
force_update: false
# deploy on tag push only
on:
APPVEYOR_REPO_TAG: true # deploy on tag push only
#on_success:
# - TODO: upload the content of dist/*.whl to a public wheelhouse
#
APPVEYOR_REPO_TAG: true

View File

@@ -3,16 +3,16 @@ language: python
git:
depth: 400
python:
- 3.6
- 3.8
matrix:
include:
- os: osx
osx_image: xcode7.3
osx_image: xcode11.3
language: generic
env: PYTHON=3.6.1
before_install:
- bash scripts/setup-osx.sh
install:
- python3 db_update.py
- export PYFA_VERSION="$(python3 scripts/dump_version.py)"
- bash scripts/package-osx.sh
before_deploy:
@@ -21,7 +21,7 @@ before_deploy:
deploy:
provider: releases
api_key:
secure: Xfu0xApoB0zUPLXl29aYUulVC3iA4/3bXQwwADKCfAKZwxgNon4dLbO7Rie5/7Ukf2POL0KwmRaQGN3kOr+XSoIVTE4M5sXxnhiaaLGKQ+48hDizLE6JuXcZGJvkxUaghaTzIdCwHsG7VGBsPfQgfGsjJcfBp8tFNLmRyM/Jpsr8T6BR2MxtBIEUVy8zrOWFNZqnmWrY2pWMsB9fYt3JFNdpqeIgRAYqbBsBcZQ1MngLTi3ztuYS5IaF+lk06RrnBlHmUsJu/5nCvIpvPvD0i2BLZ3Uu0+Fn+8QWUgjJEL9MNseXZMXynu05xd8YRk7Ajc9CUrzQIIbAktyteYp85kE3pUJHmrMLcXhh7nqkwttR5/47Zwa3OLJLJFKBxMx6wY5jFkJjkV08850B7aWrmTFl/Eqc3Q5nZMuiEt3wFRbjxHi9h1mTN/fkxfRRHg8u3ENGPR+ZPiFC3J18qtks/B/hsKjjHvZP1i79OYlET4V/zyLyyQkCbpDaARQANuotLYJyZ7tH+KWEyRsvTi0M9Yev9mNNw6aI4vzh4HfkEhvcvnWnYwckPj1dnjQ573Qpw0Z9wsconoWfHAn+hBDt3+YLMrrFZl++mCRskHH1mZChX3aGMDi49zD0kfxBUkYPOAhguc6PwudBxHUZP+O6T/SoHylff6EizCE/k5dGeAk=
secure: D8tBW0kyHlKf/sXS69aIuexsYTx9auY2DzudKFlfcvdzqat4N2XZqZbZCTVd7YVvptQ8Dj0oZ/p3KUxEGpnJZmlTeJL142rpM/qaNd6wOIMy2yUde/aZl+W9JLFNQp7KHutM+MxObYLzJGihx/8YsupmFx6lxgdngGDXtXYZe/ruDIWDs92ShoKJ4vlce9Csm7eGKv7wv6Z6V9sD5FS3E9J8xdWStHxsbrkPBOflmG+uHU09dpEqzUm+ZYROIoTwig1Xbw3fw+gfjmNrfdSU4fAJcVZI1hrgoenZyJbMfhI2Ej/nZdbZgaXcZNF/eUpqOGgbPe1JljqFnHTbexcE+LPBVyAToScsGMpByHhig67DrZ0nk9gSZoC6CPNl5YS6xub+5dncMJ3P5L03DOGYRu4SL9NczbeuQyKuea7+JPP/8VLwfFDSEqbNEAmgzABAzrdfano+VXtuBuE/Tiy5eE7le9hJu6aSQoKW1SA3cUhMsmr2amzdO96sh+PN8FA1oNr45Yuy0pqOj4SUIkb8JUy4th7vgdhljEkSxrHDK1UcHpxUTp+IIUZkZVVk50aH68dQZxGwSTVOeRxpjrTcEf7VCGaM98qxi/ZK4RW6Ewiq0eo0AxwEeB2Zm841lycGPR/406vM9/ZBzv5IhELIdDdVWTk+dGjJBXB8z5hPJOg=
file_glob: true
file: "dist/pyfa-*.zip"
skip_cleanup: true

102
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,102 @@
# Contribution
## Requirements
- Python 3.6
- Git CLI installed
- Python, pip and git are all available as command-line commands (add to the path if needed)
Virtual environment will be created in *PyfaEnv* folder. Project will be cloned and run from the *PyfaDEV* folder. Separate virtual environment will be created so required libraries won't clutter the main python installation.
> Commands and screens were created on Windows 10. Please, update all the paths according to your OS.
## Setting up the project manually
Clone the repository
```
git clone <repo> PyfaDEV
```
Create the virtual environment
```
python -m venv PyfaEnv
```
Activate the virtual environment
```
For cmd.exe: PyfaEnv\scripts\activate.bat
For bash: source <venv>/Scripts/activate
```
> For other OS check [Python documentation](https://docs.python.org/3/library/venv.html)
Install requirements for the project from *requirements.txt*
```
pip install -r PyfaDEV\requirements.txt
```
> For some Linux distributions, you may need to install separate wxPython bindings, such as `python-matplotlib-wx`
Check that the libs from *requirements.txt* are installed
```
pip list
```
Test that the project is starting properly
```
python PyfaDEV\pyfa.py
```
## Setting up the project with PyCharm/IntelliJ
Install PyCharm / Other IntelliJ product with Python plugin
After launching - select *Check out from Version Control* -> *GIt*
![welcome](https://user-images.githubusercontent.com/54093496/66862580-d8edab00-ef99-11e9-94e2-e93d7043e620.png)
Login to GitHub, paste the repo URL and select the folder to which to clone the project into, press *Clone*.
![Clone](https://user-images.githubusercontent.com/54093496/66862748-38e45180-ef9a-11e9-9f68-4903baf47385.png)
After process is complete, open *File* -> *Settings* -> *Project* -> *Project Interpreter*.
![Settings](https://user-images.githubusercontent.com/54093496/66862792-544f5c80-ef9a-11e9-9e0f-f64767f3f1b0.png)
Press on options and add new virtual environment.
![venv](https://user-images.githubusercontent.com/54093496/66862833-67622c80-ef9a-11e9-94fa-47cca0158d29.png)
Open project tree view and double-click on the *requirements.txt*. Press *Install requirements*. Install all requirements.
![Reqs](https://user-images.githubusercontent.com/54093496/66862870-7a74fc80-ef9a-11e9-9b18-e64be42c49b8.png)
Create new *Run Configuration*. Set correct *Script path* and *Python interpreter*.
![Run configuraion](https://user-images.githubusercontent.com/54093496/66862970-b4460300-ef9a-11e9-9fb4-20e24759904b.png)
Check that the project is starting properly.
## Running tests
Switch to the proper virtual environment
```
For cmd.exe: PyfaEnv\scripts\activate.bat
For bash: source <venv>/Scripts/activate
```
Install pytest
```
pip install pytest
```
Switch to pyfa directory.
Run tests (any will do)
```
python -m pytest
py.test
```
More information on tests can be found on appropriate [Wiki page](https://github.com/pyfa-org/Pyfa/wiki/Developers:-Writing-Tests-for-Pyfa).

View File

@@ -2,20 +2,19 @@
[![Join us on Slack!](https://pyfainvite.azurewebsites.net/badge.svg)](https://pyfainvite.azurewebsites.net/) [![Build Status](https://travis-ci.org/pyfa-org/Pyfa.svg?branch=master)](https://travis-ci.org/pyfa-org/Pyfa)
![pyfa](https://cloud.githubusercontent.com/assets/3904767/10271512/af385ef2-6ade-11e5-8f67-52b8b1e4c797.PNG)
![pyfa](https://user-images.githubusercontent.com/275209/66119992-864be080-e5e2-11e9-994a-3a4368c9fad7.png)
## What is it?
pyfa, short for **py**thon **f**itting **a**ssistant, allows you to create, experiment with, and save ship fittings without being in game. Open source and written in Python, it is available on any platform where Python 2.x and wxWidgets are available, including Windows, Mac OS X, and Linux.
Pyfa, short for **py**thon **f**itting **a**ssistant, allows you to create, experiment with, and save ship fittings without being in game. Open source and written in Python, it is available on any platform where Python 3 and wxWidgets are available, including Windows, Mac OS X, and Linux.
## Latest Version and Changelogs
The latest version along with release notes can always be found on the project's [Releases](https://github.com/DarkFenX/Pyfa/releases) page. pyfa will notify you if you are running an outdated version.
The latest version along with release notes can always be found on the project's [releases](https://github.com/pyfa-org/Pyfa/releases) page. Pyfa will notify you if you are running an outdated version.
## Installation
Windows and OS X users are supplied self-contained builds of pyfa on the [latest releases](https://github.com/pyfa-org/Pyfa/releases/latest) page. An `.exe` installer is also available for Windows builds. Linux users can run pyfa using their distribution's Python interpreter. There is no official self-contained package for Linux, however, there are a number of third-party packages available through distribution-specific repositories.
#### OS X
### OS X
Apart from the official release, there is also a [Homebrew](http://brew.sh) option for installing pyfa on OS X. Please note this is maintained by a third-party and is not tested by pyfa developers. Simply fire up in terminal:
```
$ brew install Caskroom/cask/pyfa
@@ -27,34 +26,31 @@ The following is a list of pyfa packages available for certain distributions. Pl
* Arch: https://aur.archlinux.org/packages/pyfa/
* Gentoo: https://github.com/ZeroPointEnergy/gentoo-pyfa-overlay
### Dependencies
If you wish to help with development or simply need to run pyfa through a Python interpreter, the following software is required:
* Python 3.6
* Requirements as listed in `requirements.txt`
## Contribution
If you wish to help with development or you need to run pyfa through a Python interpreter, check out [the instructions](https://github.com/pyfa-org/Pyfa/blob/master/CONTRIBUTING.md).
## Bug Reporting
The preferred method of reporting bugs is through the project's [GitHub Issues interface](https://github.com/pyfa-org/Pyfa/issues). Alternatively, posting a report in the [pyfa thread](http://forums.eveonline.com/default.aspx?g=posts&t=247609) on the official EVE Online forums is acceptable. Guidelines for bug reporting can be found on [this wiki page](https://github.com/DarkFenX/Pyfa/wiki/Bug-Reporting).
The preferred method of reporting bugs is through the project's [GitHub Issues interface](https://github.com/pyfa-org/Pyfa/issues). Alternatively, posting a report in the [pyfa thread](https://forums.eveonline.com/t/27156) on the official EVE Online forums is acceptable. Guidelines for bug reporting can be found on [this wiki page](https://github.com/pyfa-org/Pyfa/wiki/Bug-Reporting).
## License
pyfa is licensed under the GNU GPL v3.0, see LICENSE
Pyfa is licensed under the GNU GPL v3.0, see LICENSE
## Resources
* Development repository: [https://github.com/pyfa-org/Pyfa](https://github.com/pyfa-org/Pyfa)
* [Development repository](https://github.com/pyfa-org/Pyfa)
* [EVE forum thread](https://forums.eveonline.com/t/27156)
* [EVE University guide using pyfa](http://wiki.eveuniversity.org/Guide_to_using_PYFA)
* [EVE University guide using pyfa](https://wiki.eveuniversity.org/PYFA)
* [EVE Online website](http://www.eveonline.com/)
## Contacts:
* Sable Blitzmann
* GitHub: @blitzmann
* [TweetFleet Slack](https://www.fuzzwork.co.uk/tweetfleet-slack-invites/): @blitzmann
* [Gitter chat](https://gitter.im/pyfa-org/Pyfa): @ blitzmann
* Email: sable.blitzmann@gmail.com
* Kadesh / DarkPhoenix
* GitHub: @DarkFenX
* EVE: Kadesh Priestess
* Email: phoenix@mail.ru
* Sable Blitzmann
* GitHub: @blitzmann
* [TweetFleet Slack](https://www.fuzzwork.co.uk/tweetfleet-slack-invites/): @blitzmann
* [Gitter chat](https://gitter.im/pyfa-org/Pyfa): @blitzmann
* Email: sable.blitzmann@gmail.com
## CCP Copyright Notice
EVE Online, the EVE logo, EVE and all associated logos and designs are the intellectual property of CCP hf. All artwork, screenshots, characters, vehicles, storylines, world facts or other recognizable features of the intellectual property relating to these trademarks are likewise the intellectual property of CCP hf. EVE Online and the EVE logo are the registered trademarks of CCP hf. All rights are reserved worldwide. All other trademarks are the property of their respective owners. CCP hf. has granted permission to pyfa to use EVE Online and all associated logos and designs for promotional and information purposes on its website but does not endorse, and is not in any way affiliated with, pyfa. CCP is in no way responsible for the content on or functioning of this program, nor can it be liable for any damage arising from the use of this program.

View File

@@ -49,6 +49,8 @@ def DBInMemory_test():
gamedata_version = gamedata_session.execute(
"SELECT `field_value` FROM `metadata` WHERE `field_name` LIKE 'client_build'"
).fetchone()[0]
except (KeyboardInterrupt, SystemExit):
raise
except Exception as e:
print("Missing gamedata version.")
gamedata_version = None
@@ -72,7 +74,7 @@ def DBInMemory_test():
# noinspection PyPep8
#from eos.db.gamedata import alphaClones, attribute, category, effect, group, icon, item, marketGroup, metaData, metaGroup, queries, traits, unit
# noinspection PyPep8
#from eos.db.saveddata import booster, cargo, character, crest, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, loadDefaultDatabaseValues, miscData, module, override, price, queries, skill, targetProfile, user
#from eos.db.saveddata import booster, cargo, character, crest, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, miscData, module, override, price, queries, skill, targetProfile, user
# If using in memory saveddata, you'll want to reflect it so the data structure is good.
if saveddata_connectionstring == "sqlite:///:memory:":

View File

@@ -33,11 +33,14 @@ pyfaPath = None
savePath = None
saveDB = None
gameDB = None
imgsZIP = None
logPath = None
loggingLevel = None
logging_setup = None
cipher = None
clientHash = None
experimentalFeatures = None
version = None
ESI_CACHE = 'esi_cache'
@@ -96,11 +99,13 @@ def defPaths(customSavePath=None):
global savePath
global saveDB
global gameDB
global imgsZIP
global saveInRoot
global logPath
global cipher
global clientHash
global version
global experimentalFeatures
pyfalog.debug("Configuring Pyfa")
@@ -155,6 +160,10 @@ def defPaths(customSavePath=None):
if not gameDB:
gameDB = os.path.join(pyfaPath, "eve.db")
imgsZIP = getattr(configforced, "imgsZIP", imgsZIP)
if not imgsZIP:
imgsZIP = os.path.join(pyfaPath, "imgs.zip")
if debug:
logFile = "pyfa_debug.log"
else:
@@ -162,6 +171,10 @@ def defPaths(customSavePath=None):
logPath = os.path.join(savePath, logFile)
experimentalFeatures = getattr(configforced, "experimentalFeatures", experimentalFeatures)
if experimentalFeatures is None:
experimentalFeatures = False
# DON'T MODIFY ANYTHING BELOW
import eos.config
@@ -220,6 +233,8 @@ def defLogging():
# reset=False,
)
])
except (KeyboardInterrupt, SystemExit):
raise
except:
print("Critical error attempting to setup logging. Falling back to console only.")
logging_setup = NestedSetup([

556
db_update.py Normal file
View File

@@ -0,0 +1,556 @@
#!/usr/bin/env python3
#======================================================================
# Copyright (C) 2012 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 3 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/>.
#======================================================================
import functools
import itertools
import json
import os
import re
import sqlite3
import sys
ROOT_DIR = os.path.realpath(os.path.dirname(__file__))
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
def db_needs_update():
"""True if needs, false if it does not, none if we cannot check it."""
try:
with open(os.path.join(JSON_DIR, 'phobos', 'metadata.json')) as f:
data_version = next((r['field_value'] for r in json.load(f) if r['field_name'] == 'client_build'))
except (KeyboardInterrupt, SystemExit):
raise
# If we have no source data - return None; should not update in this case
except:
return None
if not os.path.isfile(DB_PATH):
print('Gamedata DB not found')
return True
db_data_version = None
db_schema_version = None
try:
db = sqlite3.connect(DB_PATH)
cursor = db.cursor()
cursor.execute('SELECT field_value FROM metadata WHERE field_name = \'client_build\'')
for row in cursor:
db_data_version = int(row[0])
cursor.execute('SELECT field_value FROM metadata WHERE field_name = \'schema_version\'')
for row in cursor:
db_schema_version = int(row[0])
cursor.close()
db.close()
except (KeyboardInterrupt, SystemExit):
raise
except:
print('Error when fetching gamedata DB metadata')
return True
if data_version != db_data_version:
print('Gamedata DB data version mismatch: needed {}, DB has {}'.format(data_version, db_data_version))
return True
if GAMEDATA_SCHEMA_VERSION != db_schema_version:
print('Gamedata DB schema version mismatch: needed {}, DB has {}'.format(GAMEDATA_SCHEMA_VERSION, db_schema_version))
return True
return False
def update_db():
print('Building gamedata DB...')
if os.path.isfile(DB_PATH):
os.remove(DB_PATH)
import eos.db
import eos.gamedata
# Create the database tables
eos.db.gamedata_meta.create_all()
def _readData(minerName, jsonName, keyIdName=None):
with open(os.path.join(JSON_DIR, minerName, '{}.json'.format(jsonName)), encoding='utf-8') as f:
rawData = json.load(f)
if not keyIdName:
return rawData
# IDs in keys, rows in values
data = []
for k, v in rawData.items():
row = {}
row.update(v)
if keyIdName not in row:
row[keyIdName] = int(k)
data.append(row)
return data
def _addRows(data, cls, fieldMap=None):
if fieldMap is None:
fieldMap = {}
for row in data:
instance = cls()
for k, v in row.items():
if isinstance(v, str):
v = v.strip()
setattr(instance, fieldMap.get(k, k), v)
eos.db.gamedata_session.add(instance)
def processEveTypes():
print('processing evetypes')
data = _readData('fsd_lite', 'evetypes', keyIdName='typeID')
for row in data:
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['published'] = True
elif row['typeName'].startswith('Limited Synth '):
row['published'] = False
newData = []
for row in data:
if (
row['published'] or
# group Ship Modifiers, for items like tactical t3 ship modes
row['groupID'] == 1306 or
# Micro Bombs (Fighters)
row['typeID'] in (41549, 41548, 41551, 41550) or
# Abyssal weather (environment)
row['groupID'] in (
1882,
1975,
1971,
# the "container" for the abyssal environments
1983)
):
newData.append(row)
_addRows(newData, eos.gamedata.Item)
return newData
def processEveGroups():
print('processing evegroups')
data = _readData('fsd_lite', 'evegroups', keyIdName='groupID')
_addRows(data, eos.gamedata.Group)
return data
def processEveCategories():
print('processing evecategories')
data = _readData('fsd_lite', 'evecategories', keyIdName='categoryID')
_addRows(data, eos.gamedata.Category)
def processDogmaAttributes():
print('processing dogmaattributes')
data = _readData('fsd_binary', 'dogmaattributes', keyIdName='attributeID')
_addRows(data, eos.gamedata.AttributeInfo)
def processDogmaTypeAttributes(eveTypesData):
print('processing dogmatypeattributes')
data = _readData('fsd_binary', 'typedogma', keyIdName='typeID')
eveTypeIds = set(r['typeID'] for r in eveTypesData)
newData = []
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']
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
def processDynamicItemAttributes():
print('processing dynamicitemattributes')
data = _readData('fsd_binary', 'dynamicitemattributes')
for mutaID, mutaData in data.items():
muta = eos.gamedata.DynamicItem()
muta.typeID = mutaID
muta.resultingTypeID = mutaData['inputOutputMapping'][0]['resultingType']
eos.db.gamedata_session.add(muta)
for x in mutaData['inputOutputMapping'][0]['applicableTypes']:
item = eos.gamedata.DynamicItemItem()
item.typeID = mutaID
item.applicableTypeID = x
eos.db.gamedata_session.add(item)
for attrID, attrData in mutaData['attributeIDs'].items():
attr = eos.gamedata.DynamicItemAttribute()
attr.typeID = mutaID
attr.attributeID = attrID
attr.min = attrData['min']
attr.max = attrData['max']
eos.db.gamedata_session.add(attr)
def processDogmaEffects():
print('processing dogmaeffects')
data = _readData('fsd_binary', 'dogmaeffects', keyIdName='effectID')
_addRows(data, eos.gamedata.Effect, fieldMap={'resistanceAttributeID': 'resistanceID'})
def processDogmaTypeEffects(eveTypesData):
print('processing dogmatypeeffects')
data = _readData('fsd_binary', 'typedogma', keyIdName='typeID')
eveTypeIds = set(r['typeID'] for r in eveTypesData)
newData = []
for typeData in data:
if typeData['typeID'] not in eveTypeIds:
continue
for row in typeData.get('dogmaEffects', ()):
row['typeID'] = typeData['typeID']
newData.append(row)
_addRows(newData, eos.gamedata.ItemEffect)
return newData
def processDogmaUnits():
print('processing dogmaunits')
data = _readData('fsd_binary', 'dogmaunits', keyIdName='unitID')
_addRows(data, eos.gamedata.Unit, fieldMap={'name': 'unitName'})
def processMarketGroups():
print('processing marketgroups')
data = _readData('fsd_binary', 'marketgroups', keyIdName='marketGroupID')
_addRows(data, eos.gamedata.MarketGroup, fieldMap={'name': 'marketGroupName'})
def processMetaGroups():
print('processing metagroups')
data = _readData('fsd_binary', 'metagroups', keyIdName='metaGroupID')
_addRows(data, eos.gamedata.MetaGroup)
def processCloneGrades():
print('processing clonegrades')
data = _readData('fsd_lite', 'clonegrades')
newData = []
# December, 2017 - CCP decided to use only one set of skill levels for alpha clones. However, this is still
# represented in the data as a skillset per race. To ensure that all skills are the same, we store them in a way
# that we can check to make sure all races have the same skills, as well as skill levels
check = {}
for ID in data:
for skill in data[ID]['skills']:
newData.append({
'alphaCloneID': int(ID),
'alphaCloneName': 'Alpha Clone',
'typeID': skill['typeID'],
'level': skill['level']})
if ID not in check:
check[ID] = {}
check[ID][int(skill['typeID'])] = int(skill['level'])
if not functools.reduce(lambda a, b: a if a == b else False, [v for _, v in check.items()]):
raise Exception('Alpha Clones not all equal')
newData = [x for x in newData if x['alphaCloneID'] == 1]
if len(newData) == 0:
raise Exception('Alpha Clone processing failed')
tmp = []
for row in newData:
if row['alphaCloneID'] not in tmp:
cloneParent = eos.gamedata.AlphaClone()
setattr(cloneParent, 'alphaCloneID', row['alphaCloneID'])
setattr(cloneParent, 'alphaCloneName', row['alphaCloneName'])
eos.db.gamedata_session.add(cloneParent)
tmp.append(row['alphaCloneID'])
_addRows(newData, eos.gamedata.AlphaCloneSkill)
def processTraits():
print('processing traits')
data = _readData('phobos', 'traits')
def convertSection(sectionData):
sectionLines = []
headerText = '<b>{}</b>'.format(sectionData['header'])
sectionLines.append(headerText)
for bonusData in sectionData['bonuses']:
prefix = '{} '.format(bonusData['number']) if 'number' in bonusData else ''
bonusText = '{}{}'.format(prefix, bonusData['text'].replace('\u00B7', '\u2022 '))
sectionLines.append(bonusText)
sectionLine = '<br />\n'.join(sectionLines)
return sectionLine
newData = []
for row in data:
typeLines = []
typeId = row['typeID']
traitData = row['traits']
for skillData in sorted(traitData.get('skills', ()), key=lambda i: i['header']):
typeLines.append(convertSection(skillData))
if 'role' in traitData:
typeLines.append(convertSection(traitData['role']))
if 'misc' in traitData:
typeLines.append(convertSection(traitData['misc']))
traitLine = '<br />\n<br />\n'.join(typeLines)
newRow = {'typeID': typeId, 'traitText': traitLine}
newData.append(newRow)
_addRows(newData, eos.gamedata.Traits)
def processMetadata():
print('processing metadata')
data = _readData('phobos', 'metadata')
_addRows(data, eos.gamedata.MetaData)
def processReqSkills(eveTypesData):
print('processing requiredskillsfortypes')
def composeReqSkills(raw):
reqSkills = {}
for skillTypeID, skillLevels in raw.items():
reqSkills[int(skillTypeID)] = skillLevels[0]
return reqSkills
eveTypeIds = set(r['typeID'] for r in eveTypesData)
data = _readData('fsd_binary', 'requiredskillsfortypes')
reqsByItem = {}
itemsByReq = {}
for typeID, skillreqData in data.items():
typeID = int(typeID)
if typeID not in eveTypeIds:
continue
for skillTypeID, skillLevel in composeReqSkills(skillreqData).items():
reqsByItem.setdefault(typeID, {})[skillTypeID] = skillLevel
itemsByReq.setdefault(skillTypeID, {})[typeID] = skillLevel
for item in eos.db.gamedata_session.query(eos.gamedata.Item).all():
if item.typeID in reqsByItem:
item.reqskills = json.dumps(reqsByItem[item.typeID])
if item.typeID in itemsByReq:
item.requiredfor = json.dumps(itemsByReq[item.typeID])
def processReplacements(eveTypesData, eveGroupsData, dogmaTypeAttributesData, dogmaTypeEffectsData):
print('finding item replacements')
def compareAttrs(attrs1, attrs2):
# Consider items as different if they have no attrs
if len(attrs1) == 0 and len(attrs2) == 0:
return False
if set(attrs1) != set(attrs2):
return False
if all(attrs1[aid] == attrs2[aid] for aid in attrs1):
return True
return False
skillReqAttribs = {
182: 277,
183: 278,
184: 279,
1285: 1286,
1289: 1287,
1290: 1288}
skillReqAttribsFlat = set(skillReqAttribs.keys()).union(skillReqAttribs.values())
# Get data on type groups
# Format: {type ID: group ID}
typesGroups = {}
for row in eveTypesData:
typesGroups[row['typeID']] = row['groupID']
# Get data on item effects
# Format: {type ID: set(effect, IDs)}
typesEffects = {}
for row in dogmaTypeEffectsData:
typesEffects.setdefault(row['typeID'], set()).add(row['effectID'])
# Get data on type attributes
# Format: {type ID: {attribute ID: attribute value}}
typesNormalAttribs = {}
typesSkillAttribs = {}
for row in dogmaTypeAttributesData:
attributeID = row['attributeID']
if attributeID in skillReqAttribsFlat:
typeSkillAttribs = typesSkillAttribs.setdefault(row['typeID'], {})
typeSkillAttribs[row['attributeID']] = row['value']
# Ignore these attributes for comparison purposes
elif attributeID in (
# We do not need mass as it affects final ship stats only when carried by ship itself
# (and we're not going to replace ships), but it's wildly inconsistent for other items,
# which otherwise would be the same
4, # mass
124, # mainColor
162, # radius
422, # techLevel
633, # metaLevel
1692, # metaGroupID
1768 # typeColorScheme
):
continue
else:
typeNormalAttribs = typesNormalAttribs.setdefault(row['typeID'], {})
typeNormalAttribs[row['attributeID']] = row['value']
# Get data on skill requirements
# Format: {type ID: {skill type ID: skill level}}
typesSkillReqs = {}
for typeID, typeAttribs in typesSkillAttribs.items():
typeSkillAttribs = typesSkillAttribs.get(typeID, {})
if not typeSkillAttribs:
continue
typeSkillReqs = typesSkillReqs.setdefault(typeID, {})
for skillreqTypeAttr, skillreqLevelAttr in skillReqAttribs.items():
try:
skillType = int(typeSkillAttribs[skillreqTypeAttr])
skillLevel = int(typeSkillAttribs[skillreqLevelAttr])
except (KeyError, ValueError):
continue
typeSkillReqs[skillType] = skillLevel
# Format: {group ID: category ID}
groupCategories = {}
for row in eveGroupsData:
groupCategories[row['groupID']] = row['categoryID']
# As EVE affects various types mostly depending on their group or skill requirements,
# we're going to group various types up this way
# Format: {(group ID, frozenset(skillreq, type, IDs), frozenset(type, effect, IDs): [type ID, {attribute ID: attribute value}]}
groupedData = {}
for row in eveTypesData:
typeID = row['typeID']
# Ignore items outside of categories we need
if groupCategories[typesGroups[typeID]] not in (
6, # Ship
7, # Module
8, # Charge
18, # Drone
20, # Implant
22, # Deployable
23, # Starbase
32, # Subsystem
35, # Decryptors
65, # Structure
66, # Structure Module
87, # Fighter
):
continue
typeAttribs = typesNormalAttribs.get(typeID, {})
# Ignore items w/o attributes
if not typeAttribs:
continue
# We need only skill types, not levels for keys
typeSkillreqs = frozenset(typesSkillReqs.get(typeID, {}))
typeGroup = typesGroups[typeID]
typeEffects = frozenset(typesEffects.get(typeID, ()))
groupData = groupedData.setdefault((typeGroup, typeSkillreqs, typeEffects), [])
groupData.append((typeID, typeAttribs))
# Format: {type ID: set(type IDs)}
replacements = {}
# Now, go through composed groups and for every item within it
# find items which are the same
for groupData in groupedData.values():
for type1, type2 in itertools.combinations(groupData, 2):
if compareAttrs(type1[1], type2[1]):
replacements.setdefault(type1[0], set()).add(type2[0])
replacements.setdefault(type2[0], set()).add(type1[0])
# Update DB session with data we generated
for item in eos.db.gamedata_session.query(eos.gamedata.Item).all():
itemReplacements = replacements.get(item.typeID)
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()
processDogmaAttributes()
dogmaTypeAttributesData = processDogmaTypeAttributes(eveTypesData)
processDynamicItemAttributes()
processDogmaEffects()
dogmaTypeEffectsData = processDogmaTypeEffects(eveTypesData)
processDogmaUnits()
processMarketGroups()
processMetaGroups()
processCloneGrades()
processTraits()
processMetadata()
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()
metadata_schema_version.field_name = 'schema_version'
metadata_schema_version.field_value = GAMEDATA_SCHEMA_VERSION
eos.db.gamedata_session.add(metadata_schema_version)
eos.db.gamedata_session.flush()
# CCP still has 5 subsystems assigned to T3Cs, even though only 4 are available / usable. They probably have some
# old legacy requirement or assumption that makes it difficult for them to change this value in the data. But for
# pyfa, we can do it here as a post-processing step
for attr in eos.db.gamedata_session.query(eos.gamedata.Attribute).filter(eos.gamedata.Attribute.ID == 1367).all():
attr.value = 4.0
for item in eos.db.gamedata_session.query(eos.gamedata.Item).filter(eos.gamedata.Item.name.like('%abyssal%')).all():
item.published = False
for x in [
30 # Apparel
]:
cat = eos.db.gamedata_session.query(eos.gamedata.Category).filter(eos.gamedata.Category.ID == x).first()
print ('Removing Category: {}'.format(cat.name))
eos.db.gamedata_session.delete(cat)
eos.db.gamedata_session.commit()
eos.db.gamedata_engine.execute('VACUUM')
print('done')
if __name__ == '__main__':
update_db()

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

@@ -1,78 +0,0 @@
# This apes hook-matplotlib.backends.py, but REMOVES backends, all but
# the ones in the list below.
# Courtesy of https://github.com/bpteague/cytoflow/blob/70f9291/packaging/hook-matplotlib.backends.py
KEEP = ["WXAgg", "WX", "agg"]
from PyInstaller.compat import is_darwin
from PyInstaller.utils.hooks import (
eval_statement, exec_statement, logger)
def get_matplotlib_backend_module_names():
"""
List the names of all matplotlib backend modules importable under the
current Python installation.
Returns
----------
list
List of the fully-qualified names of all such modules.
"""
# Statement safely importing a single backend module.
import_statement = """
import os, sys
# Preserve stdout.
sys_stdout = sys.stdout
try:
# Redirect output printed by this importation to "/dev/null", preventing
# such output from being erroneously interpreted as an error.
with open(os.devnull, 'w') as dev_null:
sys.stdout = dev_null
__import__('%s')
# If this is an ImportError, print this exception's message without a traceback.
# ImportError messages are human-readable and require no additional context.
except ImportError as exc:
sys.stdout = sys_stdout
print(exc)
# Else, print this exception preceded by a traceback. traceback.print_exc()
# prints to stderr rather than stdout and must not be called here!
except Exception:
sys.stdout = sys_stdout
import traceback
print(traceback.format_exc())
"""
# List of the human-readable names of all available backends.
backend_names = eval_statement(
'import matplotlib; print(matplotlib.rcsetup.all_backends)')
# List of the fully-qualified names of all importable backend modules.
module_names = []
# If the current system is not OS X and the "CocoaAgg" backend is available,
# remove this backend from consideration. Attempting to import this backend
# on non-OS X systems halts the current subprocess without printing output
# or raising exceptions, preventing its reliable detection.
if not is_darwin and 'CocoaAgg' in backend_names:
backend_names.remove('CocoaAgg')
# For safety, attempt to import each backend in a unique subprocess.
for backend_name in backend_names:
if backend_name in KEEP:
continue
module_name = 'matplotlib.backends.backend_%s' % backend_name.lower()
stdout = exec_statement(import_statement % module_name)
# If no output was printed, this backend is importable.
if not stdout:
module_names.append(module_name)
logger.info(' Matplotlib backend "%s": removed' % backend_name)
return module_names
# Freeze all importable backends, as PyInstaller is unable to determine exactly
# which backends are required by the current program.
e=get_matplotlib_backend_module_names()
print(e)
excludedimports = e

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<noInheritable/>
<assemblyIdentity
type="win32"
name="Microsoft.VC90.CRT"
version="9.0.21022.8"
processorArchitecture="x86"
publicKeyToken="1fc8b3b9a1e18e3b"/>
<file name="MSVCR90.DLL"/>
<file name="MSVCM90.DLL"/>
<file name="MSVCP90.DLL"/>
</assembly>

View File

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

View File

@@ -15,10 +15,6 @@
#define MyAppURL "https://github.com/pyfa-org/Pyfa/"
#define MyAppExeName "pyfa.exe"
; What version starts with the new structure (1.x.0). This is used to determine if we run directory structure cleanup
#define MajorVersionFlag 2
#define MinorVersionFlag 0
#ifndef MyOutputFile
#define MyOutputFile LowerCase(StringChange(MyAppName+'-'+MyAppVersion+'-win', " ", "-"))
#endif
@@ -30,7 +26,6 @@
#endif
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
@@ -41,6 +36,9 @@ AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64
CloseApplications=yes
DefaultDirName={pf}\{#MyAppName}
DefaultGroupName={#MyAppName}
AllowNoIcons=yes
@@ -49,7 +47,6 @@ OutputDir={#MyOutputDir}
OutputBaseFilename={#MyOutputFile}
SetupIconFile={#MyAppDir}\pyfa.ico
SolidCompression=yes
CloseApplications=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
@@ -83,6 +80,7 @@ Type: files; Name: "{app}\*.pyc"
[Code]
/////////////////////////////////////////////////////////////////////
function IsAppRunning(const FileName : string): Boolean;
var
FSWbemLocator: Variant;
@@ -99,6 +97,7 @@ begin
FSWbemLocator := Unassigned;
end;
/////////////////////////////////////////////////////////////////////
procedure RemoveFromVirtualStore;
var
VirtualStore,FileName,FilePath:String;
@@ -115,6 +114,7 @@ begin
end;
end;
/////////////////////////////////////////////////////////////////////
function PrepareToInstall(var NeedsRestart: Boolean): String;
begin
if(IsAppRunning( 'pyfa.exe' )) then
@@ -127,54 +127,61 @@ begin
end
end;
function GetUninstallString: string;
/////////////////////////////////////////////////////////////////////
function GetUninstallString(): String;
var
sUnInstPath: string;
sUnInstPath: String;
sUnInstallString: String;
begin
Result := '';
sUnInstPath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\{{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1'); //Your App GUID/ID
sUnInstallString := '';
if not RegQueryStringValue(HKLM, sUnInstPath, 'UninstallString', sUnInstallString) then
RegQueryStringValue(HKCU, sUnInstPath, 'UninstallString', sUnInstallString);
if not RegQueryStringValue(HKCU, sUnInstPath, 'UninstallString', sUnInstallString) then
if not RegQueryStringValue(HKLM32, sUnInstPath, 'UninstallString', sUnInstallString) then
RegQueryStringValue(HKCU32, sUnInstPath, 'UninstallString', sUnInstallString);
Result := sUnInstallString;
end;
function IsUpgrade: Boolean;
/////////////////////////////////////////////////////////////////////
function UnInstallOldVersion(): Integer;
var
sUnInstallString: String;
iResultCode: Integer;
begin
// Return Values:
// 1 - uninstall string is empty
// 2 - error executing the UnInstallString
// 3 - successfully executed the UnInstallString
// default return value
Result := 0;
// get the uninstall string of the old app
sUnInstallString := GetUninstallString();
if sUnInstallString <> '' then begin
sUnInstallString := RemoveQuotes(sUnInstallString);
if Exec(sUnInstallString, '/SILENT /NORESTART /SUPPRESSMSGBOXES','', SW_HIDE, ewWaitUntilTerminated, iResultCode) then
Result := 3
else
Result := 2;
end else
Result := 1;
end;
/////////////////////////////////////////////////////////////////////
function IsUpgrade(): Boolean;
begin
Result := (GetUninstallString() <> '');
end;
function InitializeSetup: Boolean;
var
V: Integer;
iResultCode: Integer;
sUnInstallString: string;
iOldVersionMajor: Cardinal;
iOldVersionMinor: Cardinal;
/////////////////////////////////////////////////////////////////////
procedure CurStepChanged(CurStep: TSetupStep);
begin
Result := True; // in case when no previous version is found
if RegValueExists(HKEY_LOCAL_MACHINE,'Software\Microsoft\Windows\CurrentVersion\Uninstall\{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1', 'UninstallString') then //Your App GUID/ID
if (CurStep=ssInstall) then
begin
RegQueryDWordValue(HKEY_LOCAL_MACHINE,
'Software\Microsoft\Windows\CurrentVersion\Uninstall\{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1',
'MajorVersion', iOldVersionMajor);
RegQueryDWordValue(HKEY_LOCAL_MACHINE,
'Software\Microsoft\Windows\CurrentVersion\Uninstall\{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1',
'MinorVersion', iOldVersionMinor);
if (iOldVersionMajor < {#MajorVersionFlag}) or ((iOldVersionMajor = {#MajorVersionFlag}) and (iOldVersionMinor < {#MinorVersionFlag})) then // If old version with old structure is installed.
if (IsUpgrade()) then
begin
V := MsgBox(ExpandConstant('An old version of pyfa was detected. Due to recent changes in the application structure, you must uninstall the previous version first. This will not affect your user data (saved fittings, characters, etc.). Do you want to uninstall now?'), mbInformation, MB_YESNO); //Custom Message if App installed
if V = IDYES then
begin
sUnInstallString := GetUninstallString();
sUnInstallString := RemoveQuotes(sUnInstallString);
Exec(ExpandConstant(sUnInstallString), '', '', SW_SHOW, ewWaitUntilTerminated, iResultCode);
Result := True; //if you want to proceed after uninstall
//Exit; //if you want to quit after uninstall
end
else
Result := False; //when older version present and not uninstalled
UnInstallOldVersion();
end;
end;
end;

View File

@@ -8,11 +8,6 @@
</requestedPrivileges>
</security>
</trustInfo>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.VC90.CRT" version="9.0.21022.8" processorArchitecture="x86" publicKeyToken="1fc8b3b9a1e18e3b"></assemblyIdentity>
</dependentAssembly>
</dependency>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>

View File

@@ -20,7 +20,6 @@ added_files = [
('../../service/jargon/*.yaml', 'service/jargon'),
('../../dist_assets/win/pyfa.ico', '.'),
('../../dist_assets/win/pyfa.exe.manifest', '.'),
('../../dist_assets/win/Microsoft.VC90.CRT.manifest', '.'),
(requests.certs.where(), '.'), # is this needed anymore?
('../../eve.db', '.'),
('../../README.md', '.'),
@@ -30,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

@@ -1,83 +0,0 @@
# -*- mode: python -*-
# Note: This script is provided AS-IS for those that may be interested.
# pyfa does not currently support pyInstaller (or any other build process) 100% at the moment
# Command line to build:
# (Run from directory where pyfa.py and pyfa.spec lives.)
# c:\Python27\scripts\pyinstaller.exe --clean --noconfirm --windowed --upx-dir=.\scripts\upx.exe pyfa.spec
# Don't forget to change the path to where your pyfa.py and pyfa.spec lives
# pathex=['C:\\Users\\Ebag333\\Documents\\GitHub\\Ebag333\\Pyfa'],
import os
block_cipher = None
added_files = [
( 'imgs/gui/*.png', 'imgs/gui' ),
( 'imgs/gui/*.gif', 'imgs/gui' ),
( 'imgs/icons/*.png', 'imgs/icons' ),
( 'imgs/renders/*.png', 'imgs/renders' ),
( 'dist_assets/win/pyfa.ico', '.' ),
( 'dist_assets/cacert.pem', '.' ),
( 'eve.db', '.' ),
( 'README.md', '.' ),
( 'LICENSE', '.' ),
]
import_these = []
# Walk eos.effects and add all effects so we can import them properly
for root, folders, files in os.walk("eos/effects"):
for file_ in files:
if file_.endswith(".py") and not file_.startswith("_"):
mod_name = "{}.{}".format(
root.replace("/", "."),
file_.split(".py")[0],
)
import_these.append(mod_name)
a = Analysis(
['pyfa.py'],
pathex=['C:\\projects\\pyfa\\'],
binaries=[],
datas=added_files,
hiddenimports=import_these,
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
)
pyz = PYZ(
a.pure,
a.zipped_data,
cipher=block_cipher,
)
exe = EXE(pyz,
a.scripts,
exclude_binaries=True,
debug=True,
console=True,
strip=False,
upx=True,
name='pyfa_debug',
icon='dist_assets/win/pyfa.ico',
onefile=False,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
onefile=False,
name='pyfa_debug',
icon='dist_assets/win/pyfa.ico',
)

71
eos/calc.py Normal file
View File

@@ -0,0 +1,71 @@
# =============================================================================
# Copyright (C) 2019 Ryan Holmes
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
import math
# Just copy-paste penalization chain calculation code (with some modifications,
# as multipliers arrive in different form) in here to not make actual attribute
# calculations slower than they already are due to extra function calls
def calculateMultiplier(multipliers):
"""
multipliers: dictionary in format:
{stacking group name: [(mult, resist attr ID), (mult, resist attr ID)]}
"""
val = 1
for penalizedMultipliers in multipliers.values():
# A quick explanation of how this works:
# 1: Bonuses and penalties are calculated seperately, so we'll have to filter each of them
l1 = [v[0] for v in penalizedMultipliers if v[0] > 1]
l2 = [v[0] for v in penalizedMultipliers if v[0] < 1]
# 2: The most significant bonuses take the smallest penalty,
# This means we'll have to sort
abssort = lambda _val: -abs(_val - 1)
l1.sort(key=abssort)
l2.sort(key=abssort)
# 3: The first module doesn't get penalized at all
# Any module after the first takes penalties according to:
# 1 + (multiplier - 1) * math.exp(- math.pow(i, 2) / 7.1289)
for l in (l1, l2):
for i in range(len(l)):
bonus = l[i]
val *= 1 + (bonus - 1) * math.exp(- i ** 2 / 7.1289)
return val
def calculateRangeFactor(srcOptimalRange, srcFalloffRange, distance, restrictedRange=True):
"""Range strength/chance factor, applicable to guns, ewar, RRs, etc."""
if distance is None:
return 1
if srcFalloffRange > 0:
# Most modules cannot be activated when at 3x falloff range, with few exceptions like guns
if restrictedRange and distance > srcOptimalRange + 3 * srcFalloffRange:
return 0
return 0.5 ** ((max(0, distance - srcOptimalRange) / srcFalloffRange) ** 2)
elif distance <= srcOptimalRange:
return 1
else:
return 0
def calculateLockTime(srcScanRes, tgtSigRadius):
if not srcScanRes or not tgtSigRadius:
return None
return min(40000 / srcScanRes / math.asinh(tgtSigRadius) ** 2, 30 * 60)

View File

@@ -148,6 +148,7 @@ class CapSimulator:
stability_precision = self.stability_precision
period = self.period
activation = None
iterations = 0
capCapacity = self.capacitorCapacity
@@ -162,7 +163,12 @@ class CapSimulator:
t_max = self.t_max
while 1:
activation = pop(state)
# Nothing to pop - might happen when no mods are activated, or when
# only cap injectors are active (and are postponed by code below)
try:
activation = pop(state)
except IndexError:
break
t_now, duration, capNeed, shot, clipSize, reloadTime, isInjector = activation
# Max time reached, stop simulation - we're stable
@@ -275,7 +281,8 @@ class CapSimulator:
activation[3] = shot
push(state, activation)
push(state, activation)
if activation is not None:
push(state, activation)
# update instance with relevant results.
self.t = t_last

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
@@ -56,6 +86,8 @@ try:
config.gamedata_date = gamedata_session.execute(
"SELECT `field_value` FROM `metadata` WHERE `field_name` LIKE 'dump_time'"
).fetchone()[0]
except (KeyboardInterrupt, SystemExit):
raise
except Exception as e:
pyfalog.warning("Missing gamedata version.")
pyfalog.critical(e)
@@ -82,10 +114,10 @@ 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, loadDefaultDatabaseValues, \
from eos.db.saveddata import booster, cargo, character, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, \
miscData, mutator, module, override, price, queries, skill, targetProfile, user
pyfalog.debug('Importing gamedata queries')

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

@@ -25,7 +25,7 @@ from sqlalchemy.orm.collections import attribute_mapped_collection
from eos.db import gamedata_meta
from eos.db.gamedata.dynamicAttributes import dynamicApplicable_table
from eos.db.gamedata.effect import typeeffects_table
from eos.gamedata import Attribute, DynamicItem, Effect, Group, Item, MetaType, Traits
from eos.gamedata import Attribute, DynamicItem, Effect, Group, Item, Traits, MetaGroup
items_table = Table("invtypes", gamedata_meta,
Column("typeID", Integer, primary_key=True),
@@ -33,17 +33,18 @@ 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),
Column("graphicID", Integer),
Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True),
Column("replacements", String))
Column("metaLevel", Integer),
Column("metaGroupID", Integer, ForeignKey("invmetagroups.metaGroupID"), index=True),
Column("variationParentTypeID", Integer, ForeignKey("invtypes.typeID"), index=True),
Column("replacements", String),
Column("reqskills", String),
Column("requiredfor", String))
from .metaGroup import metatypes_table # noqa
from .traits import traits_table # noqa
mapper(Item, items_table,
@@ -51,9 +52,8 @@ mapper(Item, items_table,
"group" : relation(Group, backref=backref("items", cascade="all,delete")),
"_Item__attributes": relation(Attribute, cascade='all, delete, delete-orphan', collection_class=attribute_mapped_collection('name')),
"effects": relation(Effect, secondary=typeeffects_table, collection_class=attribute_mapped_collection('name')),
"metaGroup" : relation(MetaType,
primaryjoin=metatypes_table.c.typeID == items_table.c.typeID,
uselist=False),
"metaGroup" : relation(MetaGroup, backref=backref("items", cascade="all,delete")),
"varParent" : relation(Item, backref=backref("varChildren", cascade="all,delete"), remote_side=items_table.c.typeID),
"ID" : synonym("typeID"),
"name" : synonym("typeName"),
"description" : deferred(items_table.c.description),
@@ -64,7 +64,6 @@ mapper(Item, items_table,
primaryjoin=dynamicApplicable_table.c.applicableTypeID == items_table.c.typeID,
secondaryjoin=dynamicApplicable_table.c.typeID == DynamicItem.typeID,
secondary=dynamicApplicable_table,
backref="applicableItems")
})
backref="applicableItems")})
Item.category = association_proxy("group", "category")

View File

@@ -17,35 +17,17 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from sqlalchemy import Table, Column, Integer, ForeignKey, String
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import relation, mapper, synonym
from sqlalchemy import Table, Column, Integer, String
from sqlalchemy.orm import mapper, synonym
from eos.db import gamedata_meta
from eos.db.gamedata.item import items_table
from eos.gamedata import Item, MetaGroup, MetaType
from eos.gamedata import MetaGroup
metagroups_table = Table("invmetagroups", gamedata_meta,
Column("metaGroupID", Integer, primary_key=True),
Column("metaGroupName", String))
metatypes_table = Table("invmetatypes", gamedata_meta,
Column("typeID", Integer, ForeignKey("invtypes.typeID"), primary_key=True),
Column("parentTypeID", Integer, ForeignKey("invtypes.typeID")),
Column("metaGroupID", Integer, ForeignKey("invmetagroups.metaGroupID")))
mapper(MetaGroup, metagroups_table,
properties={
"ID" : synonym("metaGroupID"),
"name": synonym("metaGroupName")
})
mapper(MetaType, metatypes_table,
properties={
"ID" : synonym("metaGroupID"),
"parent": relation(Item, primaryjoin=metatypes_table.c.parentTypeID == items_table.c.typeID),
"items" : relation(Item, primaryjoin=metatypes_table.c.typeID == items_table.c.typeID),
"info" : relation(MetaGroup, lazy=False)
})
MetaType.name = association_proxy("info", "name")
"name": synonym("metaGroupName")})

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.gamedata.metaGroup import items_table, metatypes_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,28 +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()
itemNameMap[lookfor] = item.ID
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
@@ -108,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.
@@ -124,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
@@ -147,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
@@ -161,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
@@ -171,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
@@ -182,20 +193,21 @@ 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()
groupNameMap[lookfor] = group.ID
group = get_gamedata_session().query(Group).options(*processEager(eager)).filter(Group.name == lookfor).first()
if group is not None:
groupNameMap[lookfor] = group.ID
else:
raise TypeError("Need integer or string as argument")
return group
@@ -208,23 +220,24 @@ 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()
categoryNameMap[lookfor] = category.ID
if category is not None:
categoryNameMap[lookfor] = category.ID
else:
raise TypeError("Need integer or string as argument")
return category
@@ -237,35 +250,40 @@ 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()
metaGroupNameMap[lookfor] = metaGroup.ID
if metaGroup is not None:
metaGroupNameMap[lookfor] = metaGroup.ID
else:
raise TypeError("Need integer or string as argument")
return metaGroup
def getMetaGroups():
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")
@@ -277,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
@@ -291,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()
@@ -306,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:
@@ -317,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:
@@ -342,11 +381,9 @@ def getVariations(itemids, groupIDs=None, where=None, eager=None):
if len(itemids) == 0:
return []
itemfilter = or_(*(metatypes_table.c.parentTypeID == itemid for itemid in itemids))
itemfilter = or_(*(items_table.c.variationParentTypeID == itemid for itemid in itemids))
filter = processWhere(itemfilter, where)
joinon = items_table.c.typeID == metatypes_table.c.typeID
vars = gamedata_session.query(Item).options(*processEager(eager)).join((metatypes_table, joinon)).filter(
filter).all()
vars = get_gamedata_session().query(Item).options(*processEager(eager)).filter(filter).all()
if vars:
return vars
@@ -354,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
@@ -369,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
@@ -378,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
@@ -397,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")
@@ -410,9 +447,9 @@ 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:
@@ -420,23 +457,7 @@ def getDynamicItem(itemID, eager=None):
return result
def getRequiredFor(itemID, attrMapping):
Attribute1 = aliased(Attribute)
Attribute2 = aliased(Attribute)
skillToLevelClauses = []
for attrSkill, attrLevel in attrMapping.items():
skillToLevelClauses.append(and_(Attribute1.attributeID == attrSkill, Attribute2.attributeID == attrLevel))
queryOr = or_(*skillToLevelClauses)
q = select((Attribute2.typeID, Attribute2.value),
and_(Attribute1.value == itemID, queryOr),
from_obj=[
join(Attribute1, Attribute2, Attribute1.typeID == Attribute2.typeID)
])
result = gamedata_session.execute(q).fetchall()
return result
@cachedQuery(1, "lookfor")
def getAllImplantSets():
implantSets = get_gamedata_session().query(ImplantSet).all()
return implantSets

View File

@@ -8,43 +8,25 @@ many upgrade files as there are database versions (version 5 would include
upgrade files 1-5)
"""
import pkgutil
import re
from eos.utils.pyinst_support import iterNamespace
updates = {}
appVersion = 0
prefix = __name__ + "."
# load modules to work based with and without pyinstaller
# from: https://github.com/webcomics/dosage/blob/master/dosagelib/loader.py
# see: https://github.com/pyinstaller/pyinstaller/issues/1905
# load modules using iter_modules()
# (should find all filters in normal build, but not pyinstaller)
module_names = [m[1] for m in pkgutil.iter_modules(__path__, prefix)]
# special handling for PyInstaller
importers = map(pkgutil.get_importer, __path__)
toc = set()
for i in importers:
if hasattr(i, 'toc'):
toc |= i.toc
for elm in toc:
if elm.startswith(prefix):
module_names.append(elm)
for modname in module_names:
for modName in iterNamespace(__name__, __path__):
# loop through python files, extracting update number and function, and
# adding it to a list
modname_tail = modname.rsplit('.', 1)[-1]
module = __import__(modname, fromlist=True)
modname_tail = modName.rsplit('.', 1)[-1]
m = re.match("^upgrade(?P<index>\d+)$", modname_tail)
if not m:
continue
index = int(m.group("index"))
appVersion = max(appVersion, index)
module = __import__(modName, fromlist=True)
upgrade = getattr(module, "upgrade", False)
if upgrade:
updates[index] = upgrade

View File

@@ -33,9 +33,13 @@ def upgrade(saveddata_engine):
try:
saveddata_session.execute(commandFits_table.insert(),
{"boosterID": value, "boostedID": boosted, "active": 1})
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
pass
saveddata_session.commit()
except (KeyboardInterrupt, SystemExit):
raise
except:
# Shouldn't fail unless you have updated database without the old fleet schema and manually modify the database version
# If it does, simply fail. Fleet data migration isn't critically important here

View File

@@ -4235,6 +4235,8 @@ def upgrade(saveddata_engine):
# And last but not least, delete the last subsystem
saveddata_engine.execute("DELETE FROM modules WHERE ID = ?", (oldModules[4][0],))
except (KeyboardInterrupt, SystemExit):
raise
except:
# if something fails, fuck it, we tried. It'll default to the generic conversion below
continue

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,25 @@
"""
Migration 34
- Adds projection range columns to projectable entities
"""
import sqlalchemy
def upgrade(saveddata_engine):
try:
saveddata_engine.execute("SELECT projectionRange FROM projectedFits LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE projectedFits ADD COLUMN projectionRange FLOAT;")
try:
saveddata_engine.execute("SELECT projectionRange FROM modules LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE modules ADD COLUMN projectionRange FLOAT;")
try:
saveddata_engine.execute("SELECT projectionRange FROM drones LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE drones ADD COLUMN projectionRange FLOAT;")
try:
saveddata_engine.execute("SELECT projectionRange FROM fighters LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE fighters ADD COLUMN projectionRange FLOAT;")

View File

@@ -0,0 +1,166 @@
"""
Migration 35
- Remove builtin damage patterns and target profiles from the database
"""
import sqlalchemy
dmgPatterns = (
'Uniform',
'[Bombs]Concussion Bomb',
'[Bombs]Electron Bomb',
'[Bombs]Scorch Bomb',
'[Bombs]Shrapnel Bomb',
'[Exotic Plasma]Baryon',
'[Exotic Plasma]Meson',
'[Exotic Plasma]Tetryon',
'[Exotic Plasma][T2] Mystic',
'[Exotic Plasma][T2] Occult',
'[Frequency Crystals]Gamma',
'[Frequency Crystals]Infrared',
'[Frequency Crystals]Microwave',
'[Frequency Crystals]Multifrequency',
'[Frequency Crystals]Radio',
'[Frequency Crystals]Standard',
'[Frequency Crystals]Ultraviolet',
'[Frequency Crystals]Xray',
'[Frequency Crystals][T2] Aurora',
'[Frequency Crystals][T2] Conflagration',
'[Frequency Crystals][T2] Gleam',
'[Frequency Crystals][T2] Scorch',
'[Generic]EM',
'[Generic]Explosive',
'[Generic]Kinetic',
'[Generic]Thermal',
'[Hybrid Charges]Antimatter',
'[Hybrid Charges]Iridium',
'[Hybrid Charges]Iron',
'[Hybrid Charges]Lead',
'[Hybrid Charges]Plutonium',
'[Hybrid Charges]Thorium',
'[Hybrid Charges]Tungsten',
'[Hybrid Charges]Uranium',
'[Hybrid Charges][T2] Javelin',
'[Hybrid Charges][T2] Null',
'[Hybrid Charges][T2] Spike',
'[Hybrid Charges][T2] Void',
'[Missiles]Inferno',
'[Missiles]Mjolnir',
'[Missiles]Nova',
'[Missiles]Scourge',
'[Missiles][Structure) Standup Missile',
'[Missiles][Structure] Standup Missile',
'[NPC][Asteroid] Angel Cartel',
'[NPC][Asteroid] Blood Raiders',
'[NPC][Asteroid] Guristas',
'[NPC][Asteroid] Rogue Drone',
'[NPC][Asteroid] Sanshas Nation',
'[NPC][Asteroid] Serpentis',
'[NPC][Burner] Ashimmu (Blood Raiders)',
'[NPC][Burner] Cruor (Blood Raiders)',
'[NPC][Burner] Daredevil (Serpentis)',
'[NPC][Burner] Dramiel (Angel)',
'[NPC][Burner] Enyo',
'[NPC][Burner] Hawk',
'[NPC][Burner] Jaguar',
'[NPC][Burner] Sentinel',
'[NPC][Burner] Succubus (Sanshas Nation)',
'[NPC][Burner] Talos',
'[NPC][Burner] Vengeance',
'[NPC][Burner] Worm (Guristas)',
'[NPC][Deadspace] Angel Cartel',
'[NPC][Deadspace] Blood Raiders',
'[NPC][Deadspace] Guristas',
'[NPC][Deadspace] Rogue Drone',
'[NPC][Deadspace] Sanshas Nation',
'[NPC][Deadspace] Serpentis',
'[NPC][Mission] Amarr Empire',
'[NPC][Mission] CONCORD',
'[NPC][Mission] Caldari State',
'[NPC][Mission] Gallente Federation',
'[NPC][Mission] Khanid',
'[NPC][Mission] Minmatar Republic',
'[NPC][Mission] Mordus Legion',
'[NPC][Mission] Thukker',
'[NPC][Other] Sansha Incursion',
'[NPC][Other] Sleepers',
'[Projectile Ammo]Carbonized Lead',
'[Projectile Ammo]Depleted Uranium',
'[Projectile Ammo]EMP',
'[Projectile Ammo]Fusion',
'[Projectile Ammo]Nuclear',
'[Projectile Ammo]Phased Plasma',
'[Projectile Ammo]Proton',
'[Projectile Ammo]Titanium Sabot',
'[Projectile Ammo][T2] Barrage',
'[Projectile Ammo][T2] Hail',
'[Projectile Ammo][T2] Quake',
'[Projectile Ammo][T2] Tremor')
tgtProfiles = (
'Uniform (25%)',
'Uniform (50%)',
'Uniform (75%)',
'Uniform (90%)',
'[NPC][Asteroid] Angel Cartel',
'[NPC][Asteroid] Blood Raiders',
'[NPC][Asteroid] Guristas',
'[NPC][Asteroid] Rogue Drones',
'[NPC][Asteroid] Sanshas Nation',
'[NPC][Asteroid] Serpentis',
'[NPC][Burner] Ashimmu (Blood Raiders)',
'[NPC][Burner] Cruor (Blood Raiders)',
'[NPC][Burner] Daredevil (Serpentis)',
'[NPC][Burner] Dramiel (Angel)',
'[NPC][Burner] Enyo',
'[NPC][Burner] Hawk',
'[NPC][Burner] Jaguar',
'[NPC][Burner] Sentinel',
'[NPC][Burner] Succubus (Sanshas Nation)',
'[NPC][Burner] Talos',
'[NPC][Burner] Vengeance',
'[NPC][Burner] Worm (Guristas)',
'[NPC][Deadspace] Angel Cartel',
'[NPC][Deadspace] Blood Raiders',
'[NPC][Deadspace] Guristas',
'[NPC][Deadspace] Rogue Drones',
'[NPC][Deadspace] Sanshas Nation',
'[NPC][Deadspace] Serpentis',
'[NPC][Mission] Amarr Empire',
'[NPC][Mission] CONCORD',
'[NPC][Mission] Caldari State',
'[NPC][Mission] Gallente Federation',
'[NPC][Mission] Khanid',
'[NPC][Mission] Minmatar Republic',
'[NPC][Mission] Mordus Legion',
'[NPC][Other] Sansha Incursion',
'[NPC][Other] Sleeper',
'[T1 Resist]Armor',
'[T1 Resist]Armor (+T2 DCU)',
'[T1 Resist]Hull',
'[T1 Resist]Hull (+T2 DCU)',
'[T1 Resist]Shield',
'[T1 Resist]Shield (+T2 DCU)',
'[T2 Resist]Amarr (Armor)',
'[T2 Resist]Amarr (Shield)',
'[T2 Resist]Caldari (Armor)',
'[T2 Resist]Caldari (Shield)',
'[T2 Resist]Gallente (Armor)',
'[T2 Resist]Gallente (Shield)',
'[T2 Resist]Minmatar (Armor)',
'[T2 Resist]Minmatar (Shield)')
def upgrade(saveddata_engine):
saveddata_engine.execute('DELETE FROM damagePatterns WHERE name in ({});'.format(', '.join('\'{}\''.format(n) for n in dmgPatterns)))
saveddata_engine.execute('DELETE FROM targetResists WHERE name in ({});'.format(', '.join('\'{}\''.format(n) for n in tgtProfiles)))
try:
saveddata_engine.execute("SELECT builtinDamagePatternID FROM fits LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE fits ADD COLUMN builtinDamagePatternID INT;")
try:
saveddata_engine.execute("SELECT builtinTargetResistsID FROM fits LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE fits ADD COLUMN builtinTargetResistsID INT;")

View File

@@ -0,0 +1,84 @@
"""
Migration 36
- Shield Booster, Armor Repairer and Capacitor Transfer tiericide
"""
CONVERSIONS = {
6441: ( # Small Clarity Ward Enduring Shield Booster
6443, # Small Converse Deflection Catalyzer
),
6437: ( # Small C5-L Compact Shield Booster
6439, # Small Neutron Saturation Injector I
),
10868: ( # Medium Clarity Ward Enduring Shield Booster
10870, # Medium Converse Deflection Catalyzer
),
10872: ( # Medium C5-L Compact Shield Booster
10866, # Medium Neutron Saturation Injector I
),
10876: ( # Large Clarity Ward Enduring Shield Booster
10878, # Large Converse Deflection Catalyzer
),
10880: ( # Large C5-L Compact Shield Booster
10874, # Large Neutron Saturation Injector I
),
10884: ( # X-Large Clarity Ward Enduring Shield Booster
10886, # X-Large Converse Deflection Catalyzer
),
10888: ( # X-Large C5-L Compact Shield Booster
10882, # X-Large Neutron Saturation Injector I
),
4533: ( # Small ACM Compact Armor Repairer
4531, # Small Inefficient Armor Repair Unit
),
4529: ( # Small I-a Enduring Armor Repairer
4535, # Small Automated Carapace Restoration
),
4573: ( # Medium ACM Compact Armor Repairer
4571, # Medium Inefficient Armor Repair Unit
),
4569: ( # Medium I-a Enduring Armor Repairer
4575, # Medium Automated Carapace Restoration
),
22889: ( # 'Meditation' Medium Armor Repairer I
4579, # Medium Nano Armor Repair Unit I
),
4613: ( # Large ACM Compact Armor Repairer
4611, # Large Inefficient Armor Repair Unit
),
4609: ( # Large I-a Enduring Armor Repairer
4615, # Large Automated Carapace Restoration
),
22891: ( # 'Protest' Large Armor Repairer I
4621, # Large 'Reprieve' Vestment Reconstructer I
),
5093: ( # Small Radiative Scoped Remote Capacitor Transmitter
5087, # Small Partial E95a Remote Capacitor Transmitter
),
5091: ( # Small Inductive Compact Remote Capacitor Transmitter
5089, # Small Murky Remote Capacitor Transmitter
),
16489: ( # Medium Radiative Scoped Remote Capacitor Transmitter
16493, # Medium Partial E95b Remote Capacitor Transmitter
),
16495: ( # Medium Inductive Compact Remote Capacitor Transmitter
16491, # Medium Murky Remote Capacitor Transmitter
),
16481: ( # Large Radiative Scoped Remote Capacitor Transmitter
16485, # Large Partial E95c Remote Capacitor Transmitter
),
16487: ( # Large Inductive Compact Remote Capacitor Transmitter
16483, # Large Murky Remote Capacitor Transmitter
)
}
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,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

@@ -13,6 +13,5 @@ __all__ = [
"miscData",
"targetProfile",
"override",
"implantSet",
"loadDefaultDatabaseValues"
"implantSet"
]

View File

@@ -24,16 +24,20 @@ import datetime
from eos.db import saveddata_meta
from eos.saveddata.damagePattern import DamagePattern
damagePatterns_table = Table("damagePatterns", saveddata_meta,
Column("ID", Integer, primary_key=True),
Column("name", String),
Column("emAmount", Float),
Column("thermalAmount", Float),
Column("kineticAmount", Float),
Column("explosiveAmount", Float),
Column("ownerID", ForeignKey("users.ID"), nullable=True),
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)
)
damagePatterns_table = Table(
'damagePatterns',
saveddata_meta,
Column('ID', Integer, primary_key=True),
Column('name', String),
Column('emAmount', Float),
Column('thermalAmount', Float),
Column('kineticAmount', Float),
Column('explosiveAmount', Float),
Column('ownerID', ForeignKey('users.ID'), nullable=True),
Column('created', DateTime, nullable=True, default=datetime.datetime.now),
Column('modified', DateTime, nullable=True, onupdate=datetime.datetime.now))
mapper(DamagePattern, damagePatterns_table)
mapper(
DamagePattern,
damagePatterns_table,
properties={'rawName': damagePatterns_table.c.name})

View File

@@ -17,7 +17,7 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean, DateTime
from sqlalchemy import Table, Column, Integer, Float, ForeignKey, Boolean, DateTime
from sqlalchemy.orm import mapper, relation
import datetime
@@ -33,7 +33,8 @@ drones_table = Table("drones", saveddata_meta,
Column("amountActive", Integer, nullable=False),
Column("projected", Boolean, default=False),
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
Column("projectionRange", Float, nullable=True)
)
mapper(Drone, drones_table,

View File

@@ -17,7 +17,7 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean, DateTime
from sqlalchemy import Table, Column, Integer, Float, ForeignKey, Boolean, DateTime
from sqlalchemy.orm import mapper, relation
import datetime
@@ -34,7 +34,8 @@ fighters_table = Table("fighters", saveddata_meta,
Column("amount", Integer, nullable=False),
Column("projected", Boolean, default=False),
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
Column("projectionRange", Float, nullable=True),
)
fighter_abilities_table = Table("fightersAbilities", saveddata_meta,

View File

@@ -19,7 +19,7 @@
import datetime
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Table
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, Float, String, Table
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import mapper, reconstructor, relation, relationship
from sqlalchemy.orm.collections import attribute_mapped_collection
@@ -53,8 +53,10 @@ fits_table = Table("fits", saveddata_meta,
Column("timestamp", Integer, nullable=False),
Column("characterID", ForeignKey("characters.ID"), nullable=True),
Column("damagePatternID", ForeignKey("damagePatterns.ID"), nullable=True),
Column("builtinDamagePatternID", Integer, nullable=True),
Column("booster", Boolean, nullable=False, index=True, default=0),
Column("targetResistsID", ForeignKey("targetResists.ID"), nullable=True),
Column("builtinTargetResistsID", Integer, nullable=True),
Column("modeID", Integer, nullable=True),
Column("implantLocation", Integer, nullable=False),
Column("notes", String, nullable=True),
@@ -70,7 +72,8 @@ projectedFits_table = Table("projectedFits", saveddata_meta,
Column("amount", Integer, nullable=False, default=1),
Column("active", Boolean, nullable=False, default=1),
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
Column("projectionRange", Float, nullable=True),
)
commandFits_table = Table("commandFits", saveddata_meta,
@@ -83,6 +86,7 @@ commandFits_table = Table("commandFits", saveddata_meta,
class ProjectedFit:
def __init__(self, sourceID, source_fit, amount=1, active=True):
self.sourceID = sourceID
self.source_fit = source_fit
@@ -231,8 +235,10 @@ mapper(es_Fit, fits_table,
"_Fit__character": relation(
Character,
backref="fits"),
"_Fit__damagePattern": relation(DamagePattern),
"_Fit__targetProfile": relation(TargetProfile),
"_Fit__userDamagePattern": relation(DamagePattern),
"_Fit__builtinDamagePatternID": fits_table.c.builtinDamagePatternID,
"_Fit__userTargetProfile": relation(TargetProfile),
"_Fit__builtinTargetProfileID": fits_table.c.builtinTargetResistsID,
"projectedOnto": projectedFitSourceRel,
"victimOf": relationship(
ProjectedFit,

View File

@@ -1,223 +0,0 @@
# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
import eos.db
from eos.saveddata.damagePattern import DamagePattern as es_DamagePattern
from eos.saveddata.targetProfile import TargetProfile as es_TargetProfile
class ImportError(Exception):
pass
class DefaultDatabaseValues:
def __init__(self):
pass
instance = None
@classmethod
def importDamageProfileDefaults(cls):
damageProfileList = [["Uniform", "25", "25", "25", "25"], ["[Generic]EM", "100", "0", "0", "0"],
["[Generic]Thermal", "0", "100", "0", "0"], ["[Generic]Kinetic", "0", "0", "100", "0"],
["[Generic]Explosive", "0", "0", "0", "100"],
["[NPC][Asteroid] Blood Raiders", "5067", "4214", "0", "0"],
["[Bombs]Electron Bomb", "6400", "0", "0", "0"],
["[Bombs]Scorch Bomb", "0", "6400", "0", "0"],
["[Bombs]Concussion Bomb", "0", "0", "6400", "0"],
["[Bombs]Shrapnel Bomb", "0", "0", "0", "6400"],
["[Frequency Crystals][T2] Conflagration", "61.6", "61.6", "0", "0"],
["[Frequency Crystals][T2] Scorch", "72", "16", "0", "0"],
["[Frequency Crystals][T2] Gleam", "56", "56", "0", "0"],
["[Frequency Crystals][T2] Aurora", "40", "24", "0", "0"],
["[Frequency Crystals]Multifrequency", "61.6", "44", "0", "0"],
["[Frequency Crystals]Gamma", "61.6", "35.2", "0", "0"],
["[Frequency Crystals]Xray", "52.8", "35.2", "0", "0"],
["[Frequency Crystals]Ultraviolet", "52.8", "26.4", "0", "0"],
["[Frequency Crystals]Standard", "44", "26.4", "0", "0"],
["[Frequency Crystals]Infrared", "44", "17.6", "0", "0"],
["[Frequency Crystals]Microwave", "35.2", "17.6", "0", "0"],
["[Frequency Crystals]Radio", "44", "0", "0", "0"],
["[Hybrid Charges][T2] Void", "0", "61.6", "61.6", "0"],
["[Hybrid Charges][T2] Null", "0", "48", "40", "0"],
["[Hybrid Charges][T2] Javelin", "0", "64", "48", "0"],
["[Hybrid Charges][T2] Spike", "0", "32", "32", "0"],
["[Hybrid Charges]Antimatter", "0", "48", "67.2", "0"],
["[Hybrid Charges]Plutonium", "0", "48", "57.6", "0"],
["[Hybrid Charges]Uranium", "0", "38.4", "57.6", "0"],
["[Hybrid Charges]Thorium", "0", "38.4", "48", "0"],
["[Hybrid Charges]Lead", "0", "28.8", "48", "0"],
["[Hybrid Charges]Iridium", "0", "28.8", "38.4", "0"],
["[Hybrid Charges]Tungsten", "0", "19.2", "38.4", "0"],
["[Hybrid Charges]Iron", "0", "19.2", "28.8", "0"],
["[Missiles]Mjolnir", "100", "0", "0", "0"],
["[Missiles]Inferno", "0", "100", "0", "0"],
["[Missiles]Scourge", "0", "0", "100", "0"],
["[Missiles]Nova", "0", "0", "0", "100"],
["[Missiles][Structure] Standup Missile", "100", "100", "100", "100"],
["[Projectile Ammo][T2] Hail", "0", "0", "26.4", "96.8"],
["[Projectile Ammo][T2] Barrage", "0", "0", "40", "48"],
["[Projectile Ammo][T2] Quake", "0", "0", "40", "72"],
["[Projectile Ammo][T2] Tremor", "0", "0", "24", "40"],
["[Projectile Ammo]EMP", "79.2", "0", "8.8", "17.6"],
["[Projectile Ammo]Phased Plasma", "0", "88", "17.6", "0"],
["[Projectile Ammo]Fusion", "0", "0", "17.6", "88"],
["[Projectile Ammo]Depleted Uranium", "0", "26.4", "17.6", "26.4"],
["[Projectile Ammo]Titanium Sabot", "0", "0", "52.8", "176"],
["[Projectile Ammo]Proton", "26.4", "0", "17.6", "0"],
["[Projectile Ammo]Carbonized Lead", "0", "0", "35.2", "8.8"],
["[Projectile Ammo]Nuclear", "0", "0", "8.8", "35.2"],
# Different sizes of plasma do different damage, the values here are
# average of proportions across sizes
["[Exotic Plasma][T2] Occult", "0", "55863", "0", "44137"],
["[Exotic Plasma][T2] Mystic", "0", "66319", "0", "33681"],
["[Exotic Plasma]Tetryon", "0", "69208", "0", "30792"],
["[Exotic Plasma]Baryon", "0", "59737", "0", "40263"],
["[Exotic Plasma]Meson", "0", "60519", "0", "39481"],
["[NPC][Burner] Cruor (Blood Raiders)", "90", "90", "0", "0"],
["[NPC][Burner] Dramiel (Angel)", "55", "0", "20", "96"],
["[NPC][Burner] Daredevil (Serpentis)", "0", "110", "154", "0"],
["[NPC][Burner] Succubus (Sanshas Nation)", "135", "30", "0", "0"],
["[NPC][Burner] Worm (Guristas)", "0", "0", "228", "0"],
["[NPC][Burner] Enyo", "0", "147", "147", "0"],
["[NPC][Burner] Hawk", "0", "0", "247", "0"],
["[NPC][Burner] Jaguar", "36", "0", "50", "182"],
["[NPC][Burner] Vengeance", "232", "0", "0", "0"],
["[NPC][Burner] Ashimmu (Blood Raiders)", "260", "100", "0", "0"],
["[NPC][Burner] Talos", "0", "413", "413", "0"],
["[NPC][Burner] Sentinel", "0", "75", "0", "90"],
["[NPC][Asteroid] Angel Cartel", "1838", "562", "2215", "3838"],
["[NPC][Deadspace] Angel Cartel", "369", "533", "1395", "3302"],
["[NPC][Deadspace] Blood Raiders", "6040", "5052", "10", "15"],
["[NPC][Asteroid] Guristas", "0", "1828", "7413", "0"],
["[NPC][Deadspace] Guristas", "0", "1531", "9680", "0"],
["[NPC][Asteroid] Rogue Drone", "394", "666", "1090", "1687"],
["[NPC][Deadspace] Rogue Drone", "276", "1071", "1069", "871"],
["[NPC][Asteroid] Sanshas Nation", "5586", "4112", "0", "0"],
["[NPC][Deadspace] Sanshas Nation", "3009", "2237", "0", "0"],
["[NPC][Asteroid] Serpentis", "0", "5373", "4813", "0"],
["[NPC][Deadspace] Serpentis", "0", "3110", "1929", "0"],
["[NPC][Mission] Amarr Empire", "4464", "3546", "97", "0"],
["[NPC][Mission] Caldari State", "0", "2139", "4867", "0"],
["[NPC][Mission] CONCORD", "336", "134", "212", "412"],
["[NPC][Mission] Gallente Federation", "9", "3712", "2758", "0"],
["[NPC][Mission] Khanid", "612", "483", "43", "6"],
["[NPC][Mission] Minmatar Republic", "1024", "388", "1655", "4285"],
["[NPC][Mission] Mordus Legion", "25", "262", "625", "0"],
["[NPC][Mission] Thukker", "0", "52", "10", "79"],
["[NPC][Other] Sleepers", "1472", "1472", "1384", "1384"],
["[NPC][Other] Sansha Incursion", "1682", "1347", "3678", "3678"]]
for damageProfileRow in damageProfileList:
name, em, therm, kin, exp = damageProfileRow
damageProfile = eos.db.getDamagePattern(name)
if damageProfile is None:
damageProfile = es_DamagePattern(em, therm, kin, exp)
damageProfile.name = name
eos.db.save(damageProfile)
@classmethod
def importTargetProfileDefaults(cls):
targetProfileList = [["Uniform (25%)", "0.25", "0.25", "0.25", "0.25"],
["Uniform (50%)", "0.50", "0.50", "0.50", "0.50"],
["Uniform (75%)", "0.75", "0.75", "0.75", "0.75"],
["Uniform (90%)", "0.90", "0.90", "0.90", "0.90"],
["[T1 Resist]Shield", "0.0", "0.20", "0.40", "0.50"],
["[T1 Resist]Armor", "0.50", "0.45", "0.25", "0.10"],
["[T1 Resist]Hull", "0.33", "0.33", "0.33", "0.33"],
["[T1 Resist]Shield (+T2 DCU)", "0.125", "0.30", "0.475", "0.562"],
["[T1 Resist]Armor (+T2 DCU)", "0.575", "0.532", "0.363", "0.235"],
["[T1 Resist]Hull (+T2 DCU)", "0.598", "0.598", "0.598", "0.598"],
["[T2 Resist]Amarr (Shield)", "0.0", "0.20", "0.70", "0.875"],
["[T2 Resist]Amarr (Armor)", "0.50", "0.35", "0.625", "0.80"],
["[T2 Resist]Caldari (Shield)", "0.20", "0.84", "0.76", "0.60"],
["[T2 Resist]Caldari (Armor)", "0.50", "0.8625", "0.625", "0.10"],
["[T2 Resist]Gallente (Shield)", "0.0", "0.60", "0.85", "0.50"],
["[T2 Resist]Gallente (Armor)", "0.50", "0.675", "0.8375", "0.10"],
["[T2 Resist]Minmatar (Shield)", "0.75", "0.60", "0.40", "0.50"],
["[T2 Resist]Minmatar (Armor)", "0.90", "0.675", "0.25", "0.10"],
["[NPC][Asteroid] Angel Cartel", "0.54", "0.42", "0.37", "0.32"],
["[NPC][Asteroid] Blood Raiders", "0.34", "0.39", "0.45", "0.52"],
["[NPC][Asteroid] Guristas", "0.55", "0.35", "0.3", "0.48"],
["[NPC][Asteroid] Rogue Drones", "0.35", "0.38", "0.44", "0.49"],
["[NPC][Asteroid] Sanshas Nation", "0.35", "0.4", "0.47", "0.53"],
["[NPC][Asteroid] Serpentis", "0.49", "0.38", "0.29", "0.51"],
["[NPC][Deadspace] Angel Cartel", "0.59", "0.48", "0.4", "0.32"],
["[NPC][Deadspace] Blood Raiders", "0.31", "0.39", "0.47", "0.56"],
["[NPC][Deadspace] Guristas", "0.57", "0.39", "0.31", "0.5"],
["[NPC][Deadspace] Rogue Drones", "0.42", "0.42", "0.47", "0.49"],
["[NPC][Deadspace] Sanshas Nation", "0.31", "0.39", "0.47", "0.56"],
["[NPC][Deadspace] Serpentis", "0.49", "0.38", "0.29", "0.56"],
["[NPC][Mission] Amarr Empire", "0.34", "0.38", "0.42", "0.46"],
["[NPC][Mission] Caldari State", "0.51", "0.38", "0.3", "0.51"],
["[NPC][Mission] CONCORD", "0.47", "0.46", "0.47", "0.47"],
["[NPC][Mission] Gallente Federation", "0.51", "0.38", "0.31", "0.52"],
["[NPC][Mission] Khanid", "0.51", "0.42", "0.36", "0.4"],
["[NPC][Mission] Minmatar Republic", "0.51", "0.46", "0.41", "0.35"],
["[NPC][Mission] Mordus Legion", "0.32", "0.48", "0.4", "0.62"],
["[NPC][Other] Sleeper", "0.61", "0.61", "0.61", "0.61"],
["[NPC][Other] Sansha Incursion", "0.65", "0.63", "0.64", "0.65"],
["[NPC][Burner] Cruor (Blood Raiders)", "0.8", "0.73", "0.69", "0.67"],
["[NPC][Burner] Dramiel (Angel)", "0.35", "0.48", "0.61", "0.68"],
["[NPC][Burner] Daredevil (Serpentis)", "0.69", "0.59", "0.59", "0.43"],
["[NPC][Burner] Succubus (Sanshas Nation)", "0.35", "0.48", "0.61", "0.68"],
["[NPC][Burner] Worm (Guristas)", "0.48", "0.58", "0.69", "0.74"],
["[NPC][Burner] Enyo", "0.58", "0.72", "0.86", "0.24"],
["[NPC][Burner] Hawk", "0.3", "0.86", "0.79", "0.65"],
["[NPC][Burner] Jaguar", "0.78", "0.65", "0.48", "0.56"],
["[NPC][Burner] Vengeance", "0.66", "0.56", "0.75", "0.86"],
["[NPC][Burner] Ashimmu (Blood Raiders)", "0.8", "0.76", "0.68", "0.7"],
["[NPC][Burner] Talos", "0.68", "0.59", "0.59", "0.43"],
["[NPC][Burner] Sentinel", "0.58", "0.45", "0.52", "0.66"]]
for targetProfileRow in targetProfileList:
name = targetProfileRow[0]
em = targetProfileRow[1]
therm = targetProfileRow[2]
kin = targetProfileRow[3]
exp = targetProfileRow[4]
try:
maxVel = targetProfileRow[5]
except IndexError:
maxVel = None
try:
sigRad = targetProfileRow[6]
except IndexError:
sigRad = None
try:
radius = targetProfileRow[7]
except:
radius = None
targetProfile = eos.db.eos.db.getTargetProfile(name)
if targetProfile is None:
targetProfile = es_TargetProfile(em, therm, kin, exp, maxVel, sigRad, radius)
targetProfile.name = name
eos.db.save(targetProfile)
@classmethod
def importRequiredDefaults(cls):
damageProfileList = [["Uniform", "25", "25", "25", "25"]]
for damageProfileRow in damageProfileList:
name, em, therm, kin, exp = damageProfileRow
damageProfile = eos.db.getDamagePattern(name)
if damageProfile is None:
damageProfile = es_DamagePattern(em, therm, kin, exp)
damageProfile.name = name
eos.db.save(damageProfile)

View File

@@ -42,6 +42,7 @@ modules_table = Table("modules", saveddata_meta,
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
Column("spoolType", Integer, nullable=True),
Column("spoolAmount", Float, nullable=True),
Column("projectionRange", Float, nullable=True),
CheckConstraint('("dummySlot" = NULL OR "itemID" = NULL) AND "dummySlot" != "itemID"'))
mapper(Module, modules_table,

View File

@@ -413,7 +413,7 @@ def getDamagePattern(lookfor, eager=None):
eager = processEager(eager)
with sd_lock:
pattern = saveddata_session.query(DamagePattern).options(*eager).filter(
DamagePattern.name == lookfor).first()
DamagePattern.rawName == lookfor).first()
else:
raise TypeError("Need integer or string as argument")
return pattern
@@ -434,7 +434,7 @@ def getTargetProfile(lookfor, eager=None):
eager = processEager(eager)
with sd_lock:
pattern = saveddata_session.query(TargetProfile).options(*eager).filter(
TargetProfile.name == lookfor).first()
TargetProfile.rawName == lookfor).first()
else:
raise TypeError("Need integer or string as argument")
return pattern
@@ -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
@@ -560,6 +560,8 @@ def commit():
with sd_lock:
try:
saveddata_session.commit()
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
saveddata_session.rollback()
exc_info = sys.exc_info()
@@ -570,6 +572,8 @@ def flush():
with sd_lock:
try:
saveddata_session.flush()
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
saveddata_session.rollback()
exc_info = sys.exc_info()

View File

@@ -24,23 +24,28 @@ import datetime
from eos.db import saveddata_meta
from eos.saveddata.targetProfile import TargetProfile
targetProfiles_table = Table("targetResists", saveddata_meta,
Column("ID", Integer, primary_key=True),
Column("name", String),
Column("emAmount", Float),
Column("thermalAmount", Float),
Column("kineticAmount", Float),
Column("explosiveAmount", Float),
Column("maxVelocity", Float, nullable=True),
Column("signatureRadius", Float, nullable=True),
Column("radius", Float, nullable=True),
Column("ownerID", ForeignKey("users.ID"), nullable=True),
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)
)
mapper(TargetProfile, targetProfiles_table,
properties={
"_maxVelocity": targetProfiles_table.c.maxVelocity,
"_signatureRadius": targetProfiles_table.c.signatureRadius,
"_radius": targetProfiles_table.c.radius})
targetProfiles_table = Table(
'targetResists',
saveddata_meta,
Column('ID', Integer, primary_key=True),
Column('name', String),
Column('emAmount', Float),
Column('thermalAmount', Float),
Column('kineticAmount', Float),
Column('explosiveAmount', Float),
Column('maxVelocity', Float, nullable=True),
Column('signatureRadius', Float, nullable=True),
Column('radius', Float, nullable=True),
Column('ownerID', ForeignKey('users.ID'), nullable=True),
Column('created', DateTime, nullable=True, default=datetime.datetime.now),
Column('modified', DateTime, nullable=True, onupdate=datetime.datetime.now))
mapper(
TargetProfile,
targetProfiles_table,
properties={
'rawName': targetProfiles_table.c.name,
'_maxVelocity': targetProfiles_table.c.maxVelocity,
'_signatureRadius': targetProfiles_table.c.signatureRadius,
'_radius': targetProfiles_table.c.radius})

View File

@@ -19,6 +19,7 @@
from logbook import Logger
from sqlalchemy.orm.collections import collection
pyfalog = Logger(__name__)
@@ -138,9 +139,10 @@ class HandledModuleList(HandledList):
else:
self.appendIgnoreEmpty(mod)
@collection.appender
def appendIgnoreEmpty(self, mod):
mod.position = len(self)
HandledList.append(self, mod)
super().append(mod)
if mod.isInvalid:
self.remove(mod)

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@
# ===============================================================================
import json
from collections import OrderedDict
from logbook import Logger
@@ -183,6 +184,8 @@ class Effect(EqBase):
self.__activeByDefault = True
self.__type = None
pyfalog.error("AttributeError generating handler: {0}", e)
except (KeyboardInterrupt, SystemExit):
raise
except Exception as e:
self.__handler = eos.effects.DummyEffect.handler
self.__runTime = "normal"
@@ -199,45 +202,20 @@ class Effect(EqBase):
try:
return self.__effectDef.get(key, None)
except (KeyboardInterrupt, SystemExit):
raise
except:
return getattr(self.__effectDef, key, None)
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
@@ -259,9 +237,6 @@ class Item(EqBase):
@property
def attributes(self):
if not self.__moved:
self.moveAttrs()
return self.__attributes
@property
@@ -314,50 +289,26 @@ class Item(EqBase):
eos.db.saveddata_session.delete(override)
eos.db.commit()
srqIDMap = {182: 277, 183: 278, 184: 279, 1285: 1286, 1289: 1287, 1290: 1288}
@property
def requiredSkills(self):
if self.__requiredSkills is None:
requiredSkills = OrderedDict()
self.__requiredSkills = requiredSkills
# Map containing attribute IDs we may need for required skills
# { requiredSkillX : requiredSkillXLevel }
combinedAttrIDs = set(self.srqIDMap.keys()).union(set(self.srqIDMap.values()))
# Map containing result of the request
# { attributeID : attributeValue }
skillAttrs = {}
# Get relevant attribute values from db (required skill IDs and levels) for our item
for attrInfo in eos.db.directAttributeRequest((self.ID,), tuple(combinedAttrIDs)):
attrID = attrInfo[1]
attrVal = attrInfo[2]
skillAttrs[attrID] = attrVal
# Go through all attributeID pairs
for srqIDAtrr, srqLvlAttr in self.srqIDMap.items():
# Check if we have both in returned result
if srqIDAtrr in skillAttrs and srqLvlAttr in skillAttrs:
skillID = int(skillAttrs[srqIDAtrr])
skillLvl = skillAttrs[srqLvlAttr]
# Fetch item from database and fill map
item = eos.db.getItem(skillID)
requiredSkills[item] = skillLvl
self.__requiredSkills = {}
if self.reqskills:
for skillTypeID, skillLevel in json.loads(self.reqskills).items():
skillItem = eos.db.getItem(int(skillTypeID))
if skillItem:
self.__requiredSkills[skillItem] = skillLevel
return self.__requiredSkills
@property
def requiredFor(self):
if self.__requiredFor is None:
self.__requiredFor = dict()
# Map containing attribute IDs we may need for required skills
# Get relevant attribute values from db (required skill IDs and levels) for our item
q = eos.db.getRequiredFor(self.ID, self.srqIDMap)
for itemID, lvl in q:
# Fetch item from database and fill map
item = eos.db.getItem(itemID)
self.__requiredFor[item] = lvl
self.__requiredFor = {}
if self.requiredfor:
for typeID, skillLevel in json.loads(self.requiredfor).items():
requiredForItem = eos.db.getItem(int(typeID))
if requiredForItem:
self.__requiredFor[requiredForItem] = skillLevel
return self.__requiredFor
factionMap = {
@@ -373,7 +324,8 @@ class Item(EqBase):
500016: "sisters",
500018: "mordu",
500019: "sansha",
500020: "serpentis"
500020: "serpentis",
500026: "triglavian"
}
@property
@@ -399,6 +351,7 @@ class Item(EqBase):
9 : "guristas", # Caldari + Gallente
10 : "angelserp", # Minmatar + Gallente, final race depends on the order of skills
12 : "sisters", # Amarr + Gallente
15 : "concord",
16 : "jove",
32 : "sansha", # Incrusion Sansha
128: "ore",
@@ -530,6 +483,14 @@ class Item(EqBase):
def isBooster(self):
return self.group.name == 'Booster' and self.category.name == 'Implant'
@property
def isStandup(self):
if self.category.name == "Structure Module":
return True
if self.isFighter and {'fighterSquadronIsStandupLight', 'fighterSquadronIsStandupHeavy', 'fighterSquadronIsStandupSupport'}.intersection(self.attributes):
return True
return False
def __repr__(self):
return "Item(ID={}, name={}) at {}".format(
self.ID, self.name, hex(id(self))
@@ -601,10 +562,6 @@ class MetaGroup(EqBase):
pass
class MetaType(EqBase):
pass
class Unit(EqBase):
def __init__(self):
@@ -736,5 +693,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

@@ -17,7 +17,8 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
import collections
from collections.abc import MutableMapping
from copy import copy
from math import exp
@@ -29,6 +30,7 @@ from eos.db.gamedata.queries import getAttributeInfo
defaultValuesCache = {}
cappingAttrKeyCache = {}
resistanceCache = {}
def getAttrDefault(key, fallback=None):
@@ -46,19 +48,23 @@ def getAttrDefault(key, fallback=None):
def getResistanceAttrID(modifyingItem, effect):
# If it doesn't exist on the effect, check the modifying modules attributes. If it's there, set it on the
# effect for this session so that we don't have to look here again (won't always work when it's None, but
# will catch most)
if not effect.getattr('resistanceCalculated'):
# If it doesn't exist on the effect, check the modifying module's attributes.
# If it's there, cache it and return
if effect.resistanceID:
return effect.resistanceID
cacheKey = (modifyingItem.item.ID, effect.ID)
try:
return resistanceCache[cacheKey]
except KeyError:
attrPrefix = effect.getattr('prefix')
if attrPrefix:
effect.resistanceID = int(modifyingItem.getModifiedItemAttr('{}ResistanceID'.format(attrPrefix))) or None
if not effect.resistanceID:
effect.resistanceID = int(modifyingItem.getModifiedItemAttr('{}RemoteResistanceID'.format(attrPrefix))) or None
resistanceID = int(modifyingItem.getModifiedItemAttr('{}ResistanceID'.format(attrPrefix))) or None
if not resistanceID:
resistanceID = int(modifyingItem.getModifiedItemAttr('{}RemoteResistanceID'.format(attrPrefix))) or None
else:
effect.resistanceID = int(modifyingItem.getModifiedItemAttr("remoteResistanceID")) or None
effect.resistanceCalculated = True
return effect.resistanceID
resistanceID = int(modifyingItem.getModifiedItemAttr("remoteResistanceID")) or None
resistanceCache[cacheKey] = resistanceID
return resistanceID
class ItemAttrShortcut:
@@ -67,14 +73,8 @@ class ItemAttrShortcut:
return_value = self.itemModifiedAttributes.get(key)
return return_value or default
def getModifiedItemAttrWithExtraMods(self, key, extraMultipliers=None, default=0):
"""Returns attribute value with passed modifiers applied to it."""
return_value = self.itemModifiedAttributes.getWithExtraMods(key, extraMultipliers=extraMultipliers)
return return_value or default
def getModifiedItemAttrWithoutAfflictor(self, key, afflictor, default=0):
"""Returns attribute value with passed afflictor modification removed."""
return_value = self.itemModifiedAttributes.getWithoutAfflictor(key, afflictor)
def getModifiedItemAttrExtended(self, key, extraMultipliers=None, ignoreAfflictors=(), default=0):
return_value = self.itemModifiedAttributes.getExtended(key, extraMultipliers=extraMultipliers, ignoreAfflictors=ignoreAfflictors)
return return_value or default
def getItemBaseAttrValue(self, key, default=0):
@@ -88,14 +88,8 @@ class ChargeAttrShortcut:
return_value = self.chargeModifiedAttributes.get(key)
return return_value or default
def getModifiedChargeAttrWithExtraMods(self, key, extraMultipliers=None, default=0):
"""Returns attribute value with passed modifiers applied to it."""
return_value = self.chargeModifiedAttributes.getWithExtraMods(key, extraMultipliers=extraMultipliers)
return return_value or default
def getModifiedChargeAttrWithoutAfflictor(self, key, afflictor, default=0):
"""Returns attribute value with passed modifiers applied to it."""
return_value = self.chargeModifiedAttributes.getWithoutAfflictor(key, afflictor)
def getModifiedChargeAttrExtended(self, key, extraMultipliers=None, ignoreAfflictors=(), default=0):
return_value = self.chargeModifiedAttributes.getExtended(key, extraMultipliers=extraMultipliers, ignoreAfflictors=ignoreAfflictors)
return return_value or default
def getChargeBaseAttrValue(self, key, default=0):
@@ -103,7 +97,7 @@ class ChargeAttrShortcut:
return return_value or default
class ModifiedAttributeDict(collections.MutableMapping):
class ModifiedAttributeDict(MutableMapping):
overrides_enabled = False
class CalculationPlaceholder:
@@ -211,32 +205,11 @@ class ModifiedAttributeDict(collections.MutableMapping):
# Original value is the least priority
return self.getOriginal(key)
def getWithExtraMods(self, key, extraMultipliers=None, default=0):
"""Copy of __getitem__ with some modifications."""
if not extraMultipliers:
return self.get(key, default=default)
val = self.__calculateValue(key, extraMultipliers=extraMultipliers)
if val is not None:
return val
# Then in values which are not yet calculated
if self.__intermediary:
val = self.__intermediary.get(key)
else:
val = None
if val is not None:
return val
# Original value
val = self.getOriginal(key)
if val is not None:
return val
# Passed in default value
return default
def getWithoutAfflictor(self, key, afflictor, default=0):
def getExtended(self, key, extraMultipliers=None, ignoreAfflictors=None, default=0):
"""
Here we consider couple of parameters. If they affect final result, we do
not store result, and if they are - we do.
"""
# Here we do not have support for preAssigns/forceds, as doing them would
# mean that we have to store all of them in a list which increases memory use,
# and we do not actually need those operators atm
@@ -245,8 +218,8 @@ class ModifiedAttributeDict(collections.MutableMapping):
ignorePenalizedMultipliers = {}
postIncreaseAdjustment = 0
for fit, afflictors in self.getAfflictions(key).items():
for innerAfflictor, operator, stackingGroup, preResAmount, postResAmount, used in afflictors:
if innerAfflictor is afflictor:
for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in afflictors:
if afflictor in ignoreAfflictors:
if operator == Operator.MULTIPLY:
if stackingGroup is None:
multiplierAdjustment /= postResAmount
@@ -257,29 +230,31 @@ class ModifiedAttributeDict(collections.MutableMapping):
elif operator == Operator.POSTINCREASE:
postIncreaseAdjustment -= postResAmount
if preIncreaseAdjustment == 0 and multiplierAdjustment == 1 and postIncreaseAdjustment == 0 and len(ignorePenalizedMultipliers) == 0:
# If we apply no customizations - use regular getter
if (
not extraMultipliers and
preIncreaseAdjustment == 0 and multiplierAdjustment == 1 and
postIncreaseAdjustment == 0 and len(ignorePenalizedMultipliers) == 0
):
return self.get(key, default=default)
# Try to calculate custom values
val = self.__calculateValue(
key, preIncAdj=preIncreaseAdjustment, multAdj=multiplierAdjustment,
key, extraMultipliers=extraMultipliers, preIncAdj=preIncreaseAdjustment, multAdj=multiplierAdjustment,
postIncAdj=postIncreaseAdjustment, ignorePenMult=ignorePenalizedMultipliers)
if val is not None:
return val
# Then in values which are not yet calculated
# Then the same fallbacks as in regular getter
if self.__intermediary:
val = self.__intermediary.get(key)
else:
val = None
if val is not None:
return val
# Original value
val = self.getOriginal(key)
if val is not None:
return val
# Passed in default value
return default
def __delitem__(self, key):
@@ -584,10 +559,12 @@ class ModifiedAttributeDict(collections.MutableMapping):
if 'projected' not in effectType:
return 1
remoteResistID = getResistanceAttrID(modifyingItem=fit.getModifier(), effect=effect)
if not remoteResistID:
return 1
attrInfo = getAttributeInfo(remoteResistID)
# Get the attribute of the resist
resist = fit.ship.itemModifiedAttributes[attrInfo.attributeName] or None
return resist or 1.0
return resist or 1
class Affliction:

View File

@@ -122,7 +122,7 @@ class Booster(HandledItem, ItemAttrShortcut):
(effect.isType("passive") or effect.isType("boosterSideEffect")):
if effect.isType("boosterSideEffect") and effect not in self.activeSideEffectEffects:
continue
effect.handler(fit, self, ("booster",))
effect.handler(fit, self, ("booster",), None, effect=effect)
@validates("ID", "itemID", "ammoID", "active")
def validator(self, key, val):

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

@@ -422,7 +422,7 @@ class Skill(HandledItem):
(not fit.isStructure or effect.isType("structure")) and \
effect.activeByDefault:
try:
effect.handler(fit, self, ("skill",))
effect.handler(fit, self, ("skill",), None, effect=effect)
except AttributeError:
continue

View File

@@ -18,21 +18,174 @@
# ===============================================================================
import re
from collections import OrderedDict
from sqlalchemy.orm import reconstructor
import eos.db
# Order is significant here - UI uses order as-is for built-in patterns
BUILTINS = OrderedDict([
(-1, ('Uniform', 25, 25, 25, 25)),
(-2, ('[Generic]EM', 1, 0, 0, 0)),
(-3, ('[Generic]Thermal', 0, 1, 0, 0)),
(-4, ('[Generic]Kinetic', 0, 0, 1, 0)),
(-5, ('[Generic]Explosive', 0, 0, 0, 1)),
(-6, ('[Frequency Crystals]|[T2] Aurora', 5, 3, 0, 0)),
(-7, ('[Frequency Crystals]|[T2] Scorch', 9, 2, 0, 0)),
(-8, ('[Frequency Crystals]Radio', 5, 0, 0, 0)),
(-9, ('[Frequency Crystals]Microwave', 4, 2, 0, 0)),
(-10, ('[Frequency Crystals]Infrared', 5, 2, 0, 0)),
(-11, ('[Frequency Crystals]Standard', 5, 3, 0, 0)),
(-12, ('[Frequency Crystals]Ultraviolet', 6, 3, 0, 0)),
(-13, ('[Frequency Crystals]Xray', 6, 4, 0, 0)),
(-14, ('[Frequency Crystals]Gamma', 7, 4, 0, 0)),
(-15, ('[Frequency Crystals]Multifrequency', 7, 5, 0, 0)),
(-16, ('[Frequency Crystals]|[T2] Gleam', 7, 7, 0, 0)),
(-17, ('[Frequency Crystals]|[T2] Conflagration', 7.7, 7.7, 0, 0)),
# Different sizes of plasma do different damage ratios, the values here
# are average of ratios across sizes
(-18, ('[Exotic Plasma]|[T2] Mystic', 0, 66319, 0, 33681)),
(-19, ('[Exotic Plasma]Meson', 0, 60519, 0, 39481)),
(-20, ('[Exotic Plasma]Baryon', 0, 59737, 0, 40263)),
(-21, ('[Exotic Plasma]Tetryon', 0, 69208, 0, 30792)),
(-22, ('[Exotic Plasma]|[T2] Occult', 0, 55863, 0, 44137)),
(-23, ('[Hybrid Charges]|[T2] Spike', 0, 4, 4, 0)),
(-24, ('[Hybrid Charges]|[T2] Null', 0, 6, 5, 0)),
(-25, ('[Hybrid Charges]Iron', 0, 2, 3, 0)),
(-26, ('[Hybrid Charges]Tungsten', 0, 2, 4, 0)),
(-27, ('[Hybrid Charges]Iridium', 0, 3, 4, 0)),
(-28, ('[Hybrid Charges]Lead', 0, 3, 5, 0)),
(-29, ('[Hybrid Charges]Thorium', 0, 4, 5, 0)),
(-30, ('[Hybrid Charges]Uranium', 0, 4, 6, 0)),
(-31, ('[Hybrid Charges]Plutonium', 0, 5, 6, 0)),
(-32, ('[Hybrid Charges]Antimatter', 0, 5, 7, 0)),
(-33, ('[Hybrid Charges]|[T2] Javelin', 0, 8, 6, 0)),
(-34, ('[Hybrid Charges]|[T2] Void', 0, 7.7, 7.7, 0)),
(-35, ('[Projectile Ammo]|[T2] Tremor', 0, 0, 3, 5)),
(-36, ('[Projectile Ammo]|[T2] Barrage', 0, 0, 5, 6)),
(-37, ('[Projectile Ammo]Carbonized Lead', 0, 0, 4, 1)),
(-38, ('[Projectile Ammo]Nuclear', 0, 0, 1, 4)),
(-39, ('[Projectile Ammo]Proton', 3, 0, 2, 0)),
(-40, ('[Projectile Ammo]Depleted Uranium', 0, 3, 2, 3)),
(-41, ('[Projectile Ammo]Titanium Sabot', 0, 0, 6, 2)),
(-42, ('[Projectile Ammo]EMP', 9, 0, 1, 2)),
(-43, ('[Projectile Ammo]Phased Plasma', 0, 10, 2, 0)),
(-44, ('[Projectile Ammo]Fusion', 0, 0, 2, 10)),
(-45, ('[Projectile Ammo]|[T2] Quake', 0, 0, 5, 9)),
(-46, ('[Projectile Ammo]|[T2] Hail', 0, 0, 3.3, 12.1)),
(-47, ('[Missiles]Mjolnir', 1, 0, 0, 0)),
(-48, ('[Missiles]Inferno', 0, 1, 0, 0)),
(-49, ('[Missiles]Scourge', 0, 0, 1, 0)),
(-50, ('[Missiles]Nova', 0, 0, 0, 1)),
(-51, ('[Bombs]Electron Bomb', 6400, 0, 0, 0)),
(-52, ('[Bombs]Scorch Bomb', 0, 6400, 0, 0)),
(-53, ('[Bombs]Concussion Bomb', 0, 0, 6400, 0)),
(-54, ('[Bombs]Shrapnel Bomb', 0, 0, 0, 6400)),
# Source: ticket #2067
(-55, ('[NPC][Abyssal]All', 130, 396, 258, 216)),
(-56, ('[NPC][Abyssal]Drifter', 250, 250, 250, 250)),
(-57, ('[NPC][Abyssal]Drones', 250, 250, 250, 250)),
(-58, ('[NPC][Abyssal]Overmind', 0, 408, 592, 0)),
(-59, ('[NPC][Abyssal]Seeker', 406, 406, 94, 94)),
(-60, ('[NPC][Abyssal]Sleeper', 313, 313, 187, 187)),
(-61, ('[NPC][Abyssal]Triglavian', 0, 610, 0, 390)),
(-62, ('[NPC][Asteroid]Angel Cartel', 1838, 562, 2215, 3838)),
(-63, ('[NPC][Asteroid]Blood Raiders', 5067, 4214, 0, 0)),
(-64, ('[NPC][Asteroid]Guristas', 0, 1828, 7413, 0)),
(-65, ('[NPC][Asteroid]Rogue Drone', 394, 666, 1090, 1687)),
(-66, ('[NPC][Asteroid]Sanshas Nation', 5586, 4112, 0, 0)),
(-67, ('[NPC][Asteroid]Serpentis', 0, 5373, 4813, 0)),
(-68, ('[NPC][Burner]Cruor (Blood Raiders)', 90, 90, 0, 0)),
(-69, ('[NPC][Burner]Dramiel (Angel)', 55, 0, 20, 96)),
(-70, ('[NPC][Burner]Daredevil (Serpentis)', 0, 110, 154, 0)),
(-71, ('[NPC][Burner]Succubus (Sanshas Nation)', 135, 30, 0, 0)),
(-72, ('[NPC][Burner]Worm (Guristas)', 0, 0, 228, 0)),
(-73, ('[NPC][Burner]Enyo', 0, 147, 147, 0)),
(-74, ('[NPC][Burner]Hawk', 0, 0, 247, 0)),
(-75, ('[NPC][Burner]Jaguar', 36, 0, 50, 182)),
(-76, ('[NPC][Burner]Vengeance', 232, 0, 0, 0)),
(-77, ('[NPC][Burner]Ashimmu (Blood Raiders)', 260, 100, 0, 0)),
(-78, ('[NPC][Burner]Talos', 0, 413, 413, 0)),
(-79, ('[NPC][Burner]Sentinel', 0, 75, 0, 90)),
(-80, ('[NPC][Deadspace]Angel Cartel', 369, 533, 1395, 3302)),
(-81, ('[NPC][Deadspace]Blood Raiders', 6040, 5052, 10, 15)),
(-82, ('[NPC][Deadspace]Guristas', 0, 1531, 9680, 0)),
(-83, ('[NPC][Deadspace]Rogue Drone', 276, 1071, 1069, 871)),
(-84, ('[NPC][Deadspace]Sanshas Nation', 3009, 2237, 0, 0)),
(-85, ('[NPC][Deadspace]Serpentis', 0, 3110, 1929, 0)),
# Source: ticket #2067
(-86, ('[NPC][Invasion][Invading Precursor Entities]Dread', 0, 417, 0, 583)),
(-87, ('[NPC][Invasion][Invading Precursor Entities]Normal Subcaps', 0, 610, 0, 390)),
(-88, ('[NPC][Invasion][Invading Precursor Entities]Subcaps w/missiles 0% spool up', 367, 155, 367, 112)),
(-89, ('[NPC][Invasion][Invading Precursor Entities]Subcaps w/missiles 50% spool up', 291, 243, 291, 175)),
(-90, ('[NPC][Invasion][Invading Precursor Entities]Subcaps w/missiles 100% spool up', 241, 301, 241, 217)),
(-91, ('[NPC][Invasion][Retaliating Amarr Entities]Dread/Subcaps', 583, 417, 0, 0)),
(-92, ('[NPC][Invasion][Retaliating Caldari Entities]Dread', 1000, 0, 0, 0)),
(-93, ('[NPC][Invasion][Retaliating Caldari Entities]Subcaps', 511, 21, 29, 440)),
(-94, ('[NPC][Invasion][Retaliating Gallente Entities]Dread/Subcaps', 0, 417, 583, 0)),
(-95, ('[NPC][Invasion][Retaliating Minmatar Entities]Dread', 0, 0, 583, 417)),
(-96, ('[NPC][Invasion][Retaliating Minmatar Entities]Subcaps', 302, 136, 328, 234)),
(-97, ('[NPC][Mission]Amarr Empire', 4464, 3546, 97, 0)),
(-98, ('[NPC][Mission]Caldari State', 0, 2139, 4867, 0)),
(-99, ('[NPC][Mission]CONCORD', 336, 134, 212, 412)),
(-100, ('[NPC][Mission]Gallente Federation', 9, 3712, 2758, 0)),
(-101, ('[NPC][Mission]Khanid', 612, 483, 43, 6)),
(-102, ('[NPC][Mission]Minmatar Republic', 1024, 388, 1655, 4285)),
(-103, ('[NPC][Mission]Mordus Legion', 25, 262, 625, 0)),
(-104, ('[NPC][Mission]Thukker', 0, 52, 10, 79)),
(-105, ('[NPC]Sansha Incursion', 1682, 1347, 3678, 3678)),
(-106, ('[NPC]Sleepers', 1472, 1472, 1384, 1384))])
class DamagePattern:
DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive")
DAMAGE_TYPES = ('em', 'thermal', 'kinetic', 'explosive')
_builtins = None
def __init__(self, *args, **kwargs):
self.builtin = False
self.update(*args, **kwargs)
@reconstructor
def init(self):
self.builtin = False
def update(self, emAmount=25, thermalAmount=25, kineticAmount=25, explosiveAmount=25):
self.emAmount = emAmount
self.thermalAmount = thermalAmount
self.kineticAmount = kineticAmount
self.explosiveAmount = explosiveAmount
@classmethod
def getBuiltinList(cls):
if cls._builtins is None:
cls.__generateBuiltins()
return list(cls._builtins.values())
@classmethod
def getBuiltinById(cls, id):
if cls._builtins is None:
cls.__generateBuiltins()
return cls._builtins.get(id)
@classmethod
def getDefaultBuiltin(cls):
if cls._builtins is None:
cls.__generateBuiltins()
return cls._builtins.get(-1)
@classmethod
def __generateBuiltins(cls):
cls._builtins = OrderedDict()
for id, (rawName, em, therm, kin, explo) in BUILTINS.items():
pattern = DamagePattern(emAmount=em, thermalAmount=therm, kineticAmount=kin, explosiveAmount=explo)
pattern.ID = id
pattern.rawName = rawName
pattern.builtin = True
cls._builtins[id] = pattern
def calculateEhp(self, fit):
ehp = {}
for (type, attr) in (('shield', 'shieldCapacity'), ('armor', 'armorHP'), ('hull', 'hp')):
@@ -78,6 +231,15 @@ class DamagePattern:
"exp" : "explosive"
}
@classmethod
def oneType(cls, damageType, amount=100):
pattern = DamagePattern()
pattern.update(amount if damageType == "em" else 0,
amount if damageType == "thermal" else 0,
amount if damageType == "kinetic" else 0,
amount if damageType == "explosive" else 0)
return pattern
@classmethod
def importPatterns(cls, text):
lines = re.split('[\n\r]+', text)
@@ -89,7 +251,7 @@ class DamagePattern:
lookup = {}
current = eos.db.getDamagePatternList()
for pattern in current:
lookup[pattern.name] = pattern
lookup[pattern.rawName] = pattern
for line in lines:
try:
@@ -98,6 +260,8 @@ class DamagePattern:
line = line.split('#', 1)[0] # allows for comments
type, data = line.rsplit('=', 1)
type, data = type.strip(), data.split(',')
except (KeyboardInterrupt, SystemExit):
raise
except:
# Data isn't in correct format, continue to next line
continue
@@ -112,6 +276,8 @@ class DamagePattern:
for index, val in enumerate(data):
try:
fields["%sAmount" % cls.DAMAGE_TYPES[index]] = int(val)
except (KeyboardInterrupt, SystemExit):
raise
except:
continue
@@ -122,7 +288,7 @@ class DamagePattern:
eos.db.save(pattern)
else:
pattern = DamagePattern(**fields)
pattern.name = name.strip()
pattern.rawName = name.strip()
eos.db.save(pattern)
patterns.append(pattern)
@@ -138,11 +304,41 @@ class DamagePattern:
out += "# Values are in following format:\n"
out += "# DamageProfile = [name],[EM amount],[Thermal amount],[Kinetic amount],[Explosive amount]\n\n"
for dp in patterns:
out += cls.EXPORT_FORMAT % (dp.name, dp.emAmount, dp.thermalAmount, dp.kineticAmount, dp.explosiveAmount)
out += cls.EXPORT_FORMAT % (dp.rawName, dp.emAmount, dp.thermalAmount, dp.kineticAmount, dp.explosiveAmount)
return out.strip()
@property
def name(self):
return self.rawName
@property
def fullName(self):
categories, tail = self.__parseRawName()
return '{}{}'.format(''.join('[{}]'.format(c) for c in categories), tail)
@property
def shortName(self):
return self.__parseRawName()[1]
@property
def hierarchy(self):
return self.__parseRawName()[0]
def __parseRawName(self):
categories = []
remainingName = self.rawName.strip() if self.rawName else ''
while True:
start, end = remainingName.find('['), remainingName.find(']')
if start == -1 or end == -1:
return categories, remainingName
splitter = remainingName.find('|')
if splitter != -1 and splitter == start - 1:
return categories, remainingName[1:]
categories.append(remainingName[start + 1:end])
remainingName = remainingName[end + 1:].strip()
def __deepcopy__(self, memo):
p = DamagePattern(self.emAmount, self.thermalAmount, self.kineticAmount, self.explosiveAmount)
p.name = "%s copy" % self.name
p.rawName = "%s copy" % self.rawName
return p

View File

@@ -18,6 +18,7 @@
# ===============================================================================
import math
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
@@ -25,6 +26,7 @@ import eos.db
from eos.effectHandlerHelpers import HandledCharge, HandledItem
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
from eos.utils.cycles import CycleInfo
from eos.utils.default import DEFAULT
from eos.utils.stats import DmgTypes, RRTypes
@@ -45,6 +47,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.amount = 0
self.amountActive = 0
self.projected = False
self.projectionRange = None
self.build()
@reconstructor
@@ -304,7 +307,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
else:
return True
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False):
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False, forcedProjRange=DEFAULT):
if self.projected or forceProjected:
context = "projected", "drone"
projected = True
@@ -312,6 +315,8 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
context = ("drone",)
projected = False
projectionRange = self.projectionRange if forcedProjRange is DEFAULT else forcedProjRange
for effect in self.item.effects.values():
if effect.runTime == runTime and \
effect.activeByDefault and \
@@ -319,36 +324,34 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
projected is False and effect.isType("passive")):
# See GH issue #765
if effect.getattr('grouped'):
try:
effect.handler(fit, self, context, effect=effect)
except:
effect.handler(fit, self, context)
effect.handler(fit, self, context, projectionRange, effect=effect)
else:
i = 0
while i != self.amountActive:
try:
effect.handler(fit, self, context, effect=effect)
except:
effect.handler(fit, self, context)
effect.handler(fit, self, context, projectionRange, effect=effect)
i += 1
if self.charge:
for effect in self.charge.effects.values():
if effect.runTime == runTime and effect.activeByDefault:
effect.handler(fit, self, ("droneCharge",))
effect.handler(fit, self, ("droneCharge",), projectionRange, effect=effect)
def __deepcopy__(self, memo):
copy = Drone(self.item)
copy.amount = self.amount
copy.amountActive = self.amountActive
copy.projectionRange = self.projectionRange
return copy
def rebase(self, item):
amount = self.amount
amountActive = self.amountActive
projectionRange = self.projectionRange
Drone.__init__(self, item)
self.amount = amount
self.amountActive = amountActive
self.projectionRange = projectionRange
def fits(self, fit):
fitDroneGroupLimits = set()

View File

@@ -18,6 +18,7 @@
# ===============================================================================
import math
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
@@ -27,8 +28,9 @@ from eos.effectHandlerHelpers import HandledCharge, HandledItem
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
from eos.saveddata.fighterAbility import FighterAbility
from eos.utils.cycles import CycleInfo, CycleSequence
from eos.utils.stats import DmgTypes
from eos.utils.default import DEFAULT
from eos.utils.float import floatUnerr
from eos.utils.stats import DmgTypes
pyfalog = Logger(__name__)
@@ -47,6 +49,7 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.itemID = item.ID if item is not None else None
self.projected = False
self.projectionRange = None
self.active = True
# -1 is a placeholder that represents max squadron size, which we may not know yet as ships may modify this with
@@ -96,7 +99,7 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__chargeModifiedAttributes = ModifiedAttributeDict()
if len(self.abilities) != len(self.item.effects):
if {a.effectID for a in self.abilities} != {e.ID for e in self.item.effects.values()}:
self.__abilities = []
for ability in self.__getAbilities():
self.__abilities.append(ability)
@@ -380,7 +383,7 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
else:
return True
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False):
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False, forcedProjRange=DEFAULT):
if not self.active:
return
@@ -391,6 +394,8 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
context = ("fighter",)
projected = False
projectionRange = self.projectionRange if forcedProjRange is DEFAULT else forcedProjRange
for ability in self.abilities:
if not ability.active:
continue
@@ -399,17 +404,11 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if effect.runTime == runTime and effect.activeByDefault and \
((projected and effect.isType("projected")) or not projected):
if ability.grouped:
try:
effect.handler(fit, self, context, effect=effect)
except:
effect.handler(fit, self, context)
effect.handler(fit, self, context, projectionRange, effect=effect)
else:
i = 0
while i != self.amount:
try:
effect.handler(fit, self, context, effect=effect)
except:
effect.handler(fit, self, context)
effect.handler(fit, self, context, projectionRange, effect=effect)
i += 1
def __deepcopy__(self, memo):
@@ -419,18 +418,22 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
for ability in self.abilities:
copyAbility = next(filter(lambda a: a.effectID == ability.effectID, copy.abilities))
copyAbility.active = ability.active
copy.projectionRange = self.projectionRange
return copy
def rebase(self, item):
amount = self._amount
active = self.active
abilityEffectStates = {a.effectID: a.active for a in self.abilities}
projectionRange = self.projectionRange
Fighter.__init__(self, item)
self._amount = amount
self.active = active
for ability in self.abilities:
if ability.effectID in abilityEffectStates:
ability.active = abilityEffectStates[ability.effectID]
self.projectionRange = projectionRange
def fits(self, fit):
# If ships doesn't support this type of fighter, don't add it

View File

@@ -21,21 +21,24 @@ import datetime
import time
from copy import deepcopy
from itertools import chain
from math import log, sqrt
from logbook import Logger
from math import asinh, log, sqrt
from sqlalchemy.orm import reconstructor, validates
import eos.db
from eos import capSim
from eos.calc import calculateLockTime, calculateMultiplier
from eos.const import CalcType, FitSystemSecurity, FittingHardpoint, FittingModuleState, FittingSlot, ImplantLocation
from eos.effectHandlerHelpers import (
HandledBoosterList, HandledDroneCargoList, HandledImplantList,
HandledModuleList, HandledProjectedDroneList, HandledProjectedModList)
from eos.saveddata.character import Character
from eos.saveddata.citadel import Citadel
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.stats import DmgTypes, RRTypes
@@ -162,14 +165,29 @@ class Fit:
self.__capUsed = None
self.__capRecharge = None
self.__savedCapSimData.clear()
# Ancillary tank modules affect this
self.__sustainableTank = None
self.__effectiveSustainableTank = None
@property
def targetProfile(self):
return self.__targetProfile
if self.__userTargetProfile is not None:
return self.__userTargetProfile
if self.__builtinTargetProfileID is not None:
return TargetProfile.getBuiltinById(self.__builtinTargetProfileID)
return None
@targetProfile.setter
def targetProfile(self, targetProfile):
self.__targetProfile = targetProfile
if targetProfile is None:
self.__userTargetProfile = None
self.__builtinTargetProfileID = None
elif targetProfile.builtin:
self.__userTargetProfile = None
self.__builtinTargetProfileID = targetProfile.ID
else:
self.__userTargetProfile = targetProfile
self.__builtinTargetProfileID = None
self.__weaponDpsMap = {}
self.__weaponVolleyMap = {}
self.__droneDps = None
@@ -177,11 +195,25 @@ class Fit:
@property
def damagePattern(self):
return self.__damagePattern
if self.__userDamagePattern is not None:
return self.__userDamagePattern
if self.__builtinDamagePatternID is not None:
pattern = DamagePattern.getBuiltinById(self.__builtinDamagePatternID)
if pattern is not None:
return pattern
return DamagePattern.getDefaultBuiltin()
@damagePattern.setter
def damagePattern(self, damagePattern):
self.__damagePattern = damagePattern
if damagePattern is None:
self.__userDamagePattern = None
self.__builtinDamagePatternID = None
elif damagePattern.builtin:
self.__userDamagePattern = None
self.__builtinDamagePatternID = damagePattern.ID
else:
self.__userDamagePattern = damagePattern
self.__builtinDamagePatternID = None
self.__ehp = None
self.__effectiveTank = None
@@ -440,10 +472,8 @@ class Fit:
return False
# Citadel modules are now under a new category, so we can check this to ensure only structure modules can fit on a citadel
if isinstance(self.ship, Citadel) and item.category.name != "Structure Module" or \
not isinstance(self.ship, Citadel) and item.category.name == "Structure Module":
if isinstance(self.ship, Citadel) is not item.isStandup:
return False
return True
def clear(self, projected=False, command=False):
@@ -546,11 +576,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)
@@ -560,12 +594,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)
@@ -934,13 +972,20 @@ class Fit:
To support a simpler way of doing self projections (so that we don't have to make a copy of the fit and
recalculate), this function was developed to be a common source of projected effect application.
"""
c = chain(self.drones, self.fighters, self.modules)
for item in c:
for item in chain(self.drones, self.fighters):
if item is not None:
# apply effects onto target fit x amount of times
for _ in range(projectionInfo.amount):
targetFit.register(item, origin=self)
item.calculateModifiedAttributes(targetFit, runTime, True)
item.calculateModifiedAttributes(
targetFit, runTime, forceProjected=True,
forcedProjRange=0)
for mod in self.modules:
for _ in range(projectionInfo.amount):
targetFit.register(mod, origin=self)
mod.calculateModifiedAttributes(
targetFit, runTime, forceProjected=True,
forcedProjRange=projectionInfo.projectionRange)
def fill(self):
"""
@@ -981,6 +1026,16 @@ class Fit:
if mod.isEmpty:
del self.modules[i]
def clearTail(self):
tailPositions = {}
for mod in 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
@@ -1090,7 +1145,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
@@ -1098,7 +1153,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
@@ -1219,8 +1274,8 @@ class Fit:
# Signature reduction, uses the bomb formula as per CCP Larrikin
if energyNeutralizerSignatureResolution:
capNeed = capNeed * min(1, signatureRadius / energyNeutralizerSignatureResolution)
self.__extraDrains.append((cycleTime, capNeed, clipSize, reloadTime))
if capNeed:
self.__extraDrains.append((cycleTime, capNeed, clipSize, reloadTime))
def removeDrain(self, i):
del self.__extraDrains[i]
@@ -1320,8 +1375,8 @@ class Fit:
"""Return how much cap regen do we gain from having this module"""
currentRegen = self.calculateCapRecharge()
nomodRegen = self.calculateCapRecharge(
capacity=self.ship.getModifiedItemAttrWithoutAfflictor("capacitorCapacity", mod),
rechargeRate=self.ship.getModifiedItemAttrWithoutAfflictor("rechargeRate", mod) / 1000.0)
capacity=self.ship.getModifiedItemAttrExtended("capacitorCapacity", ignoreAfflictors=[mod]),
rechargeRate=self.ship.getModifiedItemAttrExtended("rechargeRate", ignoreAfflictors=[mod]) / 1000.0)
return currentRegen - nomodRegen
def getRemoteReps(self, spoolOptions=None):
@@ -1524,9 +1579,7 @@ class Fit:
def calculateLockTime(self, radius):
scanRes = self.ship.getModifiedItemAttr("scanResolution")
if scanRes is not None and scanRes > 0:
# Yes, this function returns time in seconds, not miliseconds.
# 40,000 is indeed the correct constant here.
return min(40000 / scanRes / asinh(radius) ** 2, 30 * 60)
return calculateLockTime(srcScanRes=scanRes, tgtSigRadius=radius)
else:
return self.ship.getModifiedItemAttr("scanSpeed") / 1000.0
@@ -1621,6 +1674,22 @@ class Fit:
if ability.active:
yield fighter, ability
def getDampMultScanRes(self):
damps = []
for mod in self.activeModulesIter():
for effectName in ('remoteSensorDampFalloff', 'structureModuleEffectRemoteSensorDampener'):
if effectName in mod.item.effects:
damps.append((mod.getModifiedItemAttr('scanResolutionBonus'), 'default'))
if 'doomsdayAOEDamp' in mod.item.effects:
damps.append((mod.getModifiedItemAttr('scanResolutionBonus'), 'default'))
for drone in self.activeDronesIter():
if 'remoteSensorDampEntity' in drone.item.effects:
damps.extend(drone.amountActive * ((drone.getModifiedItemAttr('scanResolutionBonus'), 'default'),))
mults = {}
for strength, stackingGroup in damps:
mults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
return calculateMultiplier(mults)
def __deepcopy__(self, memo=None):
fitCopy = Fit()
# Character and owner are not copied
@@ -1671,6 +1740,8 @@ class Fit:
copyProjectionInfo = fit.getProjectionInfo(fitCopy.ID)
originalProjectionInfo = fit.getProjectionInfo(self.ID)
copyProjectionInfo.active = originalProjectionInfo.active
copyProjectionInfo.amount = originalProjectionInfo.amount
copyProjectionInfo.projectionRange = originalProjectionInfo.projectionRange
forceUpdateSavedata(fit)
return fitCopy

View File

@@ -95,7 +95,7 @@ class Implant(HandledItem, ItemAttrShortcut):
return
for effect in self.item.effects.values():
if effect.runTime == runTime and effect.isType("passive") and effect.activeByDefault:
effect.handler(fit, self, ("implant",))
effect.handler(fit, self, ("implant",), None, effect=effect)
@validates("fitID", "itemID", "active")
def validator(self, key, val):

View File

@@ -54,7 +54,7 @@ class Mode(ItemAttrShortcut, HandledItem):
if self.item:
for effect in self.item.effects.values():
if effect.runTime == runTime and effect.activeByDefault:
effect.handler(fit, self, context=("module",))
effect.handler(fit, self, ("module",), None, effect=effect)
def __deepcopy__(self, memo):
copy = Mode(self.item)

View File

@@ -17,8 +17,9 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from logbook import Logger
import math
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
import eos.db
@@ -28,6 +29,7 @@ from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, Modi
from eos.saveddata.citadel import Citadel
from eos.saveddata.mutator import Mutator
from eos.utils.cycles import CycleInfo, CycleSequence
from eos.utils.default import DEFAULT
from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import calculateSpoolup, resolveSpoolOptions
from eos.utils.stats import DmgTypes, RRTypes
@@ -90,6 +92,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.__charge = None
self.projected = False
self.projectionRange = None
self.state = FittingModuleState.ONLINE
self.build()
@@ -211,8 +214,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:
@@ -315,36 +318,68 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
"energyDestabilizationRange", "empFieldRange",
"ecmBurstRange", "warpScrambleRange", "cargoScanRange",
"shipScanRange", "surveyScanRange")
maxRange = None
for attr in attrs:
maxRange = self.getModifiedItemAttr(attr, None)
if maxRange is not None:
return maxRange
if self.charge is not None:
try:
chargeName = self.charge.group.name
except AttributeError:
pass
else:
if chargeName in ("Scanner Probe", "Survey Probe"):
return None
break
if maxRange is not None:
if 'burst projector' in self.item.name.lower():
maxRange -= self.owner.ship.getModifiedItemAttr("radius")
return maxRange
missileMaxRangeData = self.missileMaxRangeData
if missileMaxRangeData is None:
return None
lowerRange, higherRange, higherChance = missileMaxRangeData
maxRange = lowerRange * (1 - higherChance) + higherRange * higherChance
return maxRange
@property
def missileMaxRangeData(self):
if self.charge is None:
return None
try:
chargeName = self.charge.group.name
except AttributeError:
pass
else:
if chargeName in ("Scanner Probe", "Survey Probe"):
return None
def calculateRange(maxVelocity, mass, agility, flightTime):
# Source: http://www.eveonline.com/ingameboard.asp?a=topic&threadID=1307419&page=1#15
# D_m = V_m * (T_m + T_0*[exp(- T_m/T_0)-1])
maxVelocity = self.getModifiedChargeAttr("maxVelocity")
flightTime = self.getModifiedChargeAttr("explosionDelay") / 1000.0
mass = self.getModifiedChargeAttr("mass")
agility = self.getModifiedChargeAttr("agility")
if maxVelocity and (flightTime or mass or agility):
accelTime = min(flightTime, mass * agility / 1000000)
# Average distance done during acceleration
duringAcceleration = maxVelocity / 2 * accelTime
# Distance done after being at full speed
fullSpeed = maxVelocity * (flightTime - accelTime)
maxRange = duringAcceleration + fullSpeed
if 'fofMissileLaunching' in self.charge.effects:
rangeLimit = self.getModifiedChargeAttr("maxFOFTargetRange")
if rangeLimit:
maxRange = min(maxRange, rangeLimit)
return maxRange
accelTime = min(flightTime, mass * agility / 1000000)
# Average distance done during acceleration
duringAcceleration = maxVelocity / 2 * accelTime
# Distance done after being at full speed
fullSpeed = maxVelocity * (flightTime - accelTime)
maxRange = duringAcceleration + fullSpeed
return maxRange
maxVelocity = self.getModifiedChargeAttr("maxVelocity")
if not maxVelocity:
return None
shipRadius = self.owner.ship.getModifiedItemAttr("radius")
# Flight time has bonus based on ship radius, see https://github.com/pyfa-org/Pyfa/issues/2083
flightTime = floatUnerr(self.getModifiedChargeAttr("explosionDelay") / 1000 + shipRadius / maxVelocity)
mass = self.getModifiedChargeAttr("mass")
agility = self.getModifiedChargeAttr("agility")
lowerTime = math.floor(flightTime)
higherTime = math.ceil(flightTime)
lowerRange = calculateRange(maxVelocity, mass, agility, lowerTime)
higherRange = calculateRange(maxVelocity, mass, agility, higherTime)
# Fof range limit is supposedly calculated based on overview (surface-to-surface) range
if 'fofMissileLaunching' in self.charge.effects:
rangeLimit = self.getModifiedChargeAttr("maxFOFTargetRange")
if rangeLimit:
lowerRange = min(lowerRange, rangeLimit)
higherRange = min(higherRange, rangeLimit)
# Make range center-to-surface, as missiles spawn in the center of the ship
lowerRange = max(0, lowerRange - shipRadius)
higherRange = max(0, higherRange - shipRadius)
higherChance = flightTime - lowerTime
return lowerRange, higherRange, higherChance
@property
def falloff(self):
@@ -661,7 +696,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
@@ -743,8 +778,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
@@ -837,7 +872,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.itemModifiedAttributes.clear()
self.chargeModifiedAttributes.clear()
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False, gang=False):
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False, gang=False, forcedProjRange=DEFAULT):
# We will run the effect when two conditions are met:
# 1: It makes sense to run the effect
# The effect is either offline
@@ -854,6 +889,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
context = ("module",)
projected = False
projectionRange = self.projectionRange if forcedProjRange is DEFAULT else forcedProjRange
if self.charge is not None:
# fix for #82 and it's regression #106
if not projected or (self.projected and not forceProjected) or gang:
@@ -867,13 +904,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
(not gang or (gang and effect.isType("gang")))
):
contexts = ("moduleCharge",)
# For gang effects, we pass in the effect itself as an argument. However, to avoid going through all
# the effect definitions and defining this argument, do a simple try/catch here and be done with it.
# @todo: possibly fix this
try:
effect.handler(fit, self, contexts, effect=effect)
except:
effect.handler(fit, self, contexts)
effect.handler(fit, self, contexts, projectionRange, effect=effect)
if self.item:
if self.state >= FittingModuleState.OVERHEATED:
@@ -883,7 +914,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
and not forceProjected \
and effect.activeByDefault \
and ((gang and effect.isType("gang")) or not gang):
effect.handler(fit, self, context)
effect.handler(fit, self, context, projectionRange, effect=effect)
for effect in self.item.effects.values():
if effect.runTime == runTime and \
@@ -893,10 +924,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
(effect.isType("active") and self.state >= FittingModuleState.ACTIVE)) \
and ((projected and effect.isType("projected")) or not projected) \
and ((gang and effect.isType("gang")) or not gang):
try:
effect.handler(fit, self, context, effect=effect)
except:
effect.handler(fit, self, context)
effect.handler(fit, self, context, projectionRange, effect=effect)
def getCycleParameters(self, reloadOverride=None):
"""Copied from new eos as well"""
@@ -1027,6 +1055,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
copy.state = self.state
copy.spoolType = self.spoolType
copy.spoolAmount = self.spoolAmount
copy.projectionRange = self.projectionRange
for x in self.mutators.values():
Mutator(copy, x.attribute, x.value)
@@ -1036,10 +1065,17 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def rebase(self, item):
state = self.state
charge = self.charge
spoolType = self.spoolType
spoolAmount = self.spoolAmount
projectionRange = self.projectionRange
Module.__init__(self, item, self.baseItem, self.mutaplasmid)
self.state = state
if self.isValidCharge(charge):
self.charge = charge
self.spoolType = spoolType
self.spoolAmount = spoolAmount
self.projectionRange = projectionRange
for x in self.mutators.values():
Mutator(self, x.attribute, x.value)

View File

@@ -73,12 +73,16 @@ class Mutator(EqBase):
self.dynamicAttribute = next(a for a in self.module.mutaplasmid.attributes if a.attributeID == self.attrID)
# base attribute links to the base ite's attribute for this mutated definition (contains original, base value)
self.baseAttribute = self.module.item.attributes[self.dynamicAttribute.name]
except (KeyboardInterrupt, SystemExit):
raise
except:
self.module = None
@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

@@ -98,7 +98,7 @@ class Ship(ItemAttrShortcut, HandledItem):
# skillbook modifiers will use the stale modifier value
# GH issue #351
fit.register(self)
effect.handler(fit, self, ("ship",))
effect.handler(fit, self, ("ship",), None, effect=effect)
def validateModeItem(self, item, owner=None):
""" Checks if provided item is a valid mode """

View File

@@ -19,8 +19,10 @@
import math
import re
from collections import OrderedDict
from logbook import Logger
from sqlalchemy.orm import reconstructor
import eos.db
@@ -28,13 +30,173 @@ import eos.db
pyfalog = Logger(__name__)
BUILTINS = OrderedDict([
# 0 is taken by ideal target profile, composed manually in one of TargetProfile methods
(-1, ('Uniform (25%)', 0.25, 0.25, 0.25, 0.25)),
(-2, ('Uniform (50%)', 0.50, 0.50, 0.50, 0.50)),
(-3, ('Uniform (75%)', 0.75, 0.75, 0.75, 0.75)),
(-4, ('Uniform (90%)', 0.90, 0.90, 0.90, 0.90)),
(-5, ('[T1 Resist]Shield', 0.0, 0.20, 0.40, 0.50)),
(-6, ('[T1 Resist]Armor', 0.50, 0.45, 0.25, 0.10)),
(-7, ('[T1 Resist]Hull', 0.33, 0.33, 0.33, 0.33)),
(-8, ('[T1 Resist]Shield (+T2 DCU)', 0.125, 0.30, 0.475, 0.562)),
(-9, ('[T1 Resist]Armor (+T2 DCU)', 0.575, 0.532, 0.363, 0.235)),
(-10, ('[T1 Resist]Hull (+T2 DCU)', 0.598, 0.598, 0.598, 0.598)),
(-11, ('[T2 Resist]Amarr (Shield)', 0.0, 0.20, 0.70, 0.875)),
(-12, ('[T2 Resist]Amarr (Armor)', 0.50, 0.35, 0.625, 0.80)),
(-13, ('[T2 Resist]Caldari (Shield)', 0.20, 0.84, 0.76, 0.60)),
(-14, ('[T2 Resist]Caldari (Armor)', 0.50, 0.8625, 0.625, 0.10)),
(-15, ('[T2 Resist]Gallente (Shield)', 0.0, 0.60, 0.85, 0.50)),
(-16, ('[T2 Resist]Gallente (Armor)', 0.50, 0.675, 0.8375, 0.10)),
(-17, ('[T2 Resist]Minmatar (Shield)', 0.75, 0.60, 0.40, 0.50)),
(-18, ('[T2 Resist]Minmatar (Armor)', 0.90, 0.675, 0.25, 0.10)),
(-19, ('[NPC][Asteroid]Angel Cartel', 0.54, 0.42, 0.37, 0.32)),
(-20, ('[NPC][Asteroid]Blood Raiders', 0.34, 0.39, 0.45, 0.52)),
(-21, ('[NPC][Asteroid]Guristas', 0.55, 0.35, 0.3, 0.48)),
(-22, ('[NPC][Asteroid]Rogue Drones', 0.35, 0.38, 0.44, 0.49)),
(-23, ('[NPC][Asteroid]Sanshas Nation', 0.35, 0.4, 0.47, 0.53)),
(-24, ('[NPC][Asteroid]Serpentis', 0.49, 0.38, 0.29, 0.51)),
(-25, ('[NPC][Deadspace]Angel Cartel', 0.59, 0.48, 0.4, 0.32)),
(-26, ('[NPC][Deadspace]Blood Raiders', 0.31, 0.39, 0.47, 0.56)),
(-27, ('[NPC][Deadspace]Guristas', 0.57, 0.39, 0.31, 0.5)),
(-28, ('[NPC][Deadspace]Rogue Drones', 0.42, 0.42, 0.47, 0.49)),
(-29, ('[NPC][Deadspace]Sanshas Nation', 0.31, 0.39, 0.47, 0.56)),
(-30, ('[NPC][Deadspace]Serpentis', 0.49, 0.38, 0.29, 0.56)),
(-31, ('[NPC][Mission]Amarr Empire', 0.34, 0.38, 0.42, 0.46)),
(-32, ('[NPC][Mission]Caldari State', 0.51, 0.38, 0.3, 0.51)),
(-33, ('[NPC][Mission]CONCORD', 0.47, 0.46, 0.47, 0.47)),
(-34, ('[NPC][Mission]Gallente Federation', 0.51, 0.38, 0.31, 0.52)),
(-35, ('[NPC][Mission]Khanid', 0.51, 0.42, 0.36, 0.4)),
(-36, ('[NPC][Mission]Minmatar Republic', 0.51, 0.46, 0.41, 0.35)),
(-37, ('[NPC][Mission]Mordus Legion', 0.32, 0.48, 0.4, 0.62)),
(-38, ('[NPC][Other]Sleeper', 0.61, 0.61, 0.61, 0.61)),
(-39, ('[NPC][Other]Sansha Incursion', 0.65, 0.63, 0.64, 0.65)),
(-40, ('[NPC][Burner]Cruor (Blood Raiders)', 0.8, 0.73, 0.69, 0.67)),
(-41, ('[NPC][Burner]Dramiel (Angel)', 0.35, 0.48, 0.61, 0.68)),
(-42, ('[NPC][Burner]Daredevil (Serpentis)', 0.69, 0.59, 0.59, 0.43)),
(-43, ('[NPC][Burner]Succubus (Sanshas Nation)', 0.35, 0.48, 0.61, 0.68)),
(-44, ('[NPC][Burner]Worm (Guristas)', 0.48, 0.58, 0.69, 0.74)),
(-45, ('[NPC][Burner]Enyo', 0.58, 0.72, 0.86, 0.24)),
(-46, ('[NPC][Burner]Hawk', 0.3, 0.86, 0.79, 0.65)),
(-47, ('[NPC][Burner]Jaguar', 0.78, 0.65, 0.48, 0.56)),
(-48, ('[NPC][Burner]Vengeance', 0.66, 0.56, 0.75, 0.86)),
(-49, ('[NPC][Burner]Ashimmu (Blood Raiders)', 0.8, 0.76, 0.68, 0.7)),
(-50, ('[NPC][Burner]Talos', 0.68, 0.59, 0.59, 0.43)),
(-51, ('[NPC][Burner]Sentinel', 0.58, 0.45, 0.52, 0.66)),
# 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)),
(-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)),
(-58, ('[NPC][Abyssal][Dark Matter All Tiers]Overmind', 0.626, 0.576, 0.612, 0.624)),
(-59, ('[NPC][Abyssal][Dark Matter All Tiers]Seeker', 0.082, 0.082, 0.082, 0.082)),
(-60, ('[NPC][Abyssal][Dark Matter All Tiers]Triglavian', 0.477, 0.401, 0.449, 0.37)),
(-61, ('[NPC][Abyssal][Dark Matter All Tiers]Drifter', 0.403, 0.403, 0.403, 0.403)),
(-62, ('[NPC][Abyssal][Dark Matter All Tiers]Sleeper', 0.435, 0.435, 0.435, 0.435)),
(-63, ('[NPC][Abyssal][Dark Matter All Tiers]All', 0.507, 0.477, 0.502, 0.493)),
(-64, ('[NPC][Abyssal][Electrical T1/T2]Drones', 0.323, 0.522, 0.529, 0.435)),
(-65, ('[NPC][Abyssal][Electrical T1/T2]Overmind', 0.521, 0.576, 0.612, 0.624)),
(-66, ('[NPC][Abyssal][Electrical T1/T2]Seeker', 0, 0.082, 0.082, 0.082)),
(-67, ('[NPC][Abyssal][Electrical T1/T2]Triglavian', 0.333, 0.401, 0.449, 0.37)),
(-68, ('[NPC][Abyssal][Electrical T1/T2]Drifter', 0.267, 0.403, 0.403, 0.403)),
(-69, ('[NPC][Abyssal][Electrical T1/T2]Sleeper', 0.329, 0.435, 0.435, 0.435)),
(-70, ('[NPC][Abyssal][Electrical T1/T2]All', 0.385, 0.477, 0.502, 0.493)),
(-71, ('[NPC][Abyssal][Electrical T3 (Some T5 Rooms)]Drones', 0.255, 0.522, 0.529, 0.435)),
(-72, ('[NPC][Abyssal][Electrical T3 (Some T5 Rooms)]Overmind', 0.457, 0.576, 0.612, 0.624)),
(-73, ('[NPC][Abyssal][Electrical T3 (Some T5 Rooms)]Seeker', 0, 0.082, 0.082, 0.082)),
(-74, ('[NPC][Abyssal][Electrical T3 (Some T5 Rooms)]Triglavian', 0.241, 0.401, 0.449, 0.37)),
(-75, ('[NPC][Abyssal][Electrical T3 (Some T5 Rooms)]Drifter', 0.184, 0.403, 0.403, 0.403)),
(-76, ('[NPC][Abyssal][Electrical T3 (Some T5 Rooms)]Sleeper', 0.268, 0.435, 0.435, 0.435)),
(-77, ('[NPC][Abyssal][Electrical T3 (Some T5 Rooms)]All', 0.313, 0.477, 0.502, 0.493)),
(-78, ('[NPC][Abyssal][Electrical T4/T5]Drones', 0.193, 0.522, 0.529, 0.435)),
(-79, ('[NPC][Abyssal][Electrical T4/T5]Overmind', 0.398, 0.576, 0.612, 0.624)),
(-80, ('[NPC][Abyssal][Electrical T4/T5]Seeker', 0, 0.082, 0.082, 0.082)),
(-81, ('[NPC][Abyssal][Electrical T4/T5]Triglavian', 0.183, 0.401, 0.449, 0.37)),
(-82, ('[NPC][Abyssal][Electrical T4/T5]Drifter', 0.107, 0.403, 0.403, 0.403)),
(-83, ('[NPC][Abyssal][Electrical T4/T5]Sleeper', 0.215, 0.435, 0.435, 0.435)),
(-84, ('[NPC][Abyssal][Electrical T4/T5]All', 0.25, 0.477, 0.502, 0.493)),
(-85, ('[NPC][Abyssal][Firestorm T1/T2]Drones', 0.461, 0.425, 0.541, 0.443)),
(-86, ('[NPC][Abyssal][Firestorm T1/T2]Overmind', 0.65, 0.469, 0.625, 0.633)),
(-87, ('[NPC][Abyssal][Firestorm T1/T2]Seeker', 0.084, 0, 0.084, 0.084)),
(-88, ('[NPC][Abyssal][Firestorm T1/T2]Triglavian', 0.534, 0.266, 0.484, 0.366)),
(-89, ('[NPC][Abyssal][Firestorm T1/T2]Drifter', 0.422, 0.282, 0.422, 0.422)),
(-90, ('[NPC][Abyssal][Firestorm T1/T2]Sleeper', 0.512, 0.402, 0.512, 0.512)),
(-91, ('[NPC][Abyssal][Firestorm T1/T2]All', 0.541, 0.365, 0.524, 0.504)),
(-92, ('[NPC][Abyssal][Firestorm T3 (Some T5 Rooms)]Drones', 0.461, 0.36, 0.541, 0.443)),
(-93, ('[NPC][Abyssal][Firestorm T3 (Some T5 Rooms)]Overmind', 0.65, 0.391, 0.625, 0.633)),
(-94, ('[NPC][Abyssal][Firestorm T3 (Some T5 Rooms)]Seeker', 0.084, 0, 0.084, 0.084)),
(-95, ('[NPC][Abyssal][Firestorm T3 (Some T5 Rooms)]Triglavian', 0.534, 0.161, 0.484, 0.366)),
(-96, ('[NPC][Abyssal][Firestorm T3 (Some T5 Rooms)]Drifter', 0.422, 0.196, 0.422, 0.422)),
(-97, ('[NPC][Abyssal][Firestorm T3 (Some T5 Rooms)]Sleeper', 0.512, 0.337, 0.512, 0.512)),
(-98, ('[NPC][Abyssal][Firestorm T3 (Some T5 Rooms)]All', 0.541, 0.284, 0.524, 0.504)),
(-99, ('[NPC][Abyssal][Firestorm T4/T5]Drones', 0.461, 0.305, 0.541, 0.443)),
(-100, ('[NPC][Abyssal][Firestorm T4/T5]Overmind', 0.65, 0.323, 0.625, 0.633)),
(-101, ('[NPC][Abyssal][Firestorm T4/T5]Seeker', 0.084, 0, 0.084, 0.084)),
(-102, ('[NPC][Abyssal][Firestorm T4/T5]Triglavian', 0.534, 0.082, 0.484, 0.366)),
(-103, ('[NPC][Abyssal][Firestorm T4/T5]Drifter', 0.422, 0.114, 0.422, 0.422)),
(-104, ('[NPC][Abyssal][Firestorm T4/T5]Sleeper', 0.512, 0.276, 0.512, 0.512)),
(-105, ('[NPC][Abyssal][Firestorm T4/T5]All', 0.541, 0.214, 0.524, 0.504)),
(-106, ('[NPC][Abyssal][Exotic T1/T2]Drones', 0.439, 0.522, 0.417, 0.435)),
(-107, ('[NPC][Abyssal][Exotic T1/T2]Overmind', 0.626, 0.576, 0.496, 0.624)),
(-108, ('[NPC][Abyssal][Exotic T1/T2]Seeker', 0.082, 0.082, 0, 0.082)),
(-109, ('[NPC][Abyssal][Exotic T1/T2]Triglavian', 0.477, 0.401, 0.284, 0.37)),
(-110, ('[NPC][Abyssal][Exotic T1/T2]Drifter', 0.403, 0.403, 0.267, 0.403)),
(-111, ('[NPC][Abyssal][Exotic T1/T2]Sleeper', 0.435, 0.435, 0.329, 0.435)),
(-112, ('[NPC][Abyssal][Exotic T1/T2]All', 0.507, 0.477, 0.373, 0.493)),
(-113, ('[NPC][Abyssal][Exotic T3 (Some T5 Rooms)]Drones', 0.439, 0.522, 0.351, 0.435)),
(-114, ('[NPC][Abyssal][Exotic T3 (Some T5 Rooms)]Overmind', 0.626, 0.576, 0.419, 0.624)),
(-115, ('[NPC][Abyssal][Exotic T3 (Some T5 Rooms)]Seeker', 0.082, 0.082, 0, 0.082)),
(-116, ('[NPC][Abyssal][Exotic T3 (Some T5 Rooms)]Triglavian', 0.477, 0.401, 0.176, 0.37)),
(-117, ('[NPC][Abyssal][Exotic T3 (Some T5 Rooms)]Drifter', 0.403, 0.403, 0.184, 0.403)),
(-118, ('[NPC][Abyssal][Exotic T3 (Some T5 Rooms)]Sleeper', 0.435, 0.435, 0.268, 0.435)),
(-119, ('[NPC][Abyssal][Exotic T3 (Some T5 Rooms)]All', 0.507, 0.477, 0.293, 0.493)),
(-120, ('[NPC][Abyssal][Exotic T4/T5]Drones', 0.439, 0.522, 0.293, 0.435)),
(-121, ('[NPC][Abyssal][Exotic T4/T5]Overmind', 0.626, 0.576, 0.344, 0.624)),
(-122, ('[NPC][Abyssal][Exotic T4/T5]Seeker', 0.082, 0.082, 0, 0.082)),
(-123, ('[NPC][Abyssal][Exotic T4/T5]Triglavian', 0.477, 0.401, 0.107, 0.37)),
(-124, ('[NPC][Abyssal][Exotic T4/T5]Drifter', 0.403, 0.403, 0.107, 0.403)),
(-125, ('[NPC][Abyssal][Exotic T4/T5]Sleeper', 0.435, 0.435, 0.215, 0.435)),
(-126, ('[NPC][Abyssal][Exotic T4/T5]All', 0.507, 0.477, 0.223, 0.493)),
(-127, ('[NPC][Abyssal][Gamma T1/T2]Drones', 0.449, 0.54, 0.549, 0.336)),
(-128, ('[NPC][Abyssal][Gamma T1/T2]Overmind', 0.6, 0.557, 0.601, 0.504)),
(-129, ('[NPC][Abyssal][Gamma T1/T2]Seeker', 0.085, 0.085, 0.085, 0)),
(-130, ('[NPC][Abyssal][Gamma T1/T2]Triglavian', 0.463, 0.392, 0.447, 0.193)),
(-131, ('[NPC][Abyssal][Gamma T1/T2]Drifter', 0.428, 0.428, 0.428, 0.287)),
(-132, ('[NPC][Abyssal][Gamma T1/T2]Sleeper', 0.435, 0.435, 0.435, 0.329)),
(-133, ('[NPC][Abyssal][Gamma T1/T2]All', 0.493, 0.472, 0.5, 0.362)),
(-134, ('[NPC][Abyssal][Gamma T3 (Some T5 Rooms)]Drones', 0.449, 0.54, 0.549, 0.264)),
(-135, ('[NPC][Abyssal][Gamma T3 (Some T5 Rooms)]Overmind', 0.6, 0.557, 0.601, 0.428)),
(-136, ('[NPC][Abyssal][Gamma T3 (Some T5 Rooms)]Seeker', 0.085, 0.085, 0.085, 0)),
(-137, ('[NPC][Abyssal][Gamma T3 (Some T5 Rooms)]Triglavian', 0.463, 0.392, 0.447, 0.071)),
(-138, ('[NPC][Abyssal][Gamma T3 (Some T5 Rooms)]Drifter', 0.428, 0.428, 0.428, 0.2)),
(-139, ('[NPC][Abyssal][Gamma T3 (Some T5 Rooms)]Sleeper', 0.435, 0.435, 0.435, 0.268)),
(-140, ('[NPC][Abyssal][Gamma T3 (Some T5 Rooms)]All', 0.493, 0.472, 0.5, 0.28)),
(-141, ('[NPC][Abyssal][Gamma T4/T5]Drones', 0.449, 0.54, 0.549, 0.197)),
(-142, ('[NPC][Abyssal][Gamma T4/T5]Overmind', 0.6, 0.557, 0.601, 0.356)),
(-143, ('[NPC][Abyssal][Gamma T4/T5]Seeker', 0.085, 0.085, 0.085, 0)),
(-144, ('[NPC][Abyssal][Gamma T4/T5]Triglavian', 0.463, 0.392, 0.447, 0.029)),
(-145, ('[NPC][Abyssal][Gamma T4/T5]Drifter', 0.428, 0.428, 0.428, 0.117)),
(-146, ('[NPC][Abyssal][Gamma T4/T5]Sleeper', 0.435, 0.435, 0.435, 0.215)),
(-147, ('[NPC][Abyssal][Gamma T4/T5]All', 0.493, 0.472, 0.5, 0.21))])
class TargetProfile:
# also determined import/export order - VERY IMPORTANT
DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive")
DAMAGE_TYPES = ('em', 'thermal', 'kinetic', 'explosive')
_idealTarget = None
_builtins = None
def __init__(self, *args, **kwargs):
self.builtin = False
self.update(*args, **kwargs)
@reconstructor
def init(self):
self.builtin = False
def update(self, emAmount=0, thermalAmount=0, kineticAmount=0, explosiveAmount=0, maxVelocity=None, signatureRadius=None, radius=None):
self.emAmount = emAmount
self.thermalAmount = thermalAmount
@@ -44,7 +206,29 @@ class TargetProfile:
self._signatureRadius = signatureRadius
self._radius = radius
_idealTarget = None
@classmethod
def getBuiltinList(cls):
if cls._builtins is None:
cls.__generateBuiltins()
return list(cls._builtins.values())
@classmethod
def getBuiltinById(cls, id):
if cls._builtins is None:
cls.__generateBuiltins()
return cls._builtins.get(id)
@classmethod
def __generateBuiltins(cls):
cls._builtins = OrderedDict()
for id, data in BUILTINS.items():
rawName = data[0]
data = data[1:]
profile = TargetProfile(*data)
profile.ID = id
profile.rawName = rawName
profile.builtin = True
cls._builtins[id] = profile
@classmethod
def getIdeal(cls):
@@ -57,8 +241,9 @@ class TargetProfile:
maxVelocity=0,
signatureRadius=None,
radius=0)
cls._idealTarget.name = 'Ideal Target'
cls._idealTarget.ID = -1
cls._idealTarget.rawName = 'Ideal Target'
cls._idealTarget.ID = 0
cls._idealTarget.builtin = True
return cls._idealTarget
@property
@@ -77,6 +262,8 @@ class TargetProfile:
@signatureRadius.setter
def signatureRadius(self, val):
if val is not None and math.isinf(val):
val = None
self._signatureRadius = val
@property
@@ -98,7 +285,7 @@ class TargetProfile:
lookup = {}
current = eos.db.getTargetProfileList()
for pattern in current:
lookup[pattern.name] = pattern
lookup[pattern.rawName] = pattern
for line in lines:
try:
@@ -106,7 +293,9 @@ class TargetProfile:
continue
line = line.split('#', 1)[0] # allows for comments
type, data = line.rsplit('=', 1)
type, data = type.strip(), data.split(',')
type, data = type.strip(), [d.strip() for d in data.split(',')]
except (KeyboardInterrupt, SystemExit):
raise
except:
pyfalog.warning("Data isn't in correct format, continue to next line.")
continue
@@ -115,26 +304,41 @@ class TargetProfile:
continue
numPatterns += 1
name, data = data[0], data[1:5]
name, dataRes, dataMisc = data[0], data[1:5], data[5:8]
fields = {}
for index, val in enumerate(data):
val = float(val)
for index, val in enumerate(dataRes):
val = float(val) if val else 0
if math.isinf(val):
val = 0
try:
assert 0 <= val <= 100
fields["%sAmount" % cls.DAMAGE_TYPES[index]] = val / 100
except (KeyboardInterrupt, SystemExit):
raise
except:
pyfalog.warning("Caught unhandled exception in import patterns.")
continue
if len(fields) == 4: # Avoid possible blank lines
if len(dataMisc) == 3:
for index, val in enumerate(dataMisc):
try:
fieldName = ("maxVelocity", "signatureRadius", "radius")[index]
except IndexError:
break
val = float(val) if val else 0
if fieldName != "signatureRadius" and math.isinf(val):
val = 0
fields[fieldName] = val
if len(fields) in (4, 7): # Avoid possible blank lines
if name.strip() in lookup:
pattern = lookup[name.strip()]
pattern.update(**fields)
eos.db.save(pattern)
else:
pattern = TargetProfile(**fields)
pattern.name = name.strip()
pattern.rawName = name.strip()
eos.db.save(pattern)
patterns.append(pattern)
@@ -142,27 +346,60 @@ class TargetProfile:
return patterns, numPatterns
EXPORT_FORMAT = "TargetProfile = %s,%.1f,%.1f,%.1f,%.1f\n"
EXPORT_FORMAT = "TargetProfile = %s,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f\n"
@classmethod
def exportPatterns(cls, *patterns):
out = "# Exported from pyfa\n#\n"
out += "# Values are in following format:\n"
out += "# TargetProfile = [name],[EM %],[Thermal %],[Kinetic %],[Explosive %]\n\n"
out += "# TargetProfile = [name],[EM %],[Thermal %],[Kinetic %],[Explosive %],[Max velocity m/s],[Signature radius m],[Radius m]\n\n"
for dp in patterns:
out += cls.EXPORT_FORMAT % (
dp.name,
dp.rawName,
dp.emAmount * 100,
dp.thermalAmount * 100,
dp.kineticAmount * 100,
dp.explosiveAmount * 100
dp.explosiveAmount * 100,
dp.maxVelocity,
dp.signatureRadius,
dp.radius
)
return out.strip()
@property
def name(self):
return self.rawName
@property
def fullName(self):
categories, tail = self.__parseRawName()
return '{}{}'.format(''.join('[{}]'.format(c) for c in categories), tail)
@property
def shortName(self):
return self.__parseRawName()[1]
@property
def hierarchy(self):
return self.__parseRawName()[0]
def __parseRawName(self):
hierarchy = []
remainingName = self.rawName.strip() if self.rawName else ''
while True:
start, end = remainingName.find('['), remainingName.find(']')
if start == -1 or end == -1:
return hierarchy, remainingName
splitter = remainingName.find('|')
if splitter != -1 and splitter == start - 1:
return hierarchy, remainingName[1:]
hierarchy.append(remainingName[start + 1:end])
remainingName = remainingName[end + 1:].strip()
def __deepcopy__(self, memo):
p = TargetProfile(
self.emAmount, self.thermalAmount, self.kineticAmount, self.explosiveAmount,
self._maxVelocity, self._signatureRadius, self._radius)
p.name = "%s copy" % self.name
p.rawName = "%s copy" % self.rawName
return p

3
eos/utils/default.py Normal file
View File

@@ -0,0 +1,3 @@
class DEFAULT:
"""Singleton class to signify default argument value."""
pass

View File

@@ -0,0 +1,39 @@
"""
Slightly modified version of function taken from here:
https://github.com/pyinstaller/pyinstaller/issues/1905#issuecomment-525221546
"""
import pkgutil
def iterNamespace(name, path):
"""Pyinstaller-compatible namespace iteration.
Yields the name of all modules found at a given Fully-qualified path.
To have it running with pyinstaller, it requires to ensure a hook inject the
"hidden" modules from your plugins folder inside the executable:
- if your plugins are under the ``myappname/pluginfolder`` module
- create a file ``specs/hook-<myappname.pluginfolder>.py``
- content of this file should be:
.. code-block:: python
from PyInstaller.utils.hooks import collect_submodules
hiddenimports = collect_submodules('<myappname.pluginfolder>')
"""
prefix = name + "."
for p in pkgutil.iter_modules(path, prefix):
yield p[1]
# special handling when the package is bundled with PyInstaller 3.5
# See https://github.com/pyinstaller/pyinstaller/issues/1905#issuecomment-445787510
toc = set()
for importer in pkgutil.iter_importers(name.partition(".")[0]):
if hasattr(importer, 'toc'):
toc |= importer.toc
for name in toc:
if name.startswith(prefix):
yield name

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

@@ -38,8 +38,9 @@ def calculateSpoolup(modMaxValue, modStepValue, modCycleTime, spoolType, spoolAm
if not modMaxValue or not modStepValue:
return 0, 0, 0
if spoolType == SpoolType.SPOOL_SCALE:
# For spool scale, round to closest cycle for scaled spool amount
cycles = round(spoolAmount * modMaxValue / modStepValue)
# Find out at which point of spoolup scale we're on, find out how many cycles
# is enough to reach it and recalculate spoolup value for that amount of cycles
cycles = math.ceil(floatUnerr(modMaxValue * spoolAmount / modStepValue))
spoolValue = min(modMaxValue, cycles * modStepValue)
return spoolValue, cycles, cycles * modCycleTime
elif spoolType == SpoolType.CYCLE_SCALE:

View File

@@ -46,11 +46,11 @@ class DmgTypes:
# Round for comparison's sake because often damage profiles are
# generated from data which includes float errors
return (
floatUnerr(self.em) == floatUnerr(other.em) and
floatUnerr(self.thermal) == floatUnerr(other.thermal) and
floatUnerr(self.kinetic) == floatUnerr(other.kinetic) and
floatUnerr(self.explosive) == floatUnerr(other.explosive) and
floatUnerr(self.total) == floatUnerr(other.total))
floatUnerr(self.em) == floatUnerr(other.em) and
floatUnerr(self.thermal) == floatUnerr(other.thermal) and
floatUnerr(self.kinetic) == floatUnerr(other.kinetic) and
floatUnerr(self.explosive) == floatUnerr(other.explosive) and
floatUnerr(self.total) == floatUnerr(other.total))
def __bool__(self):
return any((
@@ -110,9 +110,19 @@ class DmgTypes:
return self
def __repr__(self):
spec = ['em', 'thermal', 'kinetic', 'explosive', 'total']
spec = DmgTypes.names()
spec.append('total')
return makeReprStr(self, spec)
@staticmethod
def names(short=None, postProcessor=None):
value = ['em', 'th', 'kin', 'exp'] if short else ['em', 'thermal', 'kinetic', 'explosive']
if postProcessor:
value = [postProcessor(x) for x in value]
return value
class RRTypes:
"""Container for tank data stats."""
@@ -136,10 +146,10 @@ class RRTypes:
# Round for comparison's sake because often tanking numbers are
# generated from data which includes float errors
return (
floatUnerr(self.shield) == floatUnerr(other.shield) and
floatUnerr(self.armor) == floatUnerr(other.armor) and
floatUnerr(self.hull) == floatUnerr(other.hull) and
floatUnerr(self.capacitor) == floatUnerr(other.capacitor))
floatUnerr(self.shield) == floatUnerr(other.shield) and
floatUnerr(self.armor) == floatUnerr(other.armor) and
floatUnerr(self.hull) == floatUnerr(other.hull) and
floatUnerr(self.capacitor) == floatUnerr(other.capacitor))
def __bool__(self):
return any((self.shield, self.armor, self.hull, self.capacitor))
@@ -191,5 +201,17 @@ class RRTypes:
return self
def __repr__(self):
spec = ['shield', 'armor', 'hull', 'capacitor']
spec = RRTypes.names(False)
return makeReprStr(self, spec)
@staticmethod
def names(ehpOnly=True, postProcessor=None):
value = ['shield', 'armor', 'hull']
if not ehpOnly:
value.append('capacitor')
if postProcessor:
value = [postProcessor(x) for x in value]
return value

BIN
eve.db

Binary file not shown.

View File

@@ -18,48 +18,20 @@
# =============================================================================
import math
from service.settings import GraphSettings
def calculateRangeFactor(srcOptimalRange, srcFalloffRange, distance, restrictedRange=True):
"""Range strength/chance factor, applicable to guns, ewar, RRs, etc."""
def checkLockRange(src, distance):
if distance is None:
return 1
if srcFalloffRange > 0:
# Most modules cannot be activated when at 3x falloff range, with few exceptions like guns
if restrictedRange and distance > srcOptimalRange + 3 * srcFalloffRange:
return 0
return 0.5 ** ((max(0, distance - srcOptimalRange) / srcFalloffRange) ** 2)
elif distance <= srcOptimalRange:
return 1
else:
return 0
return True
if GraphSettings.getInstance().get('ignoreLockRange'):
return True
return distance <= src.item.maxTargetRange
# Just copy-paste penalization chain calculation code (with some modifications,
# as multipliers arrive in different form) in here to not make actual attribute
# calculations slower than they already are due to extra function calls
def calculateMultiplier(multipliers):
"""
multipliers: dictionary in format:
{stacking group name: [(mult, resist attr ID), (mult, resist attr ID)]}
"""
val = 1
for penalizedMultipliers in multipliers.values():
# A quick explanation of how this works:
# 1: Bonuses and penalties are calculated seperately, so we'll have to filter each of them
l1 = [v[0] for v in penalizedMultipliers if v[0] > 1]
l2 = [v[0] for v in penalizedMultipliers if v[0] < 1]
# 2: The most significant bonuses take the smallest penalty,
# This means we'll have to sort
abssort = lambda _val: -abs(_val - 1)
l1.sort(key=abssort)
l2.sort(key=abssort)
# 3: The first module doesn't get penalized at all
# Any module after the first takes penalties according to:
# 1 + (multiplier - 1) * math.exp(- math.pow(i, 2) / 7.1289)
for l in (l1, l2):
for i in range(len(l)):
bonus = l[i]
val *= 1 + (bonus - 1) * math.exp(- i ** 2 / 7.1289)
return val
def checkDroneControlRange(src, distance):
if distance is None:
return True
if GraphSettings.getInstance().get('ignoreDCR'):
return True
return distance <= src.item.extraAttributes['droneControlRange']

View File

@@ -26,3 +26,5 @@ from . import fitCapacitor
from . import fitMobility
from . import fitWarpTime
from . import fitLockTime
# Hidden graphs, available via ctrl-alt-g
from . import fitEcmBurstScanresDamps

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

@@ -29,6 +29,7 @@ from service.const import GraphCacheCleanupReason
class FitGraph(metaclass=ABCMeta):
# UI stuff
hidden = False
views = []
viewMap = {}

View File

@@ -21,37 +21,47 @@
import math
from functools import lru_cache
from eos.calc import calculateRangeFactor
from eos.const import FittingHardpoint
from eos.utils.float import floatUnerr
from graphs.calc import calculateRangeFactor
from graphs.calc import checkLockRange, checkDroneControlRange
from service.attribute import Attribute
from service.const import GraphDpsDroneMode
from service.settings import GraphSettings
def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
applicationMap = {}
for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
if mod.hardpoint == FittingHardpoint.TURRET:
applicationMap[mod] = getTurretMult(
mod=mod,
src=src,
tgt=tgt,
atkSpeed=atkSpeed,
atkAngle=atkAngle,
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius)
if inLockRange:
applicationMap[mod] = getTurretMult(
mod=mod,
src=src,
tgt=tgt,
atkSpeed=atkSpeed,
atkAngle=atkAngle,
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius)
else:
applicationMap[mod] = 0
elif mod.hardpoint == FittingHardpoint.MISSILE:
applicationMap[mod] = getLauncherMult(
mod=mod,
src=src,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
# FoF missiles can shoot beyond lock range
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)
else:
applicationMap[mod] = 0
elif mod.item.group.name in ('Smart Bomb', 'Structure Area Denial Module'):
applicationMap[mod] = getSmartbombMult(
mod=mod,
@@ -64,44 +74,58 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
distance=distance,
tgtSigRadius=tgtSigRadius)
elif mod.item.group.name == 'Structure Guided Bomb Launcher':
applicationMap[mod] = getGuidedBombMult(
mod=mod,
src=src,
distance=distance,
tgtSigRadius=tgtSigRadius)
if inLockRange:
applicationMap[mod] = getGuidedBombMult(
mod=mod,
src=src,
distance=distance,
tgtSigRadius=tgtSigRadius)
else:
applicationMap[mod] = 0
elif mod.item.group.name in ('Super Weapon', 'Structure Doomsday Weapon'):
applicationMap[mod] = getDoomsdayMult(
mod=mod,
tgt=tgt,
distance=distance,
tgtSigRadius=tgtSigRadius)
# Only single-target DDs need locks
if not inLockRange and {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(mod.item.effects):
applicationMap[mod] = 0
else:
applicationMap[mod] = getDoomsdayMult(
mod=mod,
tgt=tgt,
distance=distance,
tgtSigRadius=tgtSigRadius)
for drone in src.item.activeDronesIter():
if not drone.isDealingDamage():
continue
applicationMap[drone] = getDroneMult(
drone=drone,
src=src,
tgt=tgt,
atkSpeed=atkSpeed,
atkAngle=atkAngle,
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius)
if inLockRange and inDroneRange:
applicationMap[drone] = getDroneMult(
drone=drone,
src=src,
tgt=tgt,
atkSpeed=atkSpeed,
atkAngle=atkAngle,
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius)
else:
applicationMap[drone] = 0
for fighter in src.item.activeFightersIter():
if not fighter.isDealingDamage():
continue
for ability in fighter.abilities:
if not ability.dealsDamage or not ability.active:
continue
applicationMap[(fighter, ability.effectID)] = getFighterAbilityMult(
fighter=fighter,
ability=ability,
src=src,
tgt=tgt,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
# Bomb launching doesn't need locks
if inLockRange or ability.effect.name == 'fighterAbilityLaunchBomb':
applicationMap[(fighter, ability.effectID)] = getFighterAbilityMult(
fighter=fighter,
ability=ability,
src=src,
tgt=tgt,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
else:
applicationMap[(fighter, ability.effectID)] = 0
# Ensure consistent results - round off a little to avoid float errors
for k, v in applicationMap.items():
applicationMap[k] = floatUnerr(v)
@@ -128,18 +152,24 @@ def getTurretMult(mod, src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngl
def getLauncherMult(mod, src, distance, tgtSpeed, tgtSigRadius):
modRange = mod.maxRange
if modRange is None:
missileMaxRangeData = mod.missileMaxRangeData
if missileMaxRangeData is None:
return 0
if distance is not None and distance + src.getRadius() > modRange:
return 0
mult = _calcMissileFactor(
# The ranges already consider ship radius
lowerRange, higherRange, higherChance = missileMaxRangeData
if distance is None or distance <= lowerRange:
distanceFactor = 1
elif lowerRange < distance <= higherRange:
distanceFactor = higherChance
else:
distanceFactor = 0
applicationFactor = _calcMissileFactor(
atkEr=mod.getModifiedChargeAttr('aoeCloudSize'),
atkEv=mod.getModifiedChargeAttr('aoeVelocity'),
atkDrf=mod.getModifiedChargeAttr('aoeDamageReductionFactor'),
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
return mult
return distanceFactor * applicationFactor
def getSmartbombMult(mod, distance):
@@ -200,7 +230,11 @@ def getGuidedBombMult(mod, src, distance, tgtSigRadius):
def getDroneMult(drone, src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
if distance is not None and distance > src.item.extraAttributes['droneControlRange']:
if (
distance is not None and (
(not GraphSettings.getInstance().get('ignoreDCR') and distance > src.item.extraAttributes['droneControlRange']) or
(not GraphSettings.getInstance().get('ignoreLockRange') and distance > src.item.maxTargetRange))
):
return 0
droneSpeed = drone.getModifiedItemAttr('maxVelocity')
# Hard to simulate drone behavior, so assume chance to hit is 1 for mobile drones

View File

@@ -20,37 +20,85 @@
import math
from eos.calc import calculateRangeFactor
from eos.utils.float import floatUnerr
from graphs.calc import calculateRangeFactor
from graphs.calc import checkLockRange, checkDroneControlRange
from service.const import GraphDpsDroneMode
from service.settings import GraphSettings
def getWebbedSpeed(src, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighters, distance):
def _isRegularScram(mod):
if not mod.item:
return False
if not {'warpScrambleBlockMWDWithNPCEffect', 'structureWarpScrambleBlockMWDWithNPCEffect'}.intersection(mod.item.effects):
return False
if not mod.getModifiedItemAttr('activationBlockedStrenght', 0):
return False
return True
def _isHicScram(mod):
if not mod.item:
return False
if 'warpDisruptSphere' not in mod.item.effects:
return False
if not mod.charge:
return False
if 'shipModuleFocusedWarpScramblingScript' not in mod.charge.effects:
return False
return True
def getScramRange(src):
scramRange = None
for mod in src.item.activeModulesIter():
if _isRegularScram(mod) or _isHicScram(mod):
scramRange = max(scramRange or 0, mod.maxRange or 0)
return scramRange
def getScrammables(tgt):
scrammables = []
if tgt.isFit:
for mod in tgt.item.activeModulesIter():
if not mod.item:
continue
if {'moduleBonusMicrowarpdrive', 'microJumpDrive', 'microJumpPortalDrive'}.intersection(mod.item.effects):
scrammables.append(mod)
return scrammables
def getTackledSpeed(src, tgt, currentUntackledSpeed, srcScramRange, tgtScrammables, webMods, webDrones, webFighters, distance):
# Can slow down non-immune ships and target profiles
if tgt.isFit and tgt.item.ship.getModifiedItemAttr('disallowOffensiveModifiers'):
return currentUnwebbedSpeed
maxUnwebbedSpeed = tgt.getMaxVelocity()
return currentUntackledSpeed
maxUntackledSpeed = tgt.getMaxVelocity()
# What's immobile cannot be slowed
if maxUnwebbedSpeed == 0:
return maxUnwebbedSpeed
speedRatio = currentUnwebbedSpeed / maxUnwebbedSpeed
if maxUntackledSpeed == 0:
return maxUntackledSpeed
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
speedRatio = currentUntackledSpeed / maxUntackledSpeed
# No scrams or distance is longer than longest scram - nullify scrammables list
if not inLockRange or srcScramRange is None or (distance is not None and distance > srcScramRange):
tgtScrammables = ()
appliedMultipliers = {}
# Modules first, they are applied always the same way
for wData in webMods:
appliedBoost = wData.boost * calculateRangeFactor(
srcOptimalRange=wData.optimal,
srcFalloffRange=wData.falloff,
distance=distance)
if appliedBoost:
appliedMultipliers.setdefault(wData.stackingGroup, []).append((1 + appliedBoost / 100, wData.resAttrID))
maxWebbedSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers)
currentWebbedSpeed = maxWebbedSpeed * speedRatio
# Modules first, they are always applied the same way
if inLockRange:
for wData in webMods:
appliedBoost = wData.boost * calculateRangeFactor(
srcOptimalRange=wData.optimal,
srcFalloffRange=wData.falloff,
distance=distance)
if appliedBoost:
appliedMultipliers.setdefault(wData.stackingGroup, []).append((1 + appliedBoost / 100, wData.resAttrID))
maxTackledSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers, ignoreAfflictors=tgtScrammables)
currentTackledSpeed = maxTackledSpeed * speedRatio
# Drones and fighters
mobileWebs = []
mobileWebs.extend(webFighters)
# Drones have range limit
if distance is None or distance <= src.item.extraAttributes['droneControlRange']:
if inLockRange:
mobileWebs.extend(webFighters)
if inLockRange and inDroneRange:
mobileWebs.extend(webDrones)
atkRadius = src.getRadius()
# As mobile webs either follow the target or stick to the attacking ship,
@@ -60,8 +108,8 @@ def getWebbedSpeed(src, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighte
for mwData in longEnoughMws:
appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + mwData.boost / 100, mwData.resAttrID))
mobileWebs.remove(mwData)
maxWebbedSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers)
currentWebbedSpeed = maxWebbedSpeed * speedRatio
maxTackledSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers, ignoreAfflictors=tgtScrammables)
currentTackledSpeed = maxTackledSpeed * speedRatio
# Apply remaining webs, from fastest to slowest
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
while mobileWebs:
@@ -70,7 +118,7 @@ def getWebbedSpeed(src, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighte
fastestMws = [mw for mw in mobileWebs if mw.speed == fastestMwSpeed]
for mwData in fastestMws:
# Faster than target or set to follow it - apply full slowdown
if (droneOpt == GraphDpsDroneMode.auto and mwData.speed >= currentWebbedSpeed) or droneOpt == GraphDpsDroneMode.followTarget:
if (droneOpt == GraphDpsDroneMode.auto and mwData.speed >= currentTackledSpeed) or droneOpt == GraphDpsDroneMode.followTarget:
appliedMwBoost = mwData.boost
# Otherwise project from the center of the ship
else:
@@ -84,31 +132,37 @@ def getWebbedSpeed(src, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighte
distance=rangeFactorDistance)
appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + appliedMwBoost / 100, mwData.resAttrID))
mobileWebs.remove(mwData)
maxWebbedSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers)
currentWebbedSpeed = maxWebbedSpeed * speedRatio
maxTackledSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers, ignoreAfflictors=tgtScrammables)
currentTackledSpeed = maxTackledSpeed * speedRatio
# Ensure consistent results - round off a little to avoid float errors
return floatUnerr(currentWebbedSpeed)
return floatUnerr(currentTackledSpeed)
def getTpMult(src, tgt, tgtSpeed, tpMods, tpDrones, tpFighters, distance):
def getSigRadiusMult(src, tgt, tgtSpeed, srcScramRange, tgtScrammables, tpMods, tpDrones, tpFighters, distance):
# Can blow non-immune ships and target profiles
if tgt.isFit and tgt.item.ship.getModifiedItemAttr('disallowOffensiveModifiers'):
return 1
untpedSig = tgt.getSigRadius()
# Modules
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
initSig = tgt.getSigRadius()
# No scrams or distance is longer than longest scram - nullify scrammables list
if not inLockRange or srcScramRange is None or (distance is not None and distance > srcScramRange):
tgtScrammables = ()
# TPing modules
appliedMultipliers = {}
for tpData in tpMods:
appliedBoost = tpData.boost * calculateRangeFactor(
srcOptimalRange=tpData.optimal,
srcFalloffRange=tpData.falloff,
distance=distance)
if appliedBoost:
appliedMultipliers.setdefault(tpData.stackingGroup, []).append((1 + appliedBoost / 100, tpData.resAttrID))
# Drones and fighters
if inLockRange:
for tpData in tpMods:
appliedBoost = tpData.boost * calculateRangeFactor(
srcOptimalRange=tpData.optimal,
srcFalloffRange=tpData.falloff,
distance=distance)
if appliedBoost:
appliedMultipliers.setdefault(tpData.stackingGroup, []).append((1 + appliedBoost / 100, tpData.resAttrID))
# TPing drones
mobileTps = []
mobileTps.extend(tpFighters)
# Drones have range limit
if distance is None or distance <= src.item.extraAttributes['droneControlRange']:
if inLockRange:
mobileTps.extend(tpFighters)
if inLockRange and inDroneRange:
mobileTps.extend(tpDrones)
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
atkRadius = src.getRadius()
@@ -127,9 +181,9 @@ def getTpMult(src, tgt, tgtSpeed, tpMods, tpDrones, tpFighters, distance):
srcFalloffRange=mtpData.falloff,
distance=rangeFactorDistance)
appliedMultipliers.setdefault(mtpData.stackingGroup, []).append((1 + appliedMtpBoost / 100, mtpData.resAttrID))
tpedSig = tgt.getSigRadius(extraMultipliers=appliedMultipliers)
if tpedSig == math.inf and untpedSig == math.inf:
modifiedSig = tgt.getSigRadius(extraMultipliers=appliedMultipliers, ignoreAfflictors=tgtScrammables)
if modifiedSig == math.inf and initSig == math.inf:
return 1
mult = tpedSig / untpedSig
mult = modifiedSig / initSig
# Ensure consistent results - round off a little to avoid float errors
return floatUnerr(mult)

View File

@@ -24,7 +24,7 @@ from eos.utils.stats import DmgTypes
from graphs.data.base import PointGetter, SmoothPointGetter
from service.settings import GraphSettings
from .calc.application import getApplicationPerKey
from .calc.projected import getTpMult, getWebbedSpeed
from .calc.projected import getScramRange, getScrammables, getTackledSpeed, getSigRadiusMult
def applyDamage(dmgMap, applicationMap, tgtResists):
@@ -138,8 +138,11 @@ class XDistanceMixin(SmoothPointGetter):
# Prepare time cache here because we need to do it only once,
# and this function is called once per point info fetch
self._prepareTimeCache(src=src, maxTime=miscParams['time'])
applyProjected = GraphSettings.getInstance().get('applyProjected')
return {
'applyProjected': GraphSettings.getInstance().get('applyProjected'),
'applyProjected': applyProjected,
'srcScramRange': getScramRange(src=src) if applyProjected else None,
'tgtScrammables': getScrammables(tgt=tgt) if applyProjected else (),
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
'tgtResists': tgt.getResists()}
@@ -151,18 +154,22 @@ class XDistanceMixin(SmoothPointGetter):
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getWebbedSpeed(
tgtSpeed = getTackledSpeed(
src=src,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
currentUntackledSpeed=tgtSpeed,
srcScramRange=commonData['srcScramRange'],
tgtScrammables=commonData['tgtScrammables'],
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=distance)
tgtSigRadius = tgtSigRadius * getTpMult(
tgtSigRadius = tgtSigRadius * getSigRadiusMult(
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
srcScramRange=commonData['srcScramRange'],
tgtScrammables=commonData['tgtScrammables'],
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,
@@ -189,21 +196,27 @@ class XTimeMixin(PointGetter):
tgtSpeed = miscParams['tgtSpeed']
tgtSigRadius = tgt.getSigRadius()
if GraphSettings.getInstance().get('applyProjected'):
srcScramRange = getScramRange(src=src)
tgtScrammables = getScrammables(tgt=tgt)
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getWebbedSpeed(
tgtSpeed = getTackledSpeed(
src=src,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
currentUntackledSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=miscParams['distance'])
tgtSigRadius = tgtSigRadius * getTpMult(
tgtSigRadius = tgtSigRadius * getSigRadiusMult(
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,
@@ -303,21 +316,27 @@ class XTgtSpeedMixin(SmoothPointGetter):
tgtSpeed = x
tgtSigRadius = tgt.getSigRadius()
if commonData['applyProjected']:
srcScramRange = getScramRange(src=src)
tgtScrammables = getScrammables(tgt=tgt)
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getWebbedSpeed(
tgtSpeed = getTackledSpeed(
src=src,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
currentUntackledSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=miscParams['distance'])
tgtSigRadius = tgtSigRadius * getTpMult(
tgtSigRadius = tgtSigRadius * getSigRadiusMult(
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,
@@ -347,21 +366,27 @@ class XTgtSigRadiusMixin(SmoothPointGetter):
tgtSpeed = miscParams['tgtSpeed']
tgtSigMult = 1
if GraphSettings.getInstance().get('applyProjected'):
srcScramRange = getScramRange(src=src)
tgtScrammables = getScrammables(tgt=tgt)
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getWebbedSpeed(
tgtSpeed = getTackledSpeed(
src=src,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
currentUntackledSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=miscParams['distance'])
tgtSigMult = getTpMult(
tgtSigMult = getSigRadiusMult(
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,

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

@@ -0,0 +1,24 @@
# =============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
from .graph import FitEcmBurstScanresDampsGraph
FitEcmBurstScanresDampsGraph.register()

View File

@@ -0,0 +1,117 @@
# =============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
from eos.calc import calculateLockTime
from graphs.data.base import SmoothPointGetter
ECM_BURST_DURATION = 30
DRONE_LOCK_TIME = 2
class TgtScanRes2TgtLockTimeGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
if miscParams['applyDamps']:
tgtScanResMult = src.item.getDampMultScanRes()
else:
tgtScanResMult = 1
return {
'tgtScanResMult': tgtScanResMult,
'sigRadius': src.item.ship.getModifiedItemAttr('signatureRadius')}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
scanRes = x
time = calculateLockTime(
srcScanRes=scanRes * commonData['tgtScanResMult'],
tgtSigRadius=commonData['sigRadius'])
return time
class TgtScanRes2TgtLockUptimeGetter(TgtScanRes2TgtLockTimeGetter):
def _calculatePoint(self, *args, **kwargs):
# Assuming you ECM burst every 30 seconds, find out how long you
# will be locked before you burst another time
lockTime = super()._calculatePoint(*args, **kwargs)
lockedTime = max(0, ECM_BURST_DURATION - lockTime)
return lockedTime
class SrcDmgBaseGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
if miscParams['applyDamps']:
tgtScanResMult = src.item.getDampMultScanRes()
else:
tgtScanResMult = 1
return {
'tgtScanResMult': tgtScanResMult,
'srcSigRadius': src.item.ship.getModifiedItemAttr('signatureRadius'),
'srcEhp': sum(src.item.ehp.values()),
'srcDpsWeapon': src.item.getWeaponDps().total,
'srcDpsDrone': src.item.getDroneDps().total if miscParams['applyDrones'] else 0}
@staticmethod
def _calculateInflictedDamage(srcSigRadius, srcWeaponDps, srcDroneDps, srcEhp, tgtScanRes, tgtDps, uptimeAdjustment, uptimeAmountLimit):
lockTime = calculateLockTime(srcScanRes=tgtScanRes, tgtSigRadius=srcSigRadius)
lockUptime = max(0, ECM_BURST_DURATION - lockTime - uptimeAdjustment)
lockDowntime = ECM_BURST_DURATION - lockUptime
inflictedDmg = 0
remainingEhp = srcEhp
for i in range(int(uptimeAmountLimit)):
timeAliveUnderFire = min(lockUptime, remainingEhp / tgtDps)
timeAlive = lockDowntime + timeAliveUnderFire
remainingEhp -= lockUptime * tgtDps
inflictedDmg += timeAlive * srcWeaponDps
inflictedDmg += max(0, timeAlive - DRONE_LOCK_TIME - 1) * srcDroneDps
if remainingEhp <= 0:
break
return inflictedDmg
class TgtScanRes2SrcDmgGetter(SrcDmgBaseGetter):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
damage = self._calculateInflictedDamage(
srcSigRadius=commonData['srcSigRadius'],
srcWeaponDps=commonData['srcDpsWeapon'],
srcDroneDps=commonData['srcDpsDrone'],
srcEhp=commonData['srcEhp'],
tgtScanRes=x * commonData['tgtScanResMult'],
tgtDps=miscParams['tgtDps'],
uptimeAdjustment=miscParams['uptimeAdj'],
uptimeAmountLimit=miscParams['uptimeAmtLimit'])
return damage
class TgtDps2SrcDmgGetter(SrcDmgBaseGetter):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
damage = self._calculateInflictedDamage(
srcSigRadius=commonData['srcSigRadius'],
srcWeaponDps=commonData['srcDpsWeapon'],
srcDroneDps=commonData['srcDpsDrone'],
srcEhp=commonData['srcEhp'],
tgtScanRes=miscParams['tgtScanRes'] * commonData['tgtScanResMult'],
tgtDps=x,
uptimeAdjustment=miscParams['uptimeAdj'],
uptimeAmountLimit=miscParams['uptimeAmtLimit'])
return damage

View File

@@ -0,0 +1,65 @@
# =============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
"""
Disclaimer by kadesh: this graph was made to analyze my ECM burst + damp frig
concept. I do not think it is useful for regular player, so it is disabled.
Enable by setting config.experimentalFeatures = True.
"""
import math
from graphs.data.base import FitGraph, XDef, YDef, Input, InputCheckbox
from .getter import (
TgtScanRes2TgtLockTimeGetter, TgtScanRes2TgtLockUptimeGetter,
TgtScanRes2SrcDmgGetter, TgtDps2SrcDmgGetter)
class FitEcmBurstScanresDampsGraph(FitGraph):
# UI stuff
hidden = True
internalName = 'ecmBurstScanresDamps'
name = 'ECM Burst + Scanres Damps'
xDefs = [
XDef(handle='tgtDps', unit=None, label='Enemy DPS', mainInput=('tgtDps', None)),
XDef(handle='tgtScanRes', unit='mm', label='Enemy scanres', mainInput=('tgtScanRes', 'mm'))]
yDefs = [
YDef(handle='srcDmg', unit=None, label='Damage inflicted'),
YDef(handle='tgtLockTime', unit='s', label='Lock time'),
YDef(handle='tgtLockUptime', unit='s', label='Lock uptime')]
inputs = [
Input(handle='tgtScanRes', unit='mm', label='Enemy scanres', iconID=74, defaultValue=700, defaultRange=(100, 1000)),
Input(handle='tgtDps', unit=None, label='Enemy DPS', iconID=1432, defaultValue=200, defaultRange=(100, 600)),
Input(handle='uptimeAdj', unit='s', label='Uptime adjustment', iconID=1392, defaultValue=1, defaultRange=(None, None), conditions=[(None, ('srcDmg', None))]),
Input(handle='uptimeAmtLimit', unit=None, label='Max amount of uptimes', iconID=1397, defaultValue=3, defaultRange=(None, None), conditions=[(None, ('srcDmg', None))])]
checkboxes = [
InputCheckbox(handle='applyDamps', label='Apply sensor dampeners', defaultValue=True),
InputCheckbox(handle='applyDrones', label='Use drones', defaultValue=True, conditions=[(None, ('srcDmg', None))])]
srcExtraCols = ('SigRadius', 'Damp ScanRes')
# Calculation stuff
_limiters = {'tgtScanRes': lambda src, tgt: (1, math.inf)}
_getters = {
('tgtScanRes', 'tgtLockTime'): TgtScanRes2TgtLockTimeGetter,
('tgtScanRes', 'tgtLockUptime'): TgtScanRes2TgtLockUptimeGetter,
('tgtScanRes', 'srcDmg'): TgtScanRes2SrcDmgGetter,
('tgtDps', 'srcDmg'): TgtDps2SrcDmgGetter}

View File

@@ -20,7 +20,8 @@
import math
from graphs.calc import calculateMultiplier, calculateRangeFactor
from eos.calc import calculateMultiplier, calculateRangeFactor
from graphs.calc import checkLockRange, checkDroneControlRange
from graphs.data.base import SmoothPointGetter
@@ -37,33 +38,37 @@ class Distance2NeutingStrGetter(SmoothPointGetter):
if effectName in mod.item.effects:
neuts.append((
mod.getModifiedItemAttr('energyNeutralizerAmount') / self.__getDuration(mod) * resonance,
mod.maxRange or 0, mod.falloff or 0))
mod.maxRange or 0, mod.falloff or 0, True, False))
if 'energyNosferatuFalloff' in mod.item.effects and mod.getModifiedItemAttr('nosOverride'):
neuts.append((
mod.getModifiedItemAttr('powerTransferAmount') / self.__getDuration(mod) * resonance,
mod.maxRange or 0, mod.falloff or 0))
mod.maxRange or 0, mod.falloff or 0, True, False))
if 'doomsdayAOENeut' in mod.item.effects:
neuts.append((
mod.getModifiedItemAttr('energyNeutralizerAmount') / self.__getDuration(mod) * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0))
mod.falloff or 0, False, False))
for drone in src.item.activeDronesIter():
if 'entityEnergyNeutralizerFalloff' in drone.item.effects:
neuts.extend(drone.amountActive * ((
drone.getModifiedItemAttr('energyNeutralizerAmount') / (drone.getModifiedItemAttr('energyNeutralizerDuration') / 1000) * resonance,
src.item.extraAttributes['droneControlRange'], 0),))
math.inf, 0, True, True),))
for fighter, ability in src.item.activeFighterAbilityIter():
if ability.effect.name == 'fighterAbilityEnergyNeutralizer':
nps = fighter.getModifiedItemAttr('fighterAbilityEnergyNeutralizerAmount') / (ability.cycleTime / 1000)
neuts.append((
nps * fighter.amount * resonance,
math.inf, 0))
math.inf, 0, True, False))
return {'neuts': neuts}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
combinedStr = 0
for strength, optimal, falloff in commonData['neuts']:
for strength, optimal, falloff, needsLock, needsDcr in commonData['neuts']:
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
continue
combinedStr += strength * calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
return combinedStr
@@ -84,28 +89,32 @@ class Distance2WebbingStrGetter(SmoothPointGetter):
if effectName in mod.item.effects:
webs.append((
mod.getModifiedItemAttr('speedFactor') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
mod.maxRange or 0, mod.falloff or 0, 'default', True, False))
if 'doomsdayAOEWeb' in mod.item.effects:
webs.append((
mod.getModifiedItemAttr('speedFactor') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
mod.falloff or 0, 'default', False, False))
for drone in src.item.activeDronesIter():
if 'remoteWebifierEntity' in drone.item.effects:
webs.extend(drone.amountActive * ((
drone.getModifiedItemAttr('speedFactor') * resonance,
src.item.extraAttributes['droneControlRange'], 0, 'default'),))
math.inf, 0, 'default', True, True),))
for fighter, ability in src.item.activeFighterAbilityIter():
if ability.effect.name == 'fighterAbilityStasisWebifier':
webs.append((
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierSpeedPenalty') * fighter.amount * resonance,
math.inf, 0, 'default'))
math.inf, 0, 'default', True, False))
return {'webs': webs}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
strMults = {}
for strength, optimal, falloff, stackingGroup in commonData['webs']:
for strength, optimal, falloff, stackingGroup, needsLock, needsDcr in commonData['webs']:
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
continue
strength *= calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
strMults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
strMult = calculateMultiplier(strMults)
@@ -129,28 +138,32 @@ class Distance2EcmStrMaxGetter(SmoothPointGetter):
if effectName in mod.item.effects:
ecms.append((
max(mod.getModifiedItemAttr(a) for a in self.ECM_ATTRS_GENERAL) * resonance,
mod.maxRange or 0, mod.falloff or 0))
mod.maxRange or 0, mod.falloff or 0, True, False))
if 'doomsdayAOEECM' in mod.item.effects:
ecms.append((
max(mod.getModifiedItemAttr(a) for a in self.ECM_ATTRS_GENERAL) * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0))
mod.falloff or 0, False, False))
for drone in src.item.activeDronesIter():
if 'entityECMFalloff' in drone.item.effects:
ecms.extend(drone.amountActive * ((
max(drone.getModifiedItemAttr(a) for a in self.ECM_ATTRS_GENERAL) * resonance,
src.item.extraAttributes['droneControlRange'], 0),))
math.inf, 0, True, True),))
for fighter, ability in src.item.activeFighterAbilityIter():
if ability.effect.name == 'fighterAbilityECM':
ecms.append((
max(fighter.getModifiedItemAttr(a) for a in self.ECM_ATTRS_FIGHTERS) * fighter.amount * resonance,
math.inf, 0))
math.inf, 0, True, False))
return {'ecms': ecms}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
combinedStr = 0
for strength, optimal, falloff in commonData['ecms']:
for strength, optimal, falloff, needsLock, needsDcr in commonData['ecms']:
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
continue
combinedStr += strength * calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
return combinedStr
@@ -168,23 +181,27 @@ class Distance2DampStrLockRangeGetter(SmoothPointGetter):
if effectName in mod.item.effects:
damps.append((
mod.getModifiedItemAttr('maxTargetRangeBonus') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
mod.maxRange or 0, mod.falloff or 0, 'default', True, False))
if 'doomsdayAOEDamp' in mod.item.effects:
damps.append((
mod.getModifiedItemAttr('maxTargetRangeBonus') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
mod.falloff or 0, 'default', False, False))
for drone in src.item.activeDronesIter():
if 'remoteSensorDampEntity' in drone.item.effects:
damps.extend(drone.amountActive * ((
drone.getModifiedItemAttr('maxTargetRangeBonus') * resonance,
src.item.extraAttributes['droneControlRange'], 0, 'default'),))
math.inf, 0, 'default', True, True),))
return {'damps': damps}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
strMults = {}
for strength, optimal, falloff, stackingGroup in commonData['damps']:
for strength, optimal, falloff, stackingGroup, needsLock, needsDcr in commonData['damps']:
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
continue
strength *= calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
strMults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
strMult = calculateMultiplier(strMults)
@@ -205,23 +222,27 @@ class Distance2TdStrOptimalGetter(SmoothPointGetter):
if effectName in mod.item.effects:
tds.append((
mod.getModifiedItemAttr('maxRangeBonus') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
mod.maxRange or 0, mod.falloff or 0, 'default', True, False))
if 'doomsdayAOETrack' in mod.item.effects:
tds.append((
mod.getModifiedItemAttr('maxRangeBonus') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
mod.falloff or 0, 'default', False, False))
for drone in src.item.activeDronesIter():
if 'npcEntityWeaponDisruptor' in drone.item.effects:
tds.extend(drone.amountActive * ((
drone.getModifiedItemAttr('maxRangeBonus') * resonance,
src.item.extraAttributes['droneControlRange'], 0, 'default'),))
math.inf, 0, 'default', True, True),))
return {'tds': tds}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
strMults = {}
for strength, optimal, falloff, stackingGroup in commonData['tds']:
for strength, optimal, falloff, stackingGroup, needsLock, needsDcr in commonData['tds']:
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
continue
strength *= calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
strMults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
strMult = calculateMultiplier(strMults)
@@ -243,20 +264,24 @@ class Distance2GdStrRangeGetter(SmoothPointGetter):
gds.append((
mod.getModifiedItemAttr('missileVelocityBonus') * resonance,
mod.getModifiedItemAttr('explosionDelayBonus') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
mod.maxRange or 0, mod.falloff or 0, 'default', True, False))
if 'doomsdayAOETrack' in mod.item.effects:
gds.append((
mod.getModifiedItemAttr('missileVelocityBonus') * resonance,
mod.getModifiedItemAttr('explosionDelayBonus') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
mod.falloff or 0, 'default', False, False))
return {'gds': gds}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
velocityStrMults = {}
timeStrMults = {}
for velocityStr, timeStr, optimal, falloff, stackingGroup in commonData['gds']:
for velocityStr, timeStr, optimal, falloff, stackingGroup, needsLock, needsDcr in commonData['gds']:
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
continue
rangeFactor = calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
velocityStr *= rangeFactor
timeStr *= rangeFactor
@@ -281,23 +306,27 @@ class Distance2TpStrGetter(SmoothPointGetter):
if effectName in mod.item.effects:
tps.append((
mod.getModifiedItemAttr('signatureRadiusBonus') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
mod.maxRange or 0, mod.falloff or 0, 'default', True, False))
if 'doomsdayAOEPaint' in mod.item.effects:
tps.append((
mod.getModifiedItemAttr('signatureRadiusBonus') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
mod.falloff or 0, 'default', False, False))
for drone in src.item.activeDronesIter():
if 'remoteTargetPaintEntity' in drone.item.effects:
tps.extend(drone.amountActive * ((
drone.getModifiedItemAttr('signatureRadiusBonus') * resonance,
src.item.extraAttributes['droneControlRange'], 0, 'default'),))
math.inf, 0, 'default', True, True),))
return {'tps': tps}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
strMults = {}
for strength, optimal, falloff, stackingGroup in commonData['tps']:
for strength, optimal, falloff, stackingGroup, needsLock, needsDcr in commonData['tps']:
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
continue
strength *= calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
strMults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
strMult = calculateMultiplier(strMults)

View File

@@ -20,7 +20,7 @@
import math
from graphs.data.base import FitGraph, Input, XDef, YDef
from graphs.data.base import FitGraph, XDef, YDef, Input
from .getter import TgtSigRadius2LockTimeGetter

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

@@ -18,23 +18,32 @@
# =============================================================================
from eos.calc import calculateRangeFactor
from eos.utils.float import floatUnerr
from graphs.calc import calculateRangeFactor
from graphs.calc import checkLockRange, checkDroneControlRange
def getApplicationPerKey(src, distance):
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
applicationMap = {}
for mod in src.item.activeModulesIter():
if not mod.isRemoteRepping():
continue
applicationMap[mod] = 1 if distance is None else calculateRangeFactor(
srcOptimalRange=mod.maxRange or 0,
srcFalloffRange=mod.falloff or 0,
distance=distance)
if not inLockRange:
applicationMap[mod] = 0
else:
applicationMap[mod] = calculateRangeFactor(
srcOptimalRange=mod.maxRange or 0,
srcFalloffRange=mod.falloff or 0,
distance=distance)
for drone in src.item.activeDronesIter():
if not drone.isRemoteRepping():
continue
applicationMap[drone] = 1 if distance is None or distance <= src.item.extraAttributes['droneControlRange'] else 0
if not inLockRange or not inDroneRange:
applicationMap[drone] = 0
else:
applicationMap[drone] = 1
# Ensure consistent results - round off a little to avoid float errors
for k, v in applicationMap.items():
applicationMap[k] = floatUnerr(v)

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

@@ -54,6 +54,8 @@ try:
except ImportError as e:
pyfalog.warning('Matplotlib failed to import. Likely missing or incompatible version.')
graphFrame_enabled = False
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
# We can get exceptions deep within matplotlib. Catch those. See GH #1046
tb = traceback.format_exc()
@@ -71,6 +73,8 @@ class GraphCanvasPanel(wx.Panel):
# Remove matplotlib font cache, see #234
try:
cache_dir = mpl._get_cachedir()
except (KeyboardInterrupt, SystemExit):
raise
except:
cache_dir = os.path.expanduser(os.path.join('~', '.matplotlib'))
cache_file = os.path.join(cache_dir, 'fontList.cache')
@@ -106,7 +110,9 @@ class GraphCanvasPanel(wx.Panel):
legendData = []
chosenX = self.graphFrame.ctrlPanel.xType
chosenY = self.graphFrame.ctrlPanel.yType
self.subplot.set(xlabel=self.graphFrame.ctrlPanel.formatLabel(chosenX), ylabel=self.graphFrame.ctrlPanel.formatLabel(chosenY))
self.subplot.set(
xlabel=self.graphFrame.ctrlPanel.formatLabel(chosenX),
ylabel=self.graphFrame.ctrlPanel.formatLabel(chosenY))
mainInput, miscInputs = self.graphFrame.ctrlPanel.getValues()
view = self.graphFrame.getView()
@@ -166,6 +172,8 @@ class GraphCanvasPanel(wx.Panel):
legendData.append((color, lineStyle, source.shortName))
else:
legendData.append((color, lineStyle, '{} vs {}'.format(source.shortName, target.shortName)))
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
pyfalog.warning('Failed to plot "{}" vs "{}"'.format(source.name, '' if target is None else target.name))
self.canvas.draw()
@@ -186,6 +194,7 @@ class GraphCanvasPanel(wx.Panel):
if minX is not None and maxX is not None:
minY = min(allYs, default=None)
maxY = max(allYs, default=None)
yDiff = (maxY or 0) - (minY or 0)
xMark = max(min(self.xMark, maxX), minX)
# If in top 10% of X coordinates, align labels differently
if xMark > canvasMinX + 0.9 * (canvasMaxX - canvasMinX):
@@ -212,14 +221,16 @@ class GraphCanvasPanel(wx.Panel):
def addYMark(val):
if val is None:
return
# Round according to shown Y range - the bigger the range,
# the rougher the rounding
if yDiff != 0:
rounded = roundToPrec(val, 4, nsValue=yDiff)
else:
rounded = val
# If due to some bug or insufficient plot density we're
# out of bounds, do not add anything
if minY <= val <= maxY:
if abs(val) < 0.0001:
val = 0
else:
val = roundToPrec(val, 4)
yMarks.add(val)
if minY <= val <= maxY or minY <= rounded <= maxY:
yMarks.add(rounded)
for source, target in iterList:
xs, ys = plotData[(source, target)]
@@ -236,6 +247,8 @@ class GraphCanvasPanel(wx.Panel):
src=source,
tgt=target)
addYMark(y)
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
pyfalog.warning('Failed to get X mark for "{}" vs "{}"'.format(source.name, '' if target is None else target.name))
# Silently skip this mark, otherwise other marks and legend display will fail

View File

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

@@ -38,15 +38,19 @@ from .ctrlPanel import GraphControlPanel
pyfalog = Logger(__name__)
REDRAW_DELAY = 500
class GraphFrame(AuxiliaryFrame):
def __init__(self, parent):
def __init__(self, parent, includeHidden=False):
if not canvasPanel.graphFrame_enabled:
pyfalog.warning('Matplotlib is not enabled. Skipping initialization.')
return
super().__init__(parent, title='Graphs', style=wx.RESIZE_BORDER, size=(520, 390))
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')))
@@ -71,6 +75,8 @@ class GraphFrame(AuxiliaryFrame):
# Setup - graph selector
for view in FitGraph.views:
if view.hidden and not self.includeHidden:
continue
self.graphSelection.Append(view.name, view())
self.graphSelection.SetSelection(0)
self.ctrlPanel.updateControls(layout=False)
@@ -90,14 +96,17 @@ class GraphFrame(AuxiliaryFrame):
self.mainFrame.Bind(GE.GRAPH_OPTION_CHANGED, self.OnGraphOptionChanged)
self.mainFrame.Bind(GE.EFFECTIVE_HP_TOGGLED, self.OnEffectiveHpToggled)
self.drawTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.OnDrawTimer, self.drawTimer)
self.Layout()
self.UpdateWindowSize()
self.draw()
@classmethod
def openOne(cls, parent):
def openOne(cls, parent, *args, **kwargs):
if canvasPanel.graphFrame_enabled:
super().openOne(parent)
super().openOne(parent, *args, **kwargs)
def UpdateWindowSize(self):
curW, curH = self.GetSize()
@@ -110,9 +119,7 @@ class GraphFrame(AuxiliaryFrame):
self.SetMinSize(newSize)
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == wx.WXK_ESCAPE and mstate.GetModifiers() == wx.MOD_NONE:
if event.GetKeyCode() == wx.WXK_ESCAPE and event.GetModifiers() == wx.MOD_NONE:
self.Close()
return
event.Skip()
@@ -128,7 +135,10 @@ class GraphFrame(AuxiliaryFrame):
for fitID in event.fitIDs:
self.clearCache(reason=GraphCacheCleanupReason.fitChanged, extraData=fitID)
self.ctrlPanel.OnFitChanged(event)
self.draw()
# Data has to be recalculated - delay redraw
# to give time to finish UI update in main window
self.drawTimer.Stop()
self.drawTimer.Start(REDRAW_DELAY, True)
def OnFitRemoved(self, event):
event.Skip()
@@ -184,7 +194,10 @@ class GraphFrame(AuxiliaryFrame):
self.ctrlPanel.refreshAxeLabels(restoreSelection=True)
self.Layout()
self.clearCache(reason=GraphCacheCleanupReason.hpEffectivityChanged)
self.draw()
# Data has to be recalculated - delay redraw
# to give time to finish UI update in main window
self.drawTimer.Stop()
self.drawTimer.Start(REDRAW_DELAY, True)
# Even if graph is not selected, keep it updated
for idx in range(self.graphSelection.GetCount()):
view = self.getView(idx=idx)
@@ -202,6 +215,10 @@ class GraphFrame(AuxiliaryFrame):
self.draw()
event.Skip()
def OnDrawTimer(self, event):
event.Skip()
self.draw()
def OnClose(self, event):
self.mainFrame.Unbind(GE.FIT_RENAMED, handler=self.OnFitRenamed)
self.mainFrame.Unbind(GE.FIT_CHANGED, handler=self.OnFitChanged)

View File

@@ -152,10 +152,10 @@ class BaseWrapperList(gui.display.Display):
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL:
modifiers = event.GetModifiers()
if keycode == 65 and modifiers == wx.MOD_CONTROL:
self.selectAll()
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE:
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and modifiers == wx.MOD_NONE:
self.removeWrappers(self.getSelectedWrappers())
event.Skip()

View File

@@ -18,11 +18,11 @@
# =============================================================================
from eos.calc import calculateMultiplier
from eos.saveddata.damagePattern import DamagePattern
from eos.saveddata.fit import Fit
from eos.saveddata.targetProfile import TargetProfile
from service.const import TargetResistMode
from .calc import calculateMultiplier
class BaseWrapper:
@@ -43,7 +43,7 @@ class BaseWrapper:
if self.isFit:
return '{} ({})'.format(self.item.name, self.item.ship.item.name)
elif self.isProfile:
return self.item.name
return self.item.fullName
return ''
@property
@@ -51,13 +51,16 @@ class BaseWrapper:
if self.isFit:
return '{} ({})'.format(self.item.name, self.item.ship.item.getShortName())
elif self.isProfile:
return self.item.name
return self.item.shortName
return ''
def getMaxVelocity(self, extraMultipliers=None):
def getMaxVelocity(self, extraMultipliers=None, ignoreAfflictors=()):
if self.isFit:
if extraMultipliers:
maxVelocity = self.item.ship.getModifiedItemAttrWithExtraMods('maxVelocity', extraMultipliers=extraMultipliers)
if extraMultipliers or ignoreAfflictors:
maxVelocity = self.item.ship.getModifiedItemAttrExtended(
'maxVelocity',
extraMultipliers=extraMultipliers,
ignoreAfflictors=ignoreAfflictors)
else:
maxVelocity = self.item.ship.getModifiedItemAttr('maxVelocity')
elif self.isProfile:
@@ -68,10 +71,13 @@ class BaseWrapper:
maxVelocity = None
return maxVelocity
def getSigRadius(self, extraMultipliers=None):
def getSigRadius(self, extraMultipliers=None, ignoreAfflictors=()):
if self.isFit:
if extraMultipliers:
sigRadius = self.item.ship.getModifiedItemAttrWithExtraMods('signatureRadius', extraMultipliers=extraMultipliers)
if extraMultipliers or ignoreAfflictors:
sigRadius = self.item.ship.getModifiedItemAttrExtended(
'signatureRadius',
extraMultipliers=extraMultipliers,
ignoreAfflictors=ignoreAfflictors)
else:
sigRadius = self.item.ship.getModifiedItemAttr('signatureRadius')
elif self.isProfile:

View File

@@ -19,7 +19,12 @@
import config
versionString = "{0}".format(config.version)
try:
versionString = "{0}".format(config.getVersion())
except NameError:
# is caught in case we run test and there are no config values initialized
versionString = "0.0"
licenses = (
"pyfa is released under GNU GPLv3 - see included LICENSE file",
"All EVE-Online related materials are property of CCP hf.",

View File

@@ -20,6 +20,7 @@
# noinspection PyPackageRequirements
import wx
import gui.globalEvents as GE
from gui.bitmap_loader import BitmapLoader
from gui.builtinAdditionPanes.boosterView import BoosterView
from gui.builtinAdditionPanes.cargoView import CargoView
@@ -35,9 +36,10 @@ from gui.toggle_panel import TogglePanel
class AdditionsPane(TogglePanel):
def __init__(self, parent):
def __init__(self, parent, mainFrame):
TogglePanel.__init__(self, parent, force_layout=1)
self.mainFrame = mainFrame
self.SetLabel("Additions")
pane = self.GetContentPanel()
@@ -45,7 +47,7 @@ class AdditionsPane(TogglePanel):
baseSizer = wx.BoxSizer(wx.HORIZONTAL)
pane.SetSizer(baseSizer)
self.notebook = ChromeNotebook(pane, False)
self.notebook = ChromeNotebook(pane, can_add=False, tabWidthMode=1)
self.notebook.SetMinSize((-1, 1000))
baseSizer.Add(self.notebook, 1, wx.EXPAND)
@@ -83,6 +85,9 @@ class AdditionsPane(TogglePanel):
self.notes = NotesView(self.notebook)
self.notebook.AddPage(self.notes, "Notes", image=notesImg, closeable=False)
self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged)
self.mainFrame.Bind(GE.FIT_NOTES_CHANGED, self.OnNotesChanged)
self.notebook.SetSelection(0)
PANES = ["Drones", "Fighters", "Cargo", "Implants", "Boosters", "Projected", "Command", "Notes"]
@@ -106,3 +111,25 @@ class AdditionsPane(TogglePanel):
self.parent.SetSashInvisible(False)
self.parent.SetMinimumPaneSize(200)
self.parent.SetSashPosition(self.old_pos, True)
def OnFitChanged(self, event):
event.Skip()
activeFitID = self.mainFrame.getActiveFit()
if activeFitID is not None and activeFitID not in event.fitIDs:
return
self.updateExtraText()
def OnNotesChanged(self, event):
event.Skip()
self.updateExtraText()
def updateExtraText(self):
refresh = False
for i in range(self.notebook.GetPageCount()):
page = self.notebook.GetPage(i)
if hasattr(page, 'getTabExtraText'):
refresh = True
self.notebook.SetPageTitleExtra(i, page.getTabExtraText() or '', refresh=False)
if refresh:
self.notebook.tabs_container.AdjustTabsSize()
self.notebook.Refresh()

View File

@@ -26,8 +26,10 @@ class AuxiliaryFrame(wx.Frame):
_instance = None
def __init__(self, parent, id=None, title=None, pos=None, size=None, style=None, name=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
if resizeable:
baseStyle = baseStyle | wx.RESIZE_BORDER | wx.MAXIMIZE_BOX
kwargs = {
'parent': parent,
'style': baseStyle if style is None else baseStyle | style}
@@ -51,10 +53,10 @@ class AuxiliaryFrame(wx.Frame):
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
@classmethod
def openOne(cls, parent):
def openOne(cls, parent, *args, **kwargs):
"""If window is open and alive - raise it, open otherwise"""
if not cls._instance:
frame = cls(parent)
frame = cls(parent, *args, **kwargs)
cls._instance = frame
frame.Show()
else:

View File

@@ -19,6 +19,7 @@
import io
import os.path
import zipfile
from collections import OrderedDict
# noinspection PyPackageRequirements
@@ -32,15 +33,17 @@ pyfalog = Logger(__name__)
class BitmapLoader:
# try:
# archive = zipfile.ZipFile(os.path.join(config.pyfaPath, 'imgs.zip'), 'r')
# logging.info("Using zipped image files.")
# except (IOError, TypeError):
# logging.info("Using local image files.")
# archive = None
pyfalog.info("Using local image files.")
archive = None
# Can be None if we're running from tests
if config.imgsZIP is None:
pyfalog.info("Using local image files.")
archive = None
else:
try:
archive = zipfile.ZipFile(config.imgsZIP, 'r')
pyfalog.info("Using zipped image files.")
except (IOError, TypeError):
pyfalog.info("Using local image files.")
archive = None
cached_bitmaps = OrderedDict()
dont_use_cached_bitmaps = False
@@ -86,7 +89,7 @@ class BitmapLoader:
@classmethod
def loadBitmap(cls, name, location):
if cls.scaling_factor is None:
cls.scaling_factor = int(wx.GetApp().GetTopWindow().GetContentScaleFactor())
cls.scaling_factor = 1 if 'wxGTK' in wx.PlatformInfo else int(wx.GetApp().GetTopWindow().GetContentScaleFactor())
scale = cls.scaling_factor
filename, img = cls.loadScaledBitmap(name, location, scale)
@@ -131,8 +134,8 @@ class BitmapLoader:
try:
img_data = cls.archive.read(path)
sbuf = io.StringIO(img_data)
return wx.ImageFromStream(sbuf)
bbuf = io.BytesIO(img_data)
return wx.Image(bbuf)
except KeyError:
pyfalog.warning("Missing icon file from zip: {0}".format(path))
else:

View File

@@ -88,12 +88,12 @@ class BoosterView(d.Display):
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == wx.WXK_ESCAPE and mstate.GetModifiers() == wx.MOD_NONE:
modifiers = event.GetModifiers()
if keycode == wx.WXK_ESCAPE and modifiers == wx.MOD_NONE:
self.unselectAll()
elif keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL:
elif keycode == 65 and modifiers == wx.MOD_CONTROL:
self.selectAll()
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE:
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and modifiers == wx.MOD_NONE:
boosters = self.getSelectedBoosters()
self.removeBoosters(boosters)
event.Skip()
@@ -226,3 +226,23 @@ class BoosterView(d.Display):
continue
boosters.append(booster)
return boosters
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Amount of active boosters
if opt == 1:
amount = len([b for b in fit.boosters if b.active])
return ' ({})'.format(amount) if amount else None
# Total amount of boosters
elif opt == 2:
amount = len(fit.boosters)
return ' ({})'.format(amount) if amount else None
else:
return None

View File

@@ -104,12 +104,12 @@ class CargoView(d.Display):
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == wx.WXK_ESCAPE and mstate.GetModifiers() == wx.MOD_NONE:
modifiers = event.GetModifiers()
if keycode == wx.WXK_ESCAPE and modifiers == wx.MOD_NONE:
self.unselectAll()
elif keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL:
elif keycode == 65 and modifiers == wx.MOD_CONTROL:
self.selectAll()
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE:
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and modifiers == wx.MOD_NONE:
cargos = self.getSelectedCargos()
self.removeCargos(cargos)
event.Skip()
@@ -214,3 +214,19 @@ class CargoView(d.Display):
continue
cargos.append(cargo)
return cargos
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Total amount of cargo items
if opt in (1, 2):
amount = len(fit.cargo)
return ' ({})'.format(amount) if amount else None
else:
return None

View File

@@ -102,12 +102,12 @@ class CommandView(d.Display):
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == wx.WXK_ESCAPE and mstate.GetModifiers() == wx.MOD_NONE:
modifiers = event.GetModifiers()
if keycode == wx.WXK_ESCAPE and modifiers == wx.MOD_NONE:
self.unselectAll()
elif keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL:
elif keycode == 65 and modifiers == wx.MOD_CONTROL:
self.selectAll()
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE:
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and modifiers == wx.MOD_NONE:
commandFits = self.getSelectedCommandFits()
self.removeCommandFits(commandFits)
event.Skip()
@@ -247,3 +247,27 @@ class CommandView(d.Display):
self.mainFrame.command.Submit(cmd.GuiAddCommandFitsCommand(
fitID=self.mainFrame.getActiveFit(),
commandFitIDs=fitIDs))
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Amount of active command fits
if opt == 1:
amount = 0
for commandFit in fit.commandFits:
info = commandFit.getCommandInfo(fitID)
if info is not None and info.active:
amount += 1
return ' ({})'.format(amount) if amount else None
# Total amount of command fits
elif opt == 2:
amount = len(fit.commandFits)
return ' ({})'.format(amount) if amount else None
else:
return None

View File

@@ -120,12 +120,12 @@ class DroneView(Display):
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == wx.WXK_ESCAPE and mstate.GetModifiers() == wx.MOD_NONE:
modifiers = event.GetModifiers()
if keycode == wx.WXK_ESCAPE and modifiers == wx.MOD_NONE:
self.unselectAll()
elif keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL:
elif keycode == 65 and modifiers == wx.MOD_CONTROL:
self.selectAll()
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE:
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and modifiers == wx.MOD_NONE:
drones = self.getSelectedDrones()
self.removeDroneStacks(drones)
event.Skip()
@@ -260,7 +260,7 @@ class DroneView(Display):
drone = self.drones[row]
except IndexError:
return
if wx.GetMouseState().GetModifiers() == wx.MOD_ALT:
if event.GetModifiers() == wx.MOD_ALT:
self.removeDroneStacks([drone])
else:
self.removeDrone(drone)
@@ -337,3 +337,27 @@ class DroneView(Display):
continue
drones.append(drone)
return drones
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Amount of active drones
if opt == 1:
amount = 0
for droneStack in fit.drones:
amount += droneStack.amountActive
return ' ({})'.format(amount) if amount else None
# Total amount of drones
elif opt == 2:
amount = 0
for droneStack in fit.drones:
amount += droneStack.amount
return ' ({})'.format(amount) if amount else None
else:
return None

View File

@@ -117,6 +117,26 @@ class FighterView(wx.Panel):
self.Refresh()
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Amount of active fighter squads
if opt == 1:
amount = len([f for f in fit.fighters if f.active])
return ' ({})'.format(amount) if amount else None
# Total amount of fighter squads
elif opt == 2:
amount = len(fit.fighters)
return ' ({})'.format(amount) if amount else None
else:
return None
class FighterDisplay(d.Display):
@@ -185,12 +205,12 @@ class FighterDisplay(d.Display):
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == wx.WXK_ESCAPE and mstate.GetModifiers() == wx.MOD_NONE:
modifiers = event.GetModifiers()
if keycode == wx.WXK_ESCAPE and modifiers == wx.MOD_NONE:
self.unselectAll()
elif keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL:
elif keycode == 65 and modifiers == wx.MOD_CONTROL:
self.selectAll()
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE:
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and modifiers == wx.MOD_NONE:
fighters = self.getSelectedFighters()
self.removeFighters(fighters)
event.Skip()
@@ -295,12 +315,11 @@ class FighterDisplay(d.Display):
if row != -1:
col = self.getColumn(event.Position)
if col != self.getColIndex(State):
mstate = wx.GetMouseState()
try:
fighter = self.fighters[row]
except IndexError:
return
if mstate.GetModifiers() == wx.MOD_ALT:
if event.GetModifiers() == wx.MOD_ALT:
fighters = getSimilarFighters(self.original, fighter)
else:
fighters = [fighter]

View File

@@ -101,6 +101,25 @@ class ImplantView(wx.Panel):
self.mainFrame.command.Submit(cmd.GuiChangeImplantLocationCommand(
fitID=fitID, source=ImplantLocation.FIT if self.rbFit.GetValue() else ImplantLocation.CHARACTER))
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Amount of active implants
if opt == 1:
amount = len([i for i in fit.appliedImplants if i.active])
return ' ({})'.format(amount) if amount else None
# Total amount of implants
elif opt == 2:
amount = len(fit.appliedImplants)
return ' ({})'.format(amount) if amount else None
else:
return None
class ImplantDisplay(d.Display):
@@ -143,12 +162,12 @@ class ImplantDisplay(d.Display):
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == wx.WXK_ESCAPE and mstate.GetModifiers() == wx.MOD_NONE:
modifiers = event.GetModifiers()
if keycode == wx.WXK_ESCAPE and modifiers == wx.MOD_NONE:
self.unselectAll()
elif keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL:
elif keycode == 65 and modifiers == wx.MOD_CONTROL:
self.selectAll()
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE:
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and modifiers == wx.MOD_NONE:
implants = self.getSelectedImplants()
self.removeImplants(implants)
event.Skip()
@@ -280,7 +299,10 @@ class ImplantDisplay(d.Display):
sourceContext1 = "implantItem" if fit.implantSource == ImplantLocation.FIT else "implantItemChar"
sourceContext2 = "implantItemMisc" if fit.implantSource == ImplantLocation.FIT else "implantItemMiscChar"
itemContext = None if mainImplant is None else Market.getInstance().getCategoryByItem(mainImplant.item).name
menu = ContextMenu.getMenu(self, mainImplant, selection, (sourceContext1, itemContext), (sourceContext2, itemContext))
menu = ContextMenu.getMenu(self, mainImplant, selection,
(sourceContext1, itemContext),
(sourceContext2, itemContext)
)
if menu:
self.PopupMenu(menu)
@@ -294,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

@@ -1,10 +1,11 @@
# noinspection PyPackageRequirements
import wx
from service.fit import Fit
import gui.globalEvents as GE
import gui.mainFrame
from gui.utils.helpers_wxPython import HandleCtrlBackspace
from gui.utils.numberFormatter import formatAmount
from service.fit import Fit
class NotesView(wx.Panel):
@@ -14,14 +15,14 @@ class NotesView(wx.Panel):
self.lastFitId = None
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
mainSizer = wx.BoxSizer(wx.VERTICAL)
self.editNotes = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.BORDER_NONE, )
mainSizer.Add(self.editNotes, 1, wx.EXPAND)
self.editNotes = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.BORDER_NONE)
mainSizer.Add(self.editNotes, 1, wx.EXPAND | wx.ALL, 10)
self.SetSizer(mainSizer)
self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged)
self.Bind(wx.EVT_TEXT, self.onText)
self.editNotes.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
self.saveTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.delayedSave, self.saveTimer)
self.changeTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.delayedSave, self.changeTimer)
def OnKeyDown(self, event):
if event.RawControlDown() and event.GetKeyCode() == wx.WXK_BACK:
@@ -38,7 +39,7 @@ class NotesView(wx.Panel):
sFit = Fit.getInstance()
fit = sFit.getFit(activeFitID)
self.saveTimer.Stop() # cancel any pending timers
self.changeTimer.Stop() # cancel any pending timers
self.Parent.Parent.DisablePage(self, not fit or fit.isStructure)
@@ -51,13 +52,32 @@ class NotesView(wx.Panel):
return
elif activeFitID != self.lastFitId:
self.lastFitId = activeFitID
self.editNotes.SetValue(fit.notes or "")
self.editNotes.ChangeValue(fit.notes or "")
wx.PostEvent(self.mainFrame, GE.FitNotesChanged())
def onText(self, event):
# delay the save so we're not writing to sqlite on every keystroke
self.saveTimer.Stop() # cancel the existing timer
self.saveTimer.Start(1000, True)
self.changeTimer.Stop() # cancel the existing timer
self.changeTimer.Start(1000, True)
def delayedSave(self, event):
event.Skip()
sFit = Fit.getInstance()
sFit.editNotes(self.lastFitId, self.editNotes.GetValue())
wx.PostEvent(self.mainFrame, GE.FitNotesChanged())
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Amount of active implants
if opt in (1, 2):
amount = len(self.editNotes.GetValue())
return ' ({})'.format(formatAmount(amount, 2, 0, 3)) if amount else None
else:
return None

View File

@@ -27,6 +27,7 @@ import gui.builtinAdditionPanes.droneView
import gui.display as d
import gui.fitCommands as cmd
import gui.globalEvents as GE
from eos.const import FittingModuleState
from eos.saveddata.drone import Drone as EosDrone
from eos.saveddata.fighter import Fighter as EosFighter
from eos.saveddata.fit import Fit as EosFit
@@ -74,7 +75,8 @@ class ProjectedView(d.Display):
'Ammo Icon',
'Base Icon',
'Base Name',
'Ammo']
'Ammo',
'Projection Range']
def __init__(self, parent):
d.Display.__init__(self, parent, style=wx.BORDER_NONE)
@@ -127,12 +129,12 @@ class ProjectedView(d.Display):
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == wx.WXK_ESCAPE and mstate.GetModifiers() == wx.MOD_NONE:
modifiers = event.GetModifiers()
if keycode == wx.WXK_ESCAPE and modifiers == wx.MOD_NONE:
self.unselectAll()
elif keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL:
elif keycode == 65 and modifiers == wx.MOD_CONTROL:
self.selectAll()
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE:
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and modifiers == wx.MOD_NONE:
self.mainFrame.command.Submit(cmd.GuiRemoveProjectedItemsCommand(
fitID=self.mainFrame.getActiveFit(),
items=self.getSelectedProjectors(),
@@ -265,7 +267,7 @@ class ProjectedView(d.Display):
selection = self.getSelectedProjectors()
if mainItem not in selection:
selection = [mainItem]
modPressed = wx.GetMouseState().GetModifiers() == wx.MOD_ALT
modPressed = event.GetModifiers() == wx.MOD_ALT
fitID = self.mainFrame.getActiveFit()
if isinstance(mainItem, EosModule) and modPressed:
fit = Fit.getInstance().getFit(fitID)
@@ -340,7 +342,7 @@ class ProjectedView(d.Display):
if mainItem is None:
return
fitID = self.mainFrame.getActiveFit()
modPressed = wx.GetMouseState().GetModifiers() == wx.MOD_ALT
modPressed = event.GetModifiers() == wx.MOD_ALT
if isinstance(mainItem, EosFit):
self.mainFrame.command.Submit(cmd.GuiRemoveProjectedItemsCommand(
fitID=fitID, items=[mainItem], amount=math.inf if modPressed else 1))
@@ -396,3 +398,34 @@ class ProjectedView(d.Display):
fitID=self.mainFrame.getActiveFit(),
projectedFitIDs=fitIDs,
amount=1))
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Amount of active projected items
if opt == 1:
amount = 0
for projectedFit in fit.projectedFits:
info = projectedFit.getProjectionInfo(fitID)
if info is not None and info.active:
amount += 1
amount += len([m for m in fit.projectedModules if m.state > FittingModuleState.OFFLINE])
amount += len([d for d in fit.projectedDrones if d.amountActive > 0])
amount += len([f for f in fit.projectedFighters if f.active])
return ' ({})'.format(amount) if amount else None
# Total amount of projected items
elif opt == 2:
amount = 0
amount += len(fit.projectedFits)
amount += len(fit.projectedModules)
amount += len(fit.projectedDrones)
amount += len(fit.projectedFighters)
return ' ({})'.format(amount) if amount else None
else:
return None

View File

@@ -22,6 +22,7 @@ from gui.builtinContextMenus import shipJump
# Generic item manipulations
from gui.builtinContextMenus import itemRemove
from gui.builtinContextMenus import itemAmountChange
from gui.builtinContextMenus import itemProjectionRange
from gui.builtinContextMenus import droneSplitStack
from gui.builtinContextMenus import itemVariationChange
from gui.builtinContextMenus import moduleMutations
@@ -35,7 +36,8 @@ from gui.builtinContextMenus import cargoAdd
from gui.builtinContextMenus import cargoAddAmmo
from gui.builtinContextMenus import itemProject
from gui.builtinContextMenus import ammoToDmgPattern
from gui.builtinContextMenus import implantSetAdd
from gui.builtinContextMenus import implantSetApply
from gui.builtinContextMenus import implantSetSave
# Price
from gui.builtinContextMenus import priceOptions
# Resistance panel
@@ -44,8 +46,10 @@ from gui.builtinContextMenus import damagePatternChange
from gui.builtinContextMenus import factorReload
from gui.builtinContextMenus.targetProfile import switcher
# Graph extra options
from gui.builtinContextMenus import graphDmgIgnoreResists
from gui.builtinContextMenus import graphDmgApplyProjected
from gui.builtinContextMenus import graphDmgIgnoreResists
from gui.builtinContextMenus import graphLockRange
from gui.builtinContextMenus import graphDroneControlRange
from gui.builtinContextMenus import graphDmgDroneMode
# Additions panel menus
from gui.builtinContextMenus import additionsExportSelection

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