Compare commits

..

191 Commits

Author SHA1 Message Date
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
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
Alexander Maryanovsky
b2c718d614 Implemented copying the currently open fit stats to the clipboard. 2018-09-08 20:14:34 +03:00
216 changed files with 3162484 additions and 1393 deletions

View File

@@ -45,7 +45,7 @@ install:
# 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"
- "python -m pip install --disable-pip-version-check --user --upgrade pip"
# Install the build dependencies of the project. If some dependencies contain
# compiled extensions and are not provided as pre-built wheel packages,
@@ -53,7 +53,7 @@ install:
# target Python version and architecture
- ECHO "Install pip requirements:"
- "python -m pip install -r requirements.txt"
- "python -m pip install PyInstaller"
- "python -m pip install PyInstaller==3.3"
before_build:
# directory that will contain the built files
@@ -65,6 +65,8 @@ before_build:
build_script:
- ECHO "Build pyfa:"
# Build gamedata DB
- "python db_update.py"
##########
# PyInstaller - create binaries for pyfa
##########

View File

@@ -13,6 +13,7 @@ matrix:
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:

View File

@@ -4,7 +4,7 @@
- Python 3.6
- Git CLI installed
- Python, pip and git are all available as command-line commands (add to path if needed)
- 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.
@@ -12,21 +12,21 @@ Virtual environment will be created in *PyfaEnv* folder. Project will be cloned
## Setting up the project manually
Clone the repo
Clone the repository
```
git clone <repo> PyfaDEV
```
Create virtual environment
Create the virtual environment
```
python -m venv PyfaEnv
```
Activate virtual environment
Activate the virtual environment
```
For cmd.exe: PyfaEnv\scripts\activate.bat
For bash: source <venv>/bin/activate
For bash: source <venv>/Scripts/activate
```
> For other OS check [Python documentation](https://docs.python.org/3/library/venv.html)
@@ -36,7 +36,7 @@ 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 libs from *requirements.txt* are installed
Check that the libs from *requirements.txt* are installed
```
pip list
```
@@ -52,11 +52,11 @@ python PyfaDEV\pyfa.py
Install PyCharm / Other IntelliJ product with Python plugin
After launching select *Check out from Version Control* -> *GIt*
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 repo URL and seect the folder to which to clone the project, press *Clone*.
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)
@@ -68,7 +68,7 @@ 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 *requirements.txt*. Press *Install requirements*. Install all requirements.
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)
@@ -76,4 +76,27 @@ Create new *Run Configuration*. Set correct *Script path* and *Python interprete
![Run configuraion](https://user-images.githubusercontent.com/54093496/66862970-b4460300-ef9a-11e9-9fb4-20e24759904b.png)
Test that the project is starting properly.
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

@@ -72,7 +72,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

@@ -39,6 +39,8 @@ loggingLevel = None
logging_setup = None
cipher = None
clientHash = None
experimentalFeatures = None
version = None
ESI_CACHE = 'esi_cache'
@@ -103,6 +105,7 @@ def defPaths(customSavePath=None):
global cipher
global clientHash
global version
global experimentalFeatures
pyfalog.debug("Configuring Pyfa")
@@ -168,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

510
db_update.py Normal file
View File

@@ -0,0 +1,510 @@
#!/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 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:
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:
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
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 = []
for row in eveTypesData:
for attrId, attrName in {4: 'mass', 38: 'capacity', 161: 'volume', 162: 'radius'}.items():
if attrName in row:
newData.append({'typeID': row['typeID'], 'attributeID': attrId, 'value': row[attrName]})
for typeData in data:
if typeData['typeID'] not in eveTypeIds:
continue
for row in typeData.get('dogmaAttributes', ()):
row['typeID'] = typeData['typeID']
newData.append(row)
_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))
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)
# 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

@@ -18,6 +18,38 @@
# =============================================================================
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:
@@ -31,3 +63,9 @@ def calculateRangeFactor(srcOptimalRange, srcFalloffRange, distance, restrictedR
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

@@ -85,7 +85,7 @@ pyfalog.debug('Importing gamedata DB scheme')
from eos.db.gamedata import alphaClones, attribute, category, effect, group, item, marketGroup, metaData, metaGroup, queries, traits, unit, dynamicAttributes
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

@@ -44,7 +44,9 @@ items_table = Table("invtypes", gamedata_meta,
Column("metaLevel", Integer),
Column("metaGroupID", Integer, ForeignKey("invmetagroups.metaGroupID"), index=True),
Column("variationParentTypeID", Integer, ForeignKey("invtypes.typeID"), index=True),
Column("replacements", String))
Column("replacements", String),
Column("reqskills", String),
Column("requiredfor", String))
from .traits import traits_table # noqa

View File

@@ -92,7 +92,8 @@ def getItem(lookfor, eager=None):
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
if item is not None:
itemNameMap[lookfor] = item.ID
else:
raise TypeError("Need integer or string as argument")
return item
@@ -195,7 +196,8 @@ def getGroup(lookfor, eager=None):
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
if group is not None:
groupNameMap[lookfor] = group.ID
else:
raise TypeError("Need integer or string as argument")
return group
@@ -224,7 +226,8 @@ def getCategory(lookfor, eager=None):
# Category names are unique, so we can use first() instead of one()
category = 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
@@ -253,7 +256,8 @@ def getMetaGroup(lookfor, eager=None):
# MetaGroup names are unique, so we can use first() instead of one()
metaGroup = 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
@@ -420,25 +424,3 @@ def getDynamicItem(itemID, eager=None):
except exc.NoResultFound:
result = 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

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

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

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

@@ -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),
@@ -233,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,248 +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", 1, 0, 0, 0],
["[Generic]Thermal", 0, 1, 0, 0],
["[Generic]Kinetic", 0, 0, 1, 0],
["[Generic]Explosive", 0, 0, 0, 1],
["[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", 7.7, 7.7, 0, 0],
["[Frequency Crystals][T2] Scorch", 9, 2, 0, 0],
["[Frequency Crystals][T2] Gleam", 7, 7, 0, 0],
["[Frequency Crystals][T2] Aurora", 5, 3, 0, 0],
["[Frequency Crystals]Multifrequency", 7, 5, 0, 0],
["[Frequency Crystals]Gamma", 7, 4, 0, 0],
["[Frequency Crystals]Xray", 6, 4, 0, 0],
["[Frequency Crystals]Ultraviolet", 6, 3, 0, 0],
["[Frequency Crystals]Standard", 5, 3, 0, 0],
["[Frequency Crystals]Infrared", 5, 2, 0, 0],
["[Frequency Crystals]Microwave", 4, 2, 0, 0],
["[Frequency Crystals]Radio", 5, 0, 0, 0],
["[Hybrid Charges][T2] Void", 0, 7.7, 7.7, 0],
["[Hybrid Charges][T2] Null", 0, 6, 5, 0],
["[Hybrid Charges][T2] Javelin", 0, 8, 6, 0],
["[Hybrid Charges][T2] Spike", 0, 4, 4, 0],
["[Hybrid Charges]Antimatter", 0, 5, 7, 0],
["[Hybrid Charges]Plutonium", 0, 5, 6, 0],
["[Hybrid Charges]Uranium", 0, 4, 6, 0],
["[Hybrid Charges]Thorium", 0, 4, 5, 0],
["[Hybrid Charges]Lead", 0, 3, 5, 0],
["[Hybrid Charges]Iridium", 0, 3, 4, 0],
["[Hybrid Charges]Tungsten", 0, 2, 4, 0],
["[Hybrid Charges]Iron", 0, 2, 3, 0],
["[Missiles]Mjolnir", 1, 0, 0, 0],
["[Missiles]Inferno", 0, 1, 0, 0],
["[Missiles]Scourge", 0, 0, 1, 0],
["[Missiles]Nova", 0, 0, 0, 1],
["[Missiles][Structure] Standup Missile", 1, 1, 1, 1],
["[Projectile Ammo][T2] Hail", 0, 0, 3.3, 12.1],
["[Projectile Ammo][T2] Barrage", 0, 0, 5, 6],
["[Projectile Ammo][T2] Quake", 0, 0, 5, 9],
["[Projectile Ammo][T2] Tremor", 0, 0, 3, 5],
["[Projectile Ammo]EMP", 9, 0, 1, 2],
["[Projectile Ammo]Phased Plasma", 0, 10, 2, 0],
["[Projectile Ammo]Fusion", 0, 0, 2, 10],
["[Projectile Ammo]Depleted Uranium", 0, 3, 2, 3],
["[Projectile Ammo]Titanium Sabot", 0, 0, 6, 2],
["[Projectile Ammo]Proton", 3, 0, 2, 0],
["[Projectile Ammo]Carbonized Lead", 0, 0, 4, 1],
["[Projectile Ammo]Nuclear", 0, 0, 1, 4],
# 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.add(damageProfile)
else:
damageProfile.emAmount = em
damageProfile.thermalAmount = therm
damageProfile.kineticAmount = kin
damageProfile.explosiveAmount = exp
eos.db.commit()
@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.getTargetProfile(name)
if targetProfile is None:
targetProfile = es_TargetProfile(em, therm, kin, exp, maxVel, sigRad, radius)
targetProfile.name = name
eos.db.add(targetProfile)
else:
targetProfile.emAmount = em
targetProfile.thermalAmount = therm
targetProfile.kineticAmount = kin
targetProfile.explosiveAmount = exp
targetProfile.maxVelocity = maxVel
targetProfile.signatureRadius = sigRad
targetProfile.radius = radius
eos.db.commit()
@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.add(damageProfile)
else:
damageProfile.emAmount = em
damageProfile.thermalAmount = therm
damageProfile.kineticAmount = kin
damageProfile.explosiveAmount = exp
eos.db.commit()

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

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

@@ -275,6 +275,17 @@ class Effect51(BaseEffect):
fit.ship.multiplyItemAttr('rechargeRate', module.getModifiedItemAttr('capacitorRechargeRateMultiplier'), **kwargs)
class Effect54(BaseEffect):
"""
targetPassively
Used by:
Modules from group: Passive Targeting System (6 of 6)
"""
type = 'active'
class Effect55(BaseEffect):
"""
targetHostiles
@@ -1344,7 +1355,6 @@ class Effect508(BaseEffect):
Ship: Cheetah
Ship: Freki
Ship: Republic Fleet Firetail
Ship: Rifter
Ship: Slasher
Ship: Stiletto
Ship: Wolf
@@ -1731,7 +1741,7 @@ class Effect596(BaseEffect):
ammoInfluenceRange
Used by:
Items from category: Charge (588 of 952)
Items from category: Charge (590 of 954)
"""
type = 'passive'
@@ -1746,9 +1756,10 @@ class Effect598(BaseEffect):
ammoSpeedMultiplier
Used by:
Charges from group: Festival Charges (28 of 28)
Charges from group: Festival Charges (27 of 28)
Charges from group: Interdiction Probe (2 of 2)
Items from market group: Special Edition Assets > Special Edition Festival Assets (32 of 35)
Charges from group: Structure Festival Charges (2 of 2)
Special Edition Assetss from group: Festival Charges Expired (4 of 4)
"""
type = 'passive'
@@ -1783,7 +1794,13 @@ class Effect600(BaseEffect):
ammoTrackingMultiplier
Used by:
Items from category: Charge (182 of 952)
Charges from group: Advanced Artillery Ammo (8 of 8)
Charges from group: Advanced Autocannon Ammo (8 of 8)
Charges from group: Advanced Beam Laser Crystal (8 of 8)
Charges from group: Advanced Blaster Charge (8 of 8)
Charges from group: Advanced Exotic Plasma Charge (6 of 6)
Charges from group: Advanced Pulse Laser Crystal (8 of 8)
Charges from group: Advanced Railgun Charge (8 of 8)
Charges from group: Projectile Ammo (128 of 128)
"""
@@ -2310,7 +2327,7 @@ class Effect804(BaseEffect):
ammoInfluenceCapNeed
Used by:
Items from category: Charge (494 of 952)
Items from category: Charge (496 of 954)
"""
type = 'passive'
@@ -4787,7 +4804,7 @@ class Effect1615(BaseEffect):
shipAdvancedSpaceshipCommandAgilityBonus
Used by:
Items from market group: Ships > Capital Ships (40 of 40)
Items from market group: Ships > Capital Ships (40 of 41)
"""
type = 'passive'
@@ -4820,7 +4837,7 @@ class Effect1617(BaseEffect):
shipCapitalAgilityBonus
Used by:
Items from market group: Ships > Capital Ships (31 of 40)
Items from market group: Ships > Capital Ships (31 of 41)
"""
type = 'passive'
@@ -6797,7 +6814,10 @@ class Effect2298(BaseEffect):
scanStrengthBonusPercentPassive
Used by:
Implants named like: High grade (20 of 66)
Implants named like: High grade Grail (5 of 6)
Implants named like: High grade Jackal (5 of 6)
Implants named like: High grade Spur (5 of 6)
Implants named like: High grade Talon (5 of 6)
"""
type = 'passive'
@@ -9152,7 +9172,7 @@ class Effect3001(BaseEffect):
Used by:
Modules from group: Missile Launcher Torpedo (22 of 22)
Items from market group: Ship Equipment > Turrets & Bays (429 of 888)
Items from market group: Ship Equipment > Turrets & Launchers (429 of 889)
Module: Interdiction Sphere Launcher I
"""
@@ -10683,6 +10703,7 @@ class Effect3526(BaseEffect):
Used by:
Ships from group: Force Recon Ship (8 of 9)
Ship: Venture
Skill: Cynosural Field Theory
"""
@@ -12747,8 +12768,8 @@ class Effect4038(BaseEffect):
@staticmethod
def handler(fit, module, context, projectionRange, **kwargs):
fit.modules.filteredItemMultiply(lambda mod: 'overloadECMStrenghtBonus' in mod.itemModifiedAttributes,
'overloadECMStrenghtBonus', module.getModifiedItemAttr('overloadBonusMultiplier'), **kwargs)
fit.modules.filteredItemMultiply(lambda mod: 'overloadECMStrengthBonus' in mod.itemModifiedAttributes,
'overloadECMStrengthBonus', module.getModifiedItemAttr('overloadBonusMultiplier'), **kwargs)
class Effect4039(BaseEffect):
@@ -24463,7 +24484,7 @@ class Effect6104(BaseEffect):
entosisDurationMultiply
Used by:
Items from market group: Ships > Capital Ships (31 of 40)
Items from market group: Ships > Capital Ships (31 of 41)
"""
type = 'passive'
@@ -27490,12 +27511,32 @@ class Effect6439(BaseEffect):
stackingPenalties=True, **kwargs)
class Effect6440(BaseEffect):
"""
fighterAbilityAfterburner
Used by:
Fighters named like: Shadow (2 of 2)
Fighters named like: Siren (4 of 4)
"""
displayName = 'Afterburner'
grouped = True
runTime = 'late'
type = 'active'
@staticmethod
def handler(fit, module, context, projectionRange, **kwargs):
module.boostItemAttr('maxVelocity', module.getModifiedItemAttr('fighterAbilityAfterburnerSpeedBonus'),
stackingPenalties=True, **kwargs)
class Effect6441(BaseEffect):
"""
fighterAbilityMicroWarpDrive
Used by:
Items from category: Fighter (48 of 82)
Items from category: Fighter (44 of 82)
"""
displayName = 'Microwarpdrive'
@@ -27892,7 +27933,7 @@ class Effect6488(BaseEffect):
Charges from group: Sensor Booster Script (3 of 3)
"""
type = 'active'
type = 'passive'
@staticmethod
def handler(fit, module, context, projectionRange, **kwargs):
@@ -35121,7 +35162,7 @@ class Effect7092(BaseEffect):
Ship: Hydra
Ship: Leshak
Ship: Tiamat
Ship: Triglavian Dreadnought
Ship: Zirnitra
"""
type = 'passive'
@@ -35145,7 +35186,7 @@ class Effect7093(BaseEffect):
Ship: Hydra
Ship: Leshak
Ship: Tiamat
Ship: Triglavian Dreadnought
Ship: Zirnitra
"""
type = 'passive'
@@ -35168,7 +35209,7 @@ class Effect7094(BaseEffect):
Ship: Hydra
Ship: Leshak
Ship: Tiamat
Ship: Triglavian Dreadnought
Ship: Zirnitra
"""
type = 'passive'
@@ -35225,7 +35266,7 @@ class Effect7112(BaseEffect):
Ship: Hydra
Ship: Leshak
Ship: Tiamat
Ship: Triglavian Dreadnought
Ship: Zirnitra
"""
type = 'passive'
@@ -36015,7 +36056,7 @@ class Effect7232(BaseEffect):
modifyDamageMultiplierBonusMax
Used by:
Implants named like: Grade Mimesis (10 of 18)
Implants named like: Grade Mimesis (15 of 18)
"""
type = 'passive'
@@ -36032,7 +36073,7 @@ class Effect7233(BaseEffect):
modifyDamageMultiplierBonusPerCycle
Used by:
Implants named like: Grade Mimesis (10 of 18)
Implants named like: Grade Mimesis (15 of 18)
"""
type = 'passive'
@@ -36049,7 +36090,7 @@ class Effect7234(BaseEffect):
implantSetMimesis
Used by:
Implants named like: Grade Mimesis (12 of 18)
Implants named like: Grade Mimesis (18 of 18)
"""
runTime = 'early'
@@ -36070,7 +36111,7 @@ class Effect7238(BaseEffect):
shipBonusDreadnoughtPC1DamageMultMax
Used by:
Ship: Triglavian Dreadnought
Ship: Zirnitra
"""
type = 'passive'
@@ -36086,7 +36127,7 @@ class Effect7239(BaseEffect):
shipBonusDreadnoughtPC2ArmorResists
Used by:
Ship: Triglavian Dreadnought
Ship: Zirnitra
"""
type = 'passive'
@@ -36103,7 +36144,7 @@ class Effect7240(BaseEffect):
shipBonusDreadnoughtPC3WeaponSpeed
Used by:
Ship: Triglavian Dreadnought
Ship: Zirnitra
"""
type = 'passive'
@@ -36129,3 +36170,69 @@ class Effect7242(BaseEffect):
level = container.level if 'skill' in context else 1
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill('Capital Precursor Weapon'),
'damageMultiplier', container.getModifiedItemAttr('damageMultiplierBonus') * level, **kwargs)
class Effect7247(BaseEffect):
"""
shipBonusHAMHMLAoeVelocityMC
Used by:
Ship: Bellicose
"""
type = 'passive'
@staticmethod
def handler(fit, ship, context, projectionRange, **kwargs):
fit.modules.filteredChargeBoost(lambda mod: (mod.charge.requiresSkill('Heavy Missiles') or
mod.charge.requiresSkill('Heavy Assault Missiles')),
'aoeVelocity', ship.getModifiedItemAttr('shipBonusMC'),
skill='Minmatar Cruiser', **kwargs)
class Effect7248(BaseEffect):
"""
shipPBonusROFMF
Used by:
Ship: Rifter
"""
type = 'passive'
@staticmethod
def handler(fit, ship, context, projectionRange, **kwargs):
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill('Small Projectile Turret'),
'speed', ship.getModifiedItemAttr('shipBonusMF'), skill='Minmatar Frigate', **kwargs)
class Effect8011(BaseEffect):
"""
shieldHpBonusPostPercentHpLocationShip
Used by:
Implants named like: grade Nirvana (10 of 12)
"""
type = 'passive'
@staticmethod
def handler(fit, container, context, projectionRange, **kwargs):
fit.ship.boostItemAttr('shieldCapacity', container.getModifiedItemAttr('shieldHpBonus'), **kwargs)
class Effect8013(BaseEffect):
"""
setBonusNirvana
Used by:
Implants named like: grade Nirvana (12 of 12)
"""
runTime = 'early'
type = 'passive'
@staticmethod
def handler(fit, implant, context, projectionRange, **kwargs):
fit.appliedImplants.filteredItemMultiply(lambda target: target.item.requiresSkill('Cybernetics'),
'shieldHpBonus', implant.getModifiedItemAttr('ImplantSetNirvana') or 1, **kwargs)

View File

@@ -18,6 +18,7 @@
# ===============================================================================
import json
from collections import OrderedDict
from logbook import Logger
@@ -314,50 +315,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 +350,8 @@ class Item(EqBase):
500016: "sisters",
500018: "mordu",
500019: "sansha",
500020: "serpentis"
500020: "serpentis",
500026: "triglavian"
}
@property
@@ -399,6 +377,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",

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
@@ -96,7 +97,7 @@ class ChargeAttrShortcut:
return return_value or default
class ModifiedAttributeDict(collections.MutableMapping):
class ModifiedAttributeDict(MutableMapping):
overrides_enabled = False
class CalculationPlaceholder:

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:
@@ -122,7 +284,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 +300,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

@@ -99,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)

View File

@@ -21,21 +21,24 @@ import datetime
import time
from copy import deepcopy
from itertools import chain
from math import asinh, log, sqrt
from math import log, sqrt
from logbook import Logger
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
@@ -1529,9 +1561,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
@@ -1626,6 +1656,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

View File

@@ -318,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):

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
@@ -100,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:
@@ -149,7 +334,7 @@ class TargetProfile:
eos.db.save(pattern)
else:
pattern = TargetProfile(**fields)
pattern.name = name.strip()
pattern.rawName = name.strip()
eos.db.save(pattern)
patterns.append(pattern)
@@ -166,7 +351,7 @@ class TargetProfile:
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,
@@ -178,9 +363,39 @@ class TargetProfile:
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

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

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,40 +18,9 @@
# =============================================================================
import math
from service.settings import GraphSettings
# 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 checkLockRange(src, distance):
if distance is None:
return True

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

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

View File

@@ -152,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):

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,8 +20,8 @@
import math
from eos.calc import calculateRangeFactor
from graphs.calc import calculateMultiplier, checkLockRange, checkDroneControlRange
from eos.calc import calculateMultiplier, calculateRangeFactor
from graphs.calc import checkLockRange, checkDroneControlRange
from graphs.data.base import SmoothPointGetter

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

@@ -43,7 +43,7 @@ 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
@@ -74,6 +74,8 @@ class GraphFrame(AuxiliaryFrame):
# Setup - graph selector
for view in FitGraph.views:
if view.hidden and not includeHidden:
continue
self.graphSelection.Append(view.name, view())
self.graphSelection.SetSelection(0)
self.ctrlPanel.updateControls(layout=False)
@@ -101,9 +103,9 @@ class GraphFrame(AuxiliaryFrame):
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()

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,7 +51,7 @@ 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, ignoreAfflictors=()):

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

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

@@ -33,12 +33,17 @@ pyfalog = Logger(__name__)
class BitmapLoader:
try:
archive = zipfile.ZipFile(config.imgsZIP, 'r')
pyfalog.info("Using zipped image files.")
except (IOError, TypeError):
# 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
@@ -84,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)

View File

@@ -299,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)

View File

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

View File

@@ -3,7 +3,6 @@ from gui.contextMenu import ContextMenuUnconditional
from gui.utils.clipboard import toClipboard
from service.fit import Fit
from service.port.eft import exportDrones, exportFighters, exportCargo, exportImplants, exportBoosters
from service.settings import ContextMenuSettings
viewSpecMap = {
@@ -17,12 +16,12 @@ viewSpecMap = {
class AdditionsExportAll(ContextMenuUnconditional):
visibilitySetting = 'additionsCopyPaste'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext):
if not ContextMenuSettings.getInstance().get('additionsCopyPaste'):
return False
if srcContext not in viewSpecMap:
return False
fit = Fit.getInstance().getFit(self.mainFrame.getActiveFit())

View File

@@ -3,7 +3,6 @@ from gui.contextMenu import ContextMenuSelection
from gui.utils.clipboard import toClipboard
from service.fit import Fit
from service.port.eft import exportDrones, exportFighters, exportCargo, exportImplants, exportBoosters
from service.settings import ContextMenuSettings
viewSpecMap = {
@@ -17,12 +16,12 @@ viewSpecMap = {
class AdditionsExportAll(ContextMenuSelection):
visibilitySetting = 'additionsCopyPaste'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext, selection):
if not ContextMenuSettings.getInstance().get('additionsCopyPaste'):
return False
if srcContext not in viewSpecMap:
return False
if not selection:

View File

@@ -4,7 +4,6 @@ from gui.contextMenu import ContextMenuUnconditional
from gui.utils.clipboard import fromClipboard
from service.fit import Fit
from service.port.eft import parseAdditions
from service.settings import ContextMenuSettings
viewSpecMap = {
@@ -18,12 +17,12 @@ viewSpecMap = {
class AdditionsImport(ContextMenuUnconditional):
visibilitySetting = 'additionsCopyPaste'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext):
if not ContextMenuSettings.getInstance().get('additionsCopyPaste'):
return False
if srcContext not in viewSpecMap:
return False
fit = Fit.getInstance().getFit(self.mainFrame.getActiveFit())

View File

@@ -5,19 +5,16 @@ import gui.globalEvents as GE
import gui.mainFrame
from gui.contextMenu import ContextMenuSingle
from service.fit import Fit
from service.settings import ContextMenuSettings
class AmmoToDmgPattern(ContextMenuSingle):
visibilitySetting = 'ammoPattern'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
def display(self, callingWindow, srcContext, mainItem):
if not self.settings.get('ammoPattern'):
return False
if srcContext not in ("marketItemGroup", "marketItemMisc") or self.mainFrame.getActiveFit() is None:
return False

View File

@@ -19,8 +19,8 @@ class AddToCargo(ContextMenuSingle):
sFit = Fit.getInstance()
fitID = self.mainFrame.getActiveFit()
fit = sFit.getFit(fitID)
# Make sure context menu registers in the correct view
if not fit or fit.isStructure:
if not fit or (fit.isStructure and mainItem.category.ID != 8):
return False
return True

View File

@@ -1,4 +1,5 @@
from collections import OrderedDict
from itertools import chain
# noinspection PyPackageRequirements
import wx
@@ -6,7 +7,8 @@ import wx
import gui.globalEvents as GE
import gui.mainFrame
from gui.contextMenu import ContextMenuUnconditional
from service.damagePattern import DamagePattern as import_DamagePattern
from gui.utils.sorter import smartSort
from service.damagePattern import DamagePattern as DmgPatternSvc
from service.fit import Fit
@@ -23,97 +25,90 @@ class ChangeDamagePattern(ContextMenuUnconditional):
return self.mainFrame.getActiveFit() is not None
def getText(self, callingWindow, itmContext):
sDP = import_DamagePattern.getInstance()
sDP = DmgPatternSvc.getInstance()
sFit = Fit.getInstance()
fitID = self.mainFrame.getActiveFit()
self.fit = sFit.getFit(fitID)
self.patterns = sDP.getDamagePatternList()
self.patterns.sort(key=lambda p: (p.name not in ["Uniform", "Selected Ammo"], p.name))
builtinPatterns = sDP.getBuiltinDamagePatternList()
userPatterns = sorted(sDP.getUserDamagePatternList(), key=lambda p: smartSort(p.fullName))
# Order here is important: patterns with duplicate names from the latter will overwrite
# patterns from the former
self.patterns = sorted(
chain(builtinPatterns, userPatterns),
key=lambda p: p.fullName not in ["Uniform", "Selected Ammo"])
self.patternIds = {}
self.subMenus = OrderedDict()
self.singles = []
# iterate and separate damage patterns based on "[Parent] Child"
self.patternEventMap = {}
self.items = (OrderedDict(), OrderedDict())
for pattern in self.patterns:
start, end = pattern.name.find('['), pattern.name.find(']')
if start is not -1 and end is not -1:
currBase = pattern.name[start + 1:end]
name = pattern.name[end + 1:].strip()
if not name:
self.singles.append(pattern)
continue
# set helper attr
setattr(pattern, "_name", name)
if currBase not in self.subMenus:
self.subMenus[currBase] = []
self.subMenus[currBase].append(pattern)
else:
self.singles.append(pattern)
container = self.items
for categoryName in pattern.hierarchy:
container = container[1].setdefault(categoryName, (OrderedDict(), OrderedDict()))
container[0][pattern.shortName] = pattern
# return list of names, with singles first followed by submenu names
self.m = [p.name for p in self.singles] + list(self.subMenus.keys())
return self.m
return list(self.items[0].keys()) + list(self.items[1].keys())
def addPattern(self, rootMenu, pattern):
def _addPattern(self, parentMenu, pattern, name):
id = ContextMenuUnconditional.nextID()
name = getattr(pattern, "_name", pattern.name) if pattern is not None else "No Profile"
self.patternIds[id] = pattern
menuItem = wx.MenuItem(rootMenu, id, name, kind=wx.ITEM_CHECK)
rootMenu.Bind(wx.EVT_MENU, self.handlePatternSwitch, menuItem)
# set pattern attr to menu item
menuItem.pattern = pattern
self.patternEventMap[id] = pattern
menuItem = wx.MenuItem(parentMenu, id, name, kind=wx.ITEM_CHECK)
parentMenu.Bind(wx.EVT_MENU, self.handlePatternSwitch, menuItem)
# determine active pattern
sFit = Fit.getInstance()
fitID = self.mainFrame.getActiveFit()
fit = sFit.getFit(fitID)
if fit:
dp = fit.damagePattern
checked = dp is pattern
else:
checked = False
checked = fit.damagePattern is pattern if fit else False
return menuItem, checked
def _addCategory(self, parentMenu, name):
id = ContextMenuUnconditional.nextID()
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handlePatternSwitch, menuItem)
return menuItem
def isChecked(self, i):
try:
single = self.singles[i]
patternName = list(self.items[0].keys())[i]
except IndexError:
return super().isChecked(i)
if self.fit and single is self.fit.damagePattern:
pattern = self.items[0][patternName]
if self.fit and pattern is self.fit.damagePattern:
return True
return False
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
# Attempt to remove attribute which carries info if non-sub-items should
# be checked or not
self.checked = None
if self.m[i] not in self.subMenus:
# if we're trying to get submenu to something that shouldn't have one,
# redirect event of the item to handlePatternSwitch and put pattern in
# our patternIds mapping, then return None for no submenu
# Pattern as menu item
if i < len(self.items[0]):
id = pitem.GetId()
self.patternIds[id] = self.singles[i]
self.patternEventMap[id] = list(self.items[0].values())[i]
rootMenu.Bind(wx.EVT_MENU, self.handlePatternSwitch, pitem)
return False
sub = wx.Menu()
# Items that have a parent
# Category as menu item - expands further
msw = "wxMSW" in wx.PlatformInfo
for pattern in self.subMenus[self.m[i]]:
mitem, checked = self.addPattern(rootMenu if msw else sub, pattern)
sub.Append(mitem)
mitem.Check(checked)
return sub
def makeMenu(container, parentMenu):
menu = wx.Menu()
for name, subcontainer in container[1].items():
menuItem = self._addCategory(rootMenu if msw else parentMenu, name)
subMenu = makeMenu(subcontainer, menu)
menuItem.SetSubMenu(subMenu)
menu.Append(menuItem)
for name, pattern in container[0].items():
menuItem, checked = self._addPattern(rootMenu if msw else parentMenu, pattern, name)
menu.Append(menuItem)
menuItem.Check(checked)
menu.Bind(wx.EVT_MENU, self.handlePatternSwitch)
return menu
container = list(self.items[1].values())[i - len(self.items[0])]
subMenu = makeMenu(container, rootMenu)
return subMenu
def handlePatternSwitch(self, event):
pattern = self.patternIds.get(event.Id, False)
pattern = self.patternEventMap.get(event.Id, False)
if pattern is False:
event.Skip()
return

View File

@@ -5,7 +5,7 @@ from gui.contextMenu import ContextMenuUnconditional
from service.implantSet import ImplantSets as s_ImplantSets
class AddImplantSet(ContextMenuUnconditional):
class ImplantSetApply(ContextMenuUnconditional):
def display(self, callingWindow, srcContext):
@@ -14,10 +14,11 @@ class AddImplantSet(ContextMenuUnconditional):
if len(implantSets) == 0:
return False
return srcContext in ("implantItemMisc", "implantEditor")
def getText(self, callingWindow, itmContext):
return "Add Implant Set"
def getText(self, callingWindow, context):
return "Apply Implant Set"
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
m = wx.Menu()
@@ -49,4 +50,4 @@ class AddImplantSet(ContextMenuUnconditional):
self.callingWindow.addImplantSet(impSet)
AddImplantSet.register()
ImplantSetApply.register()

View File

@@ -0,0 +1,77 @@
import wx
import gui.mainFrame
from gui.contextMenu import ContextMenuUnconditional
from service.fit import Fit
class ImplantSetSave(ContextMenuUnconditional):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext):
if srcContext not in ('implantItemMisc', 'implantItemMiscChar'):
return False
fit = Fit.getInstance().getFit(self.mainFrame.getActiveFit())
self.implants = fit.appliedImplants[:]
if not self.implants:
return False
return True
def getText(self, callingWindow, context):
return 'Save as New Implant Set'
def activate(self, callingWindow, fullContext, i):
with NameDialog(self.mainFrame, '') as dlg:
if dlg.ShowModal() == wx.ID_OK:
name = dlg.input.GetLineText(0).strip()
if name == '':
return
from gui.setEditor import ImplantSetEditor
ImplantSetEditor.openOne(parent=self.mainFrame, dataToAdd=(name, self.implants))
ImplantSetSave.register()
class NameDialog(wx.Dialog):
def __init__(self, parent, value):
super().__init__(parent, title='New Implant Set', style=wx.DEFAULT_DIALOG_STYLE)
self.SetMinSize((346, 156))
bSizer1 = wx.BoxSizer(wx.VERTICAL)
bSizer2 = wx.BoxSizer(wx.VERTICAL)
text = wx.StaticText(self, wx.ID_ANY, 'Enter a name for your new Implant Set:')
bSizer2.Add(text, 0)
bSizer1.Add(bSizer2, 0, wx.ALL, 10)
self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER)
if value is None:
value = ''
else:
value = str(value)
self.input.SetValue(value)
self.input.SelectAll()
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
bSizer3 = wx.BoxSizer(wx.VERTICAL)
bSizer3.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.BOTTOM | wx.EXPAND, 15)
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
self.input.SetFocus()
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
self.SetSizer(bSizer1)
self.CenterOnParent()
self.Fit()
def processEnter(self, evt):
self.EndModal(wx.ID_OK)

View File

@@ -1,19 +1,16 @@
import gui.fitCommands as cmd
import gui.mainFrame
from gui.contextMenu import ContextMenuSingle
from service.settings import ContextMenuSettings
class FillWithItem(ContextMenuSingle):
visibilitySetting = 'moduleFill'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
def display(self, callingWindow, srcContext, mainItem):
if not self.settings.get('moduleFill'):
return False
if srcContext not in ('marketItemGroup', 'marketItemMisc'):
return False

View File

@@ -2,19 +2,16 @@ import gui.fitCommands as cmd
import gui.mainFrame
from gui.contextMenu import ContextMenuSingle
from service.fit import Fit
from service.settings import ContextMenuSettings
class ProjectItem(ContextMenuSingle):
visibilitySetting = 'project'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
def display(self, callingWindow, srcContext, mainItem):
if not self.settings.get('project'):
return False
if srcContext not in ("marketItemGroup", "marketItemMisc") or self.mainFrame.getActiveFit() is None:
return False

View File

@@ -7,19 +7,16 @@ from gui.contextMenu import ContextMenuCombined
from gui.fitCommands.helpers import getSimilarModPositions, getSimilarFighters
from service.fit import Fit
from service.market import Market
from service.settings import ContextMenuSettings
class ChangeItemToVariation(ContextMenuCombined):
visibilitySetting = 'metaSwap'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
def display(self, callingWindow, srcContext, mainItem, selection):
if not self.settings.get('metaSwap'):
return False
if self.mainFrame.getActiveFit() is None or srcContext not in (
'fittingModule',
'droneItem',
@@ -59,8 +56,11 @@ class ChangeItemToVariation(ContextMenuCombined):
return x.metaLevel or 0
def get_metagroup(x):
# We want deadspace before officer mods
remap = {5: 6, 6: 5}
remap = {
# We want deadspace before officer mods
5: 6, 6: 5,
# For structures we want t1-t2-faction
54: 52, 52: 54}
metaGroup = sMkt.getMetaGroupByItem(x)
return remap.get(metaGroup.ID, metaGroup.ID) if metaGroup is not None else 0

View File

@@ -2,20 +2,16 @@ import gui.fitCommands as cmd
import gui.mainFrame
from gui.contextMenu import ContextMenuSingle
from service.fit import Fit
from service.settings import ContextMenuSettings
class FillWithModule(ContextMenuSingle):
visibilitySetting = 'moduleFill'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
def display(self, callingWindow, srcContext, mainItem):
if not self.settings.get('moduleFill'):
return False
if mainItem is None or getattr(mainItem, 'isEmpty', False):
return False

View File

@@ -9,20 +9,17 @@ import gui.mainFrame
from eos.utils.spoolSupport import SpoolType, SpoolOptions
from gui.contextMenu import ContextMenuSingle
from service.fit import Fit
from service.settings import ContextMenuSettings
class ChangeModuleSpool(ContextMenuSingle):
visibilitySetting = 'spoolup'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
self.resetId = None
def display(self, callingWindow, srcContext, mainItem):
if not self.settings.get('spoolup'):
return False
if srcContext not in ('fittingModule', 'projectedModule') or self.mainFrame.getActiveFit() is None:
return False

View File

@@ -8,19 +8,16 @@ from gui.bitmap_loader import BitmapLoader
from gui.contextMenu import ContextMenuSingle
from service.character import Character
from service.fit import Fit
from service.settings import ContextMenuSettings
class ChangeAffectingSkills(ContextMenuSingle):
visibilitySetting = 'changeAffectingSkills'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
def display(self, callingWindow, srcContext, mainItem):
if not self.settings.get('changeAffectingSkills'):
return False
if srcContext not in (
"fittingModule", "fittingCharge",
"fittingShip", "droneItem",

View File

@@ -7,6 +7,7 @@ import wx
import gui.mainFrame
from eos.saveddata.targetProfile import TargetProfile
from gui.contextMenu import ContextMenuUnconditional
from gui.utils.sorter import smartSort
from service.targetProfile import TargetProfile as svc_TargetProfile
@@ -18,74 +19,68 @@ class TargetProfileAdder(ContextMenuUnconditional):
def display(self, callingWindow, srcContext):
if srcContext != 'graphTgtList':
return False
sTR = svc_TargetProfile.getInstance()
self.callingWindow = callingWindow
self.profiles = sTR.getTargetProfileList()
self.profiles.sort(key=lambda p: (p.name in ['None'], p.name))
return len(self.profiles) > 0
# We always show "Ideal Profile" anyway
return True
def getText(self, callingWindow, itmContext):
return 'Add Target Profile'
def handleProfileAdd(self, event):
profile = self.profileIds.get(event.Id, False)
profile = self.profileEventMap.get(event.Id, False)
if profile is False:
event.Skip()
return
self.callingWindow.addProfile(profile)
def addProfile(self, rootMenu, profile):
def _addProfile(self, parentMenu, profile, name):
id = ContextMenuUnconditional.nextID()
name = getattr(profile, '_name', profile.name)
self.profileEventMap[id] = profile
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleProfileAdd, menuItem)
return menuItem
self.profileIds[id] = profile
item = wx.MenuItem(rootMenu, id, name)
rootMenu.Bind(wx.EVT_MENU, self.handleProfileAdd, item)
return item
def _addCategory(self, parentMenu, name):
id = ContextMenuUnconditional.nextID()
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleProfileAdd, menuItem)
return menuItem
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
self.profileIds = {}
self.subMenus = OrderedDict()
self.singles = []
self.callingWindow = callingWindow
sTR = svc_TargetProfile.getInstance()
profiles = list(chain(sTR.getBuiltinTargetProfileList(), sTR.getUserTargetProfileList()))
profiles.sort(key=lambda p: smartSort(p.fullName))
sub = wx.Menu()
for profile in chain([TargetProfile.getIdeal()], self.profiles):
start, end = profile.name.find('['), profile.name.find(']')
if start is not -1 and end is not -1:
currBase = profile.name[start + 1:end]
# set helper attr
setattr(profile, '_name', profile.name[end + 1:].strip())
if currBase not in self.subMenus:
self.subMenus[currBase] = []
self.subMenus[currBase].append(profile)
else:
self.singles.append(profile)
self.profileEventMap = {}
items = (OrderedDict(), OrderedDict())
for profile in profiles:
container = items
for categoryName in profile.hierarchy:
container = container[1].setdefault(categoryName, (OrderedDict(), OrderedDict()))
container[0][profile.shortName] = profile
# Single items, no parent
msw = 'wxMSW' in wx.PlatformInfo
for profile in self.singles:
sub.Append(self.addProfile(rootMenu if msw else sub, profile))
# Category as menu item - expands further
msw = "wxMSW" in wx.PlatformInfo
# Items that have a parent
for menuName, profiles in list(self.subMenus.items()):
# Create parent item for root menu that is simply name of parent
item = wx.MenuItem(rootMenu, ContextMenuUnconditional.nextID(), menuName)
def makeMenu(container, parentMenu, first=False):
menu = wx.Menu()
if first:
idealProfile = TargetProfile.getIdeal()
mitem = self._addProfile(rootMenu if msw else parentMenu, idealProfile, idealProfile.fullName)
menu.Append(mitem)
for name, pattern in container[0].items():
menuItem = self._addProfile(rootMenu if msw else parentMenu, pattern, name)
menu.Append(menuItem)
for name, subcontainer in container[1].items():
menuItem = self._addCategory(rootMenu if msw else parentMenu, name)
subMenu = makeMenu(subcontainer, menu)
menuItem.SetSubMenu(subMenu)
menu.Append(menuItem)
menu.Bind(wx.EVT_MENU, self.handleProfileAdd)
return menu
# Create menu for child items
grandSub = wx.Menu()
# Apply child menu to parent item
item.SetSubMenu(grandSub)
# Append child items to child menu
for profile in profiles:
grandSub.Append(self.addProfile(rootMenu if msw else grandSub, profile))
sub.Append(item) # finally, append parent item to root menu
return sub
subMenu = makeMenu(items, rootMenu, first=True)
return subMenu
TargetProfileAdder.register()

View File

@@ -1,5 +1,4 @@
import gui.mainFrame
from eos.saveddata.targetProfile import TargetProfile
from graphs.wrapper import TargetWrapper
from gui.contextMenu import ContextMenuSingle
from gui.targetProfileEditor import TargetProfileEditor
@@ -17,7 +16,7 @@ class TargetProfileEditorMenu(ContextMenuSingle):
return False
if not mainItem.isProfile:
return False
if mainItem.item is TargetProfile.getIdeal():
if mainItem.item.builtin:
return False
return True

View File

@@ -1,4 +1,5 @@
from collections import OrderedDict
from itertools import chain
# noinspection PyPackageRequirements
import wx
@@ -6,6 +7,7 @@ import wx
import gui.globalEvents as GE
import gui.mainFrame
from gui.contextMenu import ContextMenuUnconditional
from gui.utils.sorter import smartSort
from service.fit import Fit
from service.targetProfile import TargetProfile as svc_TargetProfile
@@ -16,21 +18,19 @@ class TargetProfileSwitcher(ContextMenuUnconditional):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext):
if self.mainFrame.getActiveFit() is None or srcContext != 'firepowerViewFull':
if srcContext != 'firepowerViewFull':
return False
sTR = svc_TargetProfile.getInstance()
self.profiles = sTR.getTargetProfileList()
self.profiles.sort(key=lambda p: (p.name in ['None'], p.name))
return len(self.profiles) > 0
if self.mainFrame.getActiveFit() is None:
return False
# We always show "No Profile" anyway
return True
def getText(self, callingWindow, itmContext):
# We take into consideration just target resists, so call menu item accordingly
return 'Target Resists'
def handleResistSwitch(self, event):
profile = self.profileIds.get(event.Id, False)
profile = self.profileEventMap.get(event.Id, False)
if profile is False:
event.Skip()
return
@@ -40,77 +40,62 @@ class TargetProfileSwitcher(ContextMenuUnconditional):
sFit.setTargetProfile(fitID, profile)
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(fitID,)))
def addProfile(self, rootMenu, profile):
def _addProfile(self, parentMenu, profile, name):
id = ContextMenuUnconditional.nextID()
name = getattr(profile, '_name', profile.name) if profile is not None else 'No Profile'
self.profileIds[id] = profile
item = wx.MenuItem(rootMenu, id, name, kind=wx.ITEM_CHECK)
rootMenu.Bind(wx.EVT_MENU, self.handleResistSwitch, item)
self.profileEventMap[id] = profile
menuItem = wx.MenuItem(parentMenu, id, name, kind=wx.ITEM_CHECK)
parentMenu.Bind(wx.EVT_MENU, self.handleResistSwitch, menuItem)
# determine active profile
sFit = Fit.getInstance()
fitID = self.mainFrame.getActiveFit()
f = sFit.getFit(fitID)
tr = f.targetProfile
checked = sFit.getFit(fitID).targetProfile is profile
return menuItem, checked
checked = tr == profile
return item, checked
def _addCategory(self, parentMenu, name):
id = ContextMenuUnconditional.nextID()
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleResistSwitch, menuItem)
return menuItem
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
self.profileIds = {}
self.subMenus = OrderedDict()
self.singles = []
sTR = svc_TargetProfile.getInstance()
profiles = list(chain(sTR.getBuiltinTargetProfileList(), sTR.getUserTargetProfileList()))
profiles.sort(key=lambda p: smartSort(p.fullName))
sub = wx.Menu()
for profile in self.profiles:
start, end = profile.name.find('['), profile.name.find(']')
if start is not -1 and end is not -1:
currBase = profile.name[start + 1:end]
name = profile.name[end + 1:].strip()
if not name:
self.singles.append(profile)
continue
# set helper attr
setattr(profile, '_name', name)
if currBase not in self.subMenus:
self.subMenus[currBase] = []
self.subMenus[currBase].append(profile)
else:
self.singles.append(profile)
# Add reset
msw = 'wxMSW' in wx.PlatformInfo
mitem, checked = self.addProfile(rootMenu if msw else sub, None)
sub.Append(mitem)
mitem.Check(checked)
sub.AppendSeparator()
self.profileEventMap = {}
items = (OrderedDict(), OrderedDict())
for profile in profiles:
container = items
for categoryName in profile.hierarchy:
container = container[1].setdefault(categoryName, (OrderedDict(), OrderedDict()))
container[0][profile.shortName] = profile
# Single items, no parent
for profile in self.singles:
mitem, checked = self.addProfile(rootMenu if msw else sub, profile)
sub.Append(mitem)
mitem.Check(checked)
# Category as menu item - expands further
msw = "wxMSW" in wx.PlatformInfo
# Items that have a parent
for menuName, profiles in list(self.subMenus.items()):
# Create parent item for root menu that is simply name of parent
item = wx.MenuItem(rootMenu, ContextMenuUnconditional.nextID(), menuName)
# Create menu for child items
grandSub = wx.Menu()
# Apply child menu to parent item
item.SetSubMenu(grandSub)
# Append child items to child menu
for profile in profiles:
mitem, checked = self.addProfile(rootMenu if msw else grandSub, profile)
grandSub.Append(mitem)
def makeMenu(container, parentMenu, first=False):
menu = wx.Menu()
if first:
mitem, checked = self._addProfile(rootMenu if msw else parentMenu, None, 'No Profile')
menu.Append(mitem)
mitem.Check(checked)
sub.Append(item) # finally, append parent item to root menu
if len(container[0]) > 0 or len(container[1]) > 0:
menu.AppendSeparator()
for name, pattern in container[0].items():
menuItem, checked = self._addProfile(rootMenu if msw else parentMenu, pattern, name)
menu.Append(menuItem)
menuItem.Check(checked)
for name, subcontainer in container[1].items():
menuItem = self._addCategory(rootMenu if msw else parentMenu, name)
subMenu = makeMenu(subcontainer, menu)
menuItem.SetSubMenu(subMenu)
menu.Append(menuItem)
menu.Bind(wx.EVT_MENU, self.handleResistSwitch)
return menu
return sub
subMenu = makeMenu(items, rootMenu, first=True)
return subMenu
TargetProfileSwitcher.register()

View File

@@ -10,12 +10,13 @@ _ValueChanged, EVT_VALUE_CHANGED = wx.lib.newevent.NewEvent()
class AttributeSliderChangeEvent:
def __init__(self, obj, old_value, new_value, old_percentage, new_percentage):
def __init__(self, obj, old_value, new_value, old_percentage, new_percentage, affect_modified_flag=True):
self.__obj = obj
self.__old = old_value
self.__new = new_value
self.__old_percent = old_percentage
self.__new_percent = new_percentage
self.__affect_modified_flag = affect_modified_flag
def GetObj(self):
return self.__obj
@@ -32,6 +33,10 @@ class AttributeSliderChangeEvent:
def GetPercentage(self):
return self.__new_percent
@property
def AffectsModifiedFlag(self):
return self.__affect_modified_flag
Object = property(GetObj)
OldValue = property(GetOldValue)
Value = property(GetValue)
@@ -40,9 +45,9 @@ class AttributeSliderChangeEvent:
class ValueChanged(_ValueChanged, AttributeSliderChangeEvent):
def __init__(self, obj, old_value, new_value, old_percentage, new_percentage):
def __init__(self, obj, old_value, new_value, old_percentage, new_percentage, affect_modified_flag=True):
_ValueChanged.__init__(self)
AttributeSliderChangeEvent.__init__(self, obj, old_value, new_value, old_percentage, new_percentage)
AttributeSliderChangeEvent.__init__(self, obj, old_value, new_value, old_percentage, new_percentage, affect_modified_flag=affect_modified_flag)
class AttributeSlider(wx.Panel):
@@ -118,7 +123,7 @@ class AttributeSlider(wx.Panel):
self.SetValue(self.GetValue())
evt.Skip()
def SetValue(self, value, post_event=True):
def SetValue(self, value, post_event=True, affect_modified_flag=True):
self.ctrl.SetValue(value)
invert_factor = -1 if self.inverse else 1
if value >= self.base_value:
@@ -127,7 +132,7 @@ class AttributeSlider(wx.Panel):
slider_percentage = (value - self.base_value) / (self.base_value - self.UserMinValue) * 100 * invert_factor
self.slider.SetValue(slider_percentage)
if post_event:
wx.PostEvent(self, ValueChanged(self, None, value, None, slider_percentage))
wx.PostEvent(self, ValueChanged(self, None, value, None, slider_percentage, affect_modified_flag=affect_modified_flag))
def OnMouseWheel(self, evt):
if evt.GetWheelRotation() > 0 and evt.GetWheelAxis() == wx.MOUSE_WHEEL_VERTICAL:

View File

@@ -76,6 +76,7 @@ class ItemMutatorList(wx.ScrolledWindow):
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
self.mod = mod
self.timer = None
self.isModified = False
goodColor = wx.Colour(96, 191, 0)
badColor = wx.Colour(255, 64, 0)
@@ -171,6 +172,8 @@ class ItemMutatorList(wx.ScrolledWindow):
self.SetSizer(sizer)
def changeMutatedValue(self, evt):
if evt.AffectsModifiedFlag:
self.isModified = True
m = self.event_mapping[evt.Object]
value = evt.Value
value = m.attribute.unit.ComplicateValue(value)
@@ -188,29 +191,32 @@ class ItemMutatorList(wx.ScrolledWindow):
self.timer = wx.CallLater(1000, self.callLater)
def resetMutatedValues(self, evt):
self.isModified = True
sFit = Fit.getInstance()
for slider, m in self.event_mapping.items():
value = sFit.changeMutatedValuePrelim(m, m.baseValue)
value = m.attribute.unit.SimplifyValue(value)
slider.SetValue(value)
slider.SetValue(value, affect_modified_flag=False)
evt.Skip()
def randomMutatedValues(self, evt):
self.isModified = True
sFit = Fit.getInstance()
for slider, m in self.event_mapping.items():
value = random.uniform(m.minValue, m.maxValue)
value = sFit.changeMutatedValuePrelim(m, value)
value = m.attribute.unit.SimplifyValue(value)
slider.SetValue(value)
slider.SetValue(value, affect_modified_flag=False)
evt.Skip()
def revertChanges(self, evt):
self.isModified = False
sFit = Fit.getInstance()
for slider, m in self.event_mapping.items():
if m.attrID in self.initialMutations:
value = sFit.changeMutatedValuePrelim(m, self.initialMutations[m.attrID])
value = m.attribute.unit.SimplifyValue(value)
slider.SetValue(value)
slider.SetValue(value, affect_modified_flag=False)
evt.Skip()
def OnWindowClose(self):
@@ -218,15 +224,18 @@ class ItemMutatorList(wx.ScrolledWindow):
sFit = Fit.getInstance()
fit = sFit.getFit(self.carryingFitID)
if self.mod in fit.modules:
currentMutation = {}
for slider, m in self.event_mapping.items():
# Sliders may have more up-to-date info than mutator in case we changed
# value in slider and without confirming it, decided to close window
value = slider.GetValue()
value = m.attribute.unit.ComplicateValue(value)
if value != m.value:
value = sFit.changeMutatedValuePrelim(m, value)
currentMutation[m.attrID] = value
if self.isModified:
currentMutation = {}
for slider, m in self.event_mapping.items():
# Sliders may have more up-to-date info than mutator in case we changed
# value in slider and without confirming it, decided to close window
value = slider.GetValue()
value = m.attribute.unit.ComplicateValue(value)
if value != m.value:
value = sFit.changeMutatedValuePrelim(m, value)
currentMutation[m.attrID] = value
else:
currentMutation = self.initialMutations
mainFrame = gui.mainFrame.MainFrame.getInstance()
mainFrame.getCommandForFit(self.carryingFitID).Submit(cmd.GuiChangeLocalModuleMutationCommand(
fitID=self.carryingFitID,

View File

@@ -21,9 +21,12 @@ class PFContextMenuPref(PreferenceView):
self.stTitle.SetFont(wx.Font(12, 70, 90, 90, False, wx.EmptyString))
mainSizer.Add(self.stTitle, 0, wx.EXPAND | wx.ALL, 5)
self.stSubTitle = wx.StaticText(panel, wx.ID_ANY,
"Disabling context menus can improve responsiveness.",
wx.DefaultPosition, wx.DefaultSize, 0)
self.stSubTitle = wx.StaticText(
panel, wx.ID_ANY,
'Disabling context menus can improve responsiveness.\n'
'You can hold {} key + right-click to show all menu items regardless of these settings.'.format(
'Command' if 'wxMac' in wx.PlatformInfo else 'Control'),
wx.DefaultPosition, wx.DefaultSize, 0)
self.stSubTitle.Wrap(-1)
mainSizer.Add(self.stSubTitle, 0, wx.ALL, 5)

View File

@@ -1,7 +1,6 @@
import wx
import config
from eos.db.saveddata.loadDefaultDatabaseValues import DefaultDatabaseValues
from eos.db.saveddata.queries import clearPrices, clearDamagePatterns, clearTargetProfiles
from gui.bitmap_loader import BitmapLoader
from gui.preferenceView import PreferenceView
@@ -75,10 +74,6 @@ class PFGeneralPref(PreferenceView):
btnSizer = wx.BoxSizer(wx.VERTICAL)
btnSizer.AddStretchSpacer()
self.btnImportDefaults = wx.Button(panel, wx.ID_ANY, "Reimport Database Defaults", wx.DefaultPosition, wx.DefaultSize, 0)
btnSizer.Add(self.btnImportDefaults, 0, wx.ALL, 5)
self.btnImportDefaults.Bind(wx.EVT_BUTTON, self.loadDatabaseDefaults)
self.btnDeleteDamagePatterns = wx.Button(panel, wx.ID_ANY, "Delete All Damage Pattern Profiles", wx.DefaultPosition, wx.DefaultSize, 0)
btnSizer.Add(self.btnDeleteDamagePatterns, 0, wx.ALL, 5)
self.btnDeleteDamagePatterns.Bind(wx.EVT_BUTTON, self.DeleteDamagePatterns)
@@ -97,14 +92,6 @@ class PFGeneralPref(PreferenceView):
panel.SetSizer(mainSizer)
panel.Layout()
def loadDatabaseDefaults(self, event):
# Import values that must exist otherwise Pyfa breaks
DefaultDatabaseValues.importRequiredDefaults()
# Import default values for damage profiles
DefaultDatabaseValues.importDamageProfileDefaults()
# Import default values for target resist profiles
DefaultDatabaseValues.importTargetProfileDefaults()
def DeleteDamagePatterns(self, event):
question = "This is a destructive action that will delete all damage pattern profiles.\nAre you sure you want to do this?"
if wxHelpers.YesNoDialog(question, "Confirm"):

View File

@@ -21,7 +21,7 @@
import wx
from gui.statsView import StatsView
from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount
from gui.utils.numberFormatter import formatAmount, roundToPrec
class CapacitorViewFull(StatsView):
@@ -133,14 +133,20 @@ class CapacitorViewFull(StatsView):
label.SetLabel('{}{}'.format(formatAmount(value, prec, lowest, highest, forceSign=forceSign), unit))
label.SetToolTip(wx.ToolTip("%.1f" % value))
if labelName == 'label%sCapacitorDelta':
label_tooltip = 'Capacitor delta:\n+{} GJ/s\n-{} GJ/s'.format(
formatAmount(cap_recharge, 3, 0, 3),
formatAmount(cap_use, 3, 0, 3))
label.SetToolTip(wx.ToolTip(label_tooltip))
if labelName == 'label%sCapacitorDelta' and (cap_recharge or cap_use):
lines = []
lines.append('Capacitor delta:')
lines.append(' +{} GJ/s'.format(formatAmount(cap_recharge, 3, 0, 3)))
lines.append(' -{} GJ/s'.format(formatAmount(cap_use, 3, 0, 3)))
delta = round(cap_recharge - cap_use, 3)
if delta > 0 and 0 < round(neut_res, 4) < 1:
lines.append('')
lines.append('Effective excessive gain:')
lines.append(' +{} GJ/s'.format(formatAmount(delta / neut_res, 3, 0, 3)))
label.SetToolTip(wx.ToolTip('\n'.join(lines)))
if labelName == 'label%sCapacitorResist':
texts = ['Neutralizer resistance']
if cap_amount > 0 and neut_res < 1:
if cap_amount > 0 and 0 < round(neut_res, 4) < 1:
texts.append('Effective capacity: {} GJ'.format(formatAmount(cap_amount / neut_res, 3, 0, 9)))
label.SetToolTip(wx.ToolTip('\n'.join(texts)))

View File

@@ -151,52 +151,71 @@ class FirepowerViewFull(StatsView):
else:
self.stEff.Hide()
def dpsToolTip(preSpool, fullSpool, prec, lowest, highest):
if roundToPrec(preSpool, prec) == roundToPrec(fullSpool, prec):
def hasSpoolUp(preSpool, fullSpool):
if preSpool is None or fullSpool is None:
return False
return roundToPrec(preSpool.total, prec) != roundToPrec(fullSpool.total, prec)
def dpsToolTip(normal, preSpool, fullSpool, prec, lowest, highest):
if normal is None or preSpool is None or fullSpool is None:
return ""
else:
return "Spool up: {}-{}".format(
formatAmount(preSpool, prec, lowest, highest),
formatAmount(fullSpool, prec, lowest, highest))
hasSpool = hasSpoolUp(preSpool, fullSpool)
lines = []
if hasSpool:
lines.append("Spool up: {}-{}".format(
formatAmount(preSpool.total, prec, lowest, highest),
formatAmount(fullSpool.total, prec, lowest, highest)))
if getattr(normal, 'total', None):
if hasSpool:
lines.append("")
lines.append("Current: {}".format(formatAmount(normal.total, prec, lowest, highest)))
for dmgType in normal.names():
val = getattr(normal, dmgType, None)
if val:
lines.append("{}{}: {}%".format(
" " if hasSpool else "",
dmgType.capitalize(),
formatAmount(val / normal.total * 100, 3, 0, 0)))
return "\n".join(lines)
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
stats = (
(
"labelFullDpsWeapon",
lambda: fit.getWeaponDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)).total,
lambda: fit.getWeaponDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)).total,
lambda: fit.getWeaponDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)).total,
lambda: fit.getWeaponDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)),
lambda: fit.getWeaponDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)),
lambda: fit.getWeaponDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)),
3, 0, 0, "{}{} DPS"),
(
"labelFullDpsDrone",
lambda: fit.getDroneDps().total,
lambda: fit.getDroneDps().total,
lambda: fit.getDroneDps().total,
lambda: fit.getDroneDps(),
lambda: fit.getDroneDps(),
lambda: fit.getDroneDps(),
3, 0, 0, "{}{} DPS"),
(
"labelFullVolleyTotal",
lambda: fit.getTotalVolley(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)).total,
lambda: fit.getTotalVolley(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)).total,
lambda: fit.getTotalVolley(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)).total,
lambda: fit.getTotalVolley(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)),
lambda: fit.getTotalVolley(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)),
lambda: fit.getTotalVolley(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)),
3, 0, 0, "{}{}"),
(
"labelFullDpsTotal",
lambda: fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)).total,
lambda: fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)).total,
lambda: fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)).total,
lambda: fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)),
lambda: fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)),
lambda: fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)),
3, 0, 0, "{}{}"))
counter = 0
for labelName, val, preSpoolVal, fullSpoolVal, prec, lowest, highest, valueFormat in stats:
label = getattr(self, labelName)
val = val() if fit is not None else 0
preSpoolVal = preSpoolVal() if fit is not None else 0
fullSpoolVal = fullSpoolVal() if fit is not None else 0
val = val() if fit is not None else None
preSpoolVal = preSpoolVal() if fit is not None else None
fullSpoolVal = fullSpoolVal() if fit is not None else None
if self._cachedValues[counter] != val:
tooltipText = dpsToolTip(preSpoolVal, fullSpoolVal, prec, lowest, highest)
tooltipText = dpsToolTip(val, preSpoolVal, fullSpoolVal, prec, lowest, highest)
label.SetLabel(valueFormat.format(
formatAmount(val, prec, lowest, highest),
"\u02e2" if tooltipText else ""))
formatAmount(0 if val is None else val.total, prec, lowest, highest),
"\u02e2" if hasSpoolUp(preSpoolVal, fullSpoolVal) else ""))
label.SetToolTip(wx.ToolTip(tooltipText))
self._cachedValues[counter] = val
counter += 1

View File

@@ -111,7 +111,7 @@ class BaseName(ViewColumn):
elif isinstance(stuff, Implant):
return stuff.item.name
elif isinstance(stuff, TargetProfile):
return stuff.name
return stuff.shortName
else:
item = getattr(stuff, "item", stuff)

View File

@@ -0,0 +1,60 @@
# =============================================================================
# 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/>.
# =============================================================================
# noinspection PyPackageRequirements
import wx
from eos.saveddata.fit import Fit
from graphs.wrapper import BaseWrapper
from gui.bitmap_loader import BitmapLoader
from eos.utils.float import floatUnerr
from gui.utils.numberFormatter import formatAmount
from gui.viewColumn import ViewColumn
class DampScanResColumn(ViewColumn):
name = 'Damp ScanRes'
def __init__(self, fittingView, params):
ViewColumn.__init__(self, fittingView)
self.imageId = fittingView.imageList.GetImageIndex(74, 'icons')
self.bitmap = BitmapLoader.getBitmap(74, 'icons')
self.mask = wx.LIST_MASK_IMAGE
def getText(self, stuff):
if isinstance(stuff, BaseWrapper):
stuff = stuff.item
mult = 1
if isinstance(stuff, Fit):
mult = floatUnerr(stuff.getDampMultScanRes())
if mult == 1:
text = ''
else:
text = '{}%'.format(formatAmount((mult - 1) * 100, 3, 0, 0, forceSign=True))
return text
def getImageId(self, stuff):
return -1
def getToolTip(self, stuff):
return 'Scan resolution dampening'
DampScanResColumn.register()

View File

@@ -24,7 +24,7 @@ from eos.saveddata.mode import Mode
from service.attribute import Attribute
from gui.viewColumn import ViewColumn
from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount
from gui.utils.numberFormatter import formatAmount, roundToPrec
class MaxRange(ViewColumn):
@@ -77,7 +77,21 @@ class MaxRange(ViewColumn):
return ("displayName", bool, False), ("showIcon", bool, True)
def getToolTip(self, mod):
return "Optimal + Falloff"
lines = []
missileRangeData = mod.missileMaxRangeData if hasattr(mod, "missileMaxRangeData") else None
if missileRangeData is not None:
lines.append('Missile flight range')
lowerRange, higherRange, higherChance = missileRangeData
if roundToPrec(higherChance, 3) not in (0, 1):
lines.append('{}% chance to fly {}m'.format(
formatAmount((1 - higherChance) * 100, prec=3, lowest=0, highest=0),
formatAmount(lowerRange, prec=3, lowest=0, highest=3)))
lines.append('{}% chance to fly {}m'.format(
formatAmount(higherChance * 100, prec=3, lowest=0, highest=0),
formatAmount(higherRange, prec=3, lowest=0, highest=3)))
else:
lines.append("Optimal + Falloff")
return '\n'.join(lines)
MaxRange.register()

View File

@@ -339,23 +339,25 @@ class Miscellanea(ViewColumn):
formatAmount(radar, 3, 0, 3),
)
return text, tooltip
elif itemGroup in ("Remote Sensor Booster", "Sensor Booster", "Signal Amplifier"):
elif itemGroup in ("Remote Sensor Booster", "Sensor Booster", "Signal Amplifier", "Structure Signal Amplifier"):
textLines = []
tooltipLines = []
scanResBonus = stuff.getModifiedItemAttr("scanResolutionBonus")
if scanResBonus:
textLines.append("{}%".format(formatAmount(scanResBonus, 3, 0, 3)))
tooltipLines.append("{}% scan resolution".format(formatAmount(scanResBonus, 3, 0, 3)))
lockRangeBonus = stuff.getModifiedItemAttr("maxTargetRangeBonus")
if lockRangeBonus:
textLines.append("{}%".format(formatAmount(lockRangeBonus, 3, 0, 3)))
tooltipLines.append("{}% lock range".format(formatAmount(lockRangeBonus, 3, 0, 3)))
gravBonus = stuff.getModifiedItemAttr("scanGravimetricStrengthPercent")
if scanResBonus is None or lockRangeBonus is None or gravBonus is None:
if gravBonus:
textLines.append("{}%".format(formatAmount(gravBonus, 3, 0, 3)))
tooltipLines.append("{}% sensor strength".format(formatAmount(gravBonus, 3, 0, 3)))
if not textLines:
return "", None
text = "{0}% | {1}% | {2}%".format(
formatAmount(scanResBonus, 3, 0, 3),
formatAmount(lockRangeBonus, 3, 0, 3),
formatAmount(gravBonus, 3, 0, 3),
)
tooltip = "Applied bonuses:\n{0}% scan resolution | {1}% lock range | {2}% sensor strength".format(
formatAmount(scanResBonus, 3, 0, 3),
formatAmount(lockRangeBonus, 3, 0, 3),
formatAmount(gravBonus, 3, 0, 3),
)
text = " | ".join(textLines)
tooltip = "Applied bonuses:\n{}".format(" | ".join(tooltipLines))
return text, tooltip
elif itemGroup in ("Projected ECCM", "ECCM", "Sensor Backup Array"):
grav = stuff.getModifiedItemAttr("scanGravimetricStrengthPercent")
@@ -588,7 +590,7 @@ class Miscellanea(ViewColumn):
):
if "Armor" in itemGroup or "Shield" in itemGroup:
boosted_attribute = "HP"
reload_time = item.getAttribute("reloadTime", 0) / 1000
reload_time = stuff.getModifiedItemAttr("reloadTime", 0) / 1000
elif "Capacitor" in itemGroup:
boosted_attribute = "Cap"
reload_time = 10

View File

@@ -178,10 +178,17 @@ class EntityEditor(wx.Panel):
return True
def checkEntitiesExist(self):
if len(self.choices) == 0:
self.Parent.Hide()
if self.OnNew(None) is False:
return False
self.Parent.Show()
if len(self.choices) > 0:
return True
else:
return self.enterNewEntity()
def enterNewEntity(self):
self.Parent.Hide()
if self.OnNew(None) is False:
return False
self.Parent.Show()
return True

View File

@@ -468,6 +468,8 @@ class FittingView(d.Display):
elif item.isModule:
if mstate.GetModifiers() == wx.MOD_ALT:
self.mainFrame.command.Submit(cmd.GuiFillWithNewLocalModulesCommand(fitID=fitID, itemID=itemID))
elif dstPos is not None:
self.mainFrame.command.Submit(cmd.GuiReplaceLocalModuleCommand(fitID=fitID, itemID=itemID, positions=[dstPos]))
else:
self.mainFrame.command.Submit(cmd.GuiAddLocalModuleCommand(fitID=fitID, itemID=itemID))
elif item.isSubsystem:

View File

@@ -428,6 +428,31 @@ class SkillTreeView(wx.Panel):
# This cuases issues with GTK, see #1866
# self.Layout()
# For level keyboard shortcuts
self.ChangeLevelEvent, CHANGE_LEVEL_EVENT = wx.lib.newevent.NewEvent()
self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent)
self.Bind(CHANGE_LEVEL_EVENT, self.changeLevel)
def kbEvent(self, event):
keyLevelMap = {
# Regular number keys
48: 0, 49: 1, 50: 2, 51: 3, 52: 4, 53: 5,
# Numpad keys
wx.WXK_NUMPAD0: 0, wx.WXK_NUMPAD1: 1, wx.WXK_NUMPAD2: 2,
wx.WXK_NUMPAD3: 3, wx.WXK_NUMPAD4: 4, wx.WXK_NUMPAD5: 5}
keycode = event.GetKeyCode()
if keycode in keyLevelMap and event.GetModifiers() == wx.MOD_NONE:
level = keyLevelMap[keycode]
selection = self.skillTreeListCtrl.GetSelection()
if selection:
dataType, skillID = self.skillTreeListCtrl.GetItemData(selection)
if dataType == 'skill':
event = self.ChangeLevelEvent()
event.SetId(self.idLevels[level])
wx.PostEvent(self, event)
return
event.Skip()
def importSkills(self, evt):
with wx.MessageDialog(
@@ -611,6 +636,8 @@ class SkillTreeView(wx.Panel):
sChar = Character.getInstance()
char = self.charEditor.entityEditor.getActiveEntity()
if char.name in ("All 0", "All 5"):
return
selection = self.skillTreeListCtrl.GetSelection()
dataType, skillID = self.skillTreeListCtrl.GetItemData(selection)

View File

@@ -23,6 +23,8 @@ from abc import ABCMeta, abstractmethod
import wx
from logbook import Logger
from service.settings import ContextMenuSettings
pyfalog = Logger(__name__)
@@ -32,6 +34,7 @@ class ContextMenu(metaclass=ABCMeta):
menus = []
_ids = [] # [wx.NewId() for x in xrange(200)] # init with decent amount
_idxid = -1
visibilitySetting = None
@classmethod
def register(cls):
@@ -72,6 +75,10 @@ class ContextMenu(metaclass=ABCMeta):
ContextMenu._idxid = -1
debug_start = len(ContextMenu._ids)
# Control being pressed forces all hidden menu items to be shown
visibilitySettingOverride = wx.GetMouseState().GetModifiers() == wx.MOD_CONTROL
cmSettings = ContextMenuSettings.getInstance()
rootMenu = wx.Menu()
rootMenu.info = {}
rootMenu.selection = (selection,) if not hasattr(selection, "__iter__") else selection
@@ -87,7 +94,11 @@ class ContextMenu(metaclass=ABCMeta):
for menuHandler in cls.menus:
# loop through registered menus
m = menuHandler()
if m._baseDisplay(callingWindow, srcContext, mainItem, selection):
if m.visibilitySetting:
visible = visibilitySettingOverride or cmSettings.get(m.visibilitySetting)
else:
visible = True
if visible and m._baseDisplay(callingWindow, srcContext, mainItem, selection):
display_amount += 1
texts = m._baseGetText(callingWindow, itemContext, mainItem, selection)
@@ -182,12 +193,12 @@ class ContextMenu(metaclass=ABCMeta):
return ContextMenu._ids[ContextMenu._idxid]
def isChecked(self, i):
'''If menu item is toggleable, this should return bool value'''
"""If menu item is toggleable, this should return bool value"""
return None
@property
def enabled(self):
'''If menu item is enabled. Allows an item to display, but not be selected'''
"""If menu item is enabled. Allows an item to display, but not be selected"""
return True
@abstractmethod

View File

@@ -40,6 +40,7 @@ class CopySelectDialog(wx.Dialog):
copyFormatEsi = 3
copyFormatMultiBuy = 4
copyFormatEfs = 5
copyFormatFitStats = 6
def __init__(self, parent):
super().__init__(parent, id=wx.ID_ANY, title="Select a format", size=(-1, -1), style=wx.DEFAULT_DIALOG_STYLE)
@@ -50,7 +51,8 @@ class CopySelectDialog(wx.Dialog):
CopySelectDialog.copyFormatDna : self.exportDna,
CopySelectDialog.copyFormatEsi : self.exportEsi,
CopySelectDialog.copyFormatMultiBuy: self.exportMultiBuy,
CopySelectDialog.copyFormatEfs : self.exportEfs
CopySelectDialog.copyFormatEfs : self.exportEfs,
CopySelectDialog.copyFormatFitStats: self.exportFitStats
}
self.mainFrame = parent
@@ -62,6 +64,7 @@ class CopySelectDialog(wx.Dialog):
("ESI", (CopySelectDialog.copyFormatEsi, None)),
("DNA", (CopySelectDialog.copyFormatDna, DNA_OPTIONS)),
("EFS", (CopySelectDialog.copyFormatEfs, None)),
("Stats", (CopySelectDialog.copyFormatFitStats, None)),
# ("XML", (CopySelectDialog.copyFormatXml, None)),
))
@@ -71,7 +74,7 @@ class CopySelectDialog(wx.Dialog):
continue
defaultFormatOptions[formatId] = {opt[0]: opt[3] for opt in formatOptions}
self.settings = SettingsProvider.getInstance().getSettings("pyfaExport", {"format": 0, "options": defaultFormatOptions})
self.settings = SettingsProvider.getInstance().getSettings("pyfaExport", {"format": self.copyFormatEft, "options": defaultFormatOptions})
# Options used to be stored as int (EFT export options only),
# overwrite them with new format when needed
if isinstance(self.settings["options"], int):
@@ -80,6 +83,7 @@ class CopySelectDialog(wx.Dialog):
self.options = {}
initialized = False
self.copyFormat = self.copyFormatEft
for formatName, formatData in self.copyFormats.items():
formatId, formatOptions = formatData
if not initialized:
@@ -117,7 +121,8 @@ class CopySelectDialog(wx.Dialog):
self.Center()
def Validate(self):
# Since this dialog is shown through aa ShowModal(), we hook into the Validate function to veto the closing of the dialog until we're ready.
# Since this dialog is shown through as ShowModal(),
# we hook into the Validate function to veto the closing of the dialog until we're ready.
# This always returns False, and when we're ready will EndModal()
selected = self.GetSelected()
options = self.GetOptions()
@@ -185,3 +190,10 @@ class CopySelectDialog(wx.Dialog):
def exportEfs(self, options, callback):
fit = getFit(self.mainFrame.getActiveFit())
EfsPort.exportEfs(fit, 0, callback)
# noinspection PyUnusedLocal
def exportFitStats(self, options, callback):
""" Puts fit stats in textual format into the clipboard """
fit = getFit(self.mainFrame.getActiveFit())
Port.exportFitStats(fit, callback)

View File

@@ -321,7 +321,10 @@ def activeStateLimit(itemIdentity):
item = Market.getInstance().getItem(itemIdentity)
if {
'moduleBonusAssaultDamageControl', 'moduleBonusIndustrialInvulnerability',
'microJumpDrive', 'microJumpPortalDrive'
'microJumpDrive', 'microJumpPortalDrive', 'emergencyHullEnergizer',
'cynosuralGeneration', 'jumpPortalGeneration', 'jumpPortalGenerationBO',
'cloneJumpAccepting', 'cloakingWarpSafe', 'cloakingPrototype', 'cloaking',
'massEntanglerEffect5', 'electronicAttributeModifyOnline', 'targetPassively'
}.intersection(item.effects):
return FittingModuleState.ONLINE
return FittingModuleState.ACTIVE

View File

@@ -204,6 +204,7 @@ class MainFrame(wx.Frame):
self.addPageId = wx.NewId()
self.closePageId = wx.NewId()
self.closeAllPagesId = wx.NewId()
self.hiddenGraphsId = wx.NewId()
self.widgetInspectMenuID = wx.NewId()
self.SetMenuBar(MainMenuBar(self))
@@ -423,6 +424,9 @@ class MainFrame(wx.Frame):
def OnShowGraphFrame(self, event):
GraphFrame.openOne(self)
def OnShowGraphFrameHidden(self, event):
GraphFrame.openOne(self, includeHidden=True)
def OnShowDevTools(self, event):
DevTools.openOne(parent=self)
@@ -551,6 +555,7 @@ class MainFrame(wx.Frame):
# Graphs
self.Bind(wx.EVT_MENU, self.OnShowGraphFrame, id=menuBar.graphFrameId)
self.Bind(wx.EVT_MENU, self.OnShowGraphFrameHidden, id=self.hiddenGraphsId)
toggleSearchBoxId = wx.NewId()
toggleShipMarketId = wx.NewId()
@@ -576,6 +581,9 @@ class MainFrame(wx.Frame):
(wx.ACCEL_CTRL, wx.WXK_F4, self.closePageId),
(wx.ACCEL_CMD, ord("W"), self.closePageId),
(wx.ACCEL_CTRL | wx.ACCEL_ALT, ord("G"), self.hiddenGraphsId),
(wx.ACCEL_CMD | wx.ACCEL_ALT, ord("G"), self.hiddenGraphsId),
(wx.ACCEL_CTRL | wx.ACCEL_ALT, ord("W"), self.closeAllPagesId),
(wx.ACCEL_CTRL | wx.ACCEL_ALT, wx.WXK_F4, self.closeAllPagesId),
(wx.ACCEL_CMD | wx.ACCEL_ALT, ord("W"), self.closeAllPagesId),

View File

@@ -48,7 +48,7 @@ class DmgPatternNameValidator(BaseValidator):
try:
if len(text) == 0:
raise ValueError("You must supply a name for your Damage Profile!")
elif text in [x.name for x in entityEditor.choices]:
elif text in [x.rawName for x in entityEditor.choices]:
raise ValueError("Damage Profile name already in use, please choose another.")
return True
@@ -66,8 +66,8 @@ class DmgPatternEntityEditor(EntityEditor):
def getEntitiesFromContext(self):
sDP = DamagePattern.getInstance()
choices = sorted(sDP.getDamagePatternList(), key=lambda p: p.name)
return [c for c in choices if c.name != "Selected Ammo"]
choices = sorted(sDP.getUserDamagePatternList(), key=lambda p: p.rawName)
return [c for c in choices if c.rawName != "Selected Ammo"]
def DoNew(self, name):
sDP = DamagePattern.getInstance()
@@ -237,7 +237,7 @@ class DmgPatternEditor(AuxiliaryFrame):
if p is None:
return
if p.name == "Uniform" or p.name == "Selected Ammo":
if p.rawName == "Uniform" or p.rawName == "Selected Ammo":
self.restrict()
else:
self.unrestrict()

View File

@@ -47,7 +47,7 @@ class ImplantTextValidor(BaseValidator):
if len(text) == 0:
raise ValueError("You must supply a name for the Implant Set!")
elif text in [x.name for x in entityEditor.choices]:
raise ValueError("Imlplant Set name already in use, please choose another.")
raise ValueError("Implant Set name already in use, please choose another.")
return True
except ValueError as e:
@@ -106,7 +106,7 @@ class ImplantSetEditorView(BaseImplantEditorView):
sIS = ImplantSets.getInstance()
set_ = self.Parent.entityEditor.getActiveEntity()
sIS.addImplant(set_.ID, item.ID)
sIS.addImplants(set_.ID, item.ID)
def removeImplantFromContext(self, implant):
sIS = ImplantSets.getInstance()
@@ -117,7 +117,7 @@ class ImplantSetEditorView(BaseImplantEditorView):
class ImplantSetEditor(AuxiliaryFrame):
def __init__(self, parent):
def __init__(self, parent, dataToAdd=None):
super().__init__(
parent, id=wx.ID_ANY, title="Implant Set Editor", resizeable=True,
size=wx.Size(950, 500) if "wxGTK" in wx.PlatformInfo else wx.Size(850, 420))
@@ -166,7 +166,13 @@ class ImplantSetEditor(AuxiliaryFrame):
self.SetSizer(mainSizer)
self.Layout()
if not self.entityEditor.checkEntitiesExist():
if dataToAdd:
name, implants = dataToAdd
newSet = self.entityEditor.DoNew(name)
ImplantSets.getInstance().addImplants(newSet.ID, *[i.item.ID for i in implants])
self.entityEditor.refreshEntityList(newSet)
wx.PostEvent(self.entityEditor.entityChoices, wx.CommandEvent(wx.wxEVT_COMMAND_CHOICE_SELECTED))
elif not self.entityEditor.checkEntitiesExist():
self.Close()
return

View File

@@ -189,7 +189,7 @@ class ShipBrowser(wx.Panel):
RACE_ORDER = [
"amarr", "caldari", "gallente", "minmatar",
"sisters", "ore",
"sisters", "ore", "concord",
"serpentis", "angel", "blood", "sansha", "guristas", "mordu",
"jove", "upwell", "triglavian", None
]

View File

@@ -68,7 +68,7 @@ class TargetProfileNameValidator(BaseValidator):
try:
if len(text) == 0:
raise ValueError("You must supply a name for your Target Profile!")
elif text in [x.name for x in entityEditor.choices]:
elif text in [x.rawName for x in entityEditor.choices]:
raise ValueError("Target Profile name already in use, please choose another.")
return True
@@ -88,7 +88,7 @@ class TargetProfileEntityEditor(EntityEditor):
def getEntitiesFromContext(self):
sTR = TargetProfile.getInstance()
choices = sorted(sTR.getTargetProfileList(), key=lambda p: p.name)
choices = sorted(sTR.getUserTargetProfileList(), key=lambda p: p.rawName)
return choices
def DoNew(self, name):

13
gui/utils/sorter.py Normal file
View File

@@ -0,0 +1,13 @@
"""
Taken from https://stackoverflow.com/questions/2669059/how-to-sort-alpha-numeric-set-in-python
"""
import re
def _convert(text):
return int(text) if text.isdigit() else text
def smartSort(key):
return [_convert(c) for c in re.split('([0-9]+)', key)]

View File

@@ -77,6 +77,7 @@ from gui.builtinViewColumns import ( # noqa: E402, F401
baseIcon,
baseName,
capacitorUse,
dampScanRes,
graphColor,
graphLightness,
graphLineStyle,

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 B

After

Width:  |  Height:  |  Size: 939 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 636 B

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

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