Compare commits

...

564 Commits

Author SHA1 Message Date
DarkPhoenix
5df2db5879 Bump version 2019-09-02 01:21:32 +03:00
DarkPhoenix
5a34db0d2f Change how fit deletion confirmation dialog is destroyed 2019-09-02 01:14:35 +03:00
DarkPhoenix
6f50be1e7e Fix another set of crashes with manual login 2019-08-30 15:36:34 +03:00
DarkPhoenix
d15fefcf1b Avoid various crashes when working with SslLoginServer dialog 2019-08-30 15:26:58 +03:00
DarkPhoenix
c2a240bab0 Fix mutadaptive rep group 2019-08-26 23:12:44 +03:00
DarkPhoenix
40c3bf723f Make drone RR rigs stacking penalized for shield RR bots 2019-08-26 19:43:44 +03:00
DarkPhoenix
7a92ace2db Remove stacking penalty from scorpion ECM strength bonus 2019-08-26 19:33:55 +03:00
DarkPhoenix
500f5b8310 Add redraw delay on fit changes 2019-08-26 14:47:34 +03:00
DarkPhoenix
44830a4de6 Add capacity to container name if it was added to cargo 2019-08-26 14:13:26 +03:00
DarkPhoenix
f3f13e7ba8 Make cargo containers searchable 2019-08-26 13:38:03 +03:00
DarkPhoenix
0269a64ae1 Add maximize button to resizeable windows and make character editor resizeable 2019-08-26 12:59:00 +03:00
DarkPhoenix
5d6cdcbd23 Fix indent 2019-08-26 12:28:55 +03:00
DarkPhoenix
81906a7bd2 Do not store item-specific resistance attrs on effects 2019-08-26 12:27:43 +03:00
DarkPhoenix
b25b038934 Fix on-effect resistance definition 2019-08-26 09:28:48 +03:00
DarkPhoenix
b469fa520e Do not crash on cap boosters only in fit 2019-08-26 09:02:55 +03:00
DarkPhoenix
4f865896c7 Force variations menu to respect meta group overrides 2019-08-26 04:01:23 +03:00
DarkPhoenix
3b50dddef2 Allow to online extra amount of command bursts on various caps 2019-08-26 03:48:17 +03:00
DarkPhoenix
380e9c2e87 Change how rounding on Y ticks happens - now it relies on shown Y range 2019-08-26 03:35:35 +03:00
DarkPhoenix
1c1443c862 Move calculation of normalization shift to separate function 2019-08-26 02:56:11 +03:00
DarkPhoenix
7dab220009 Ignore non-active scrams and scrammables 2019-08-23 13:44:15 +03:00
DarkPhoenix
eebd59413b Apply scrams in DPS graph when projected mods is enabled 2019-08-23 13:19:17 +03:00
DarkPhoenix
f4a635eb43 Implement hybrid extended attribute getter and few scram-related functions for DPS graph 2019-08-23 11:53:13 +03:00
DarkPhoenix
0e57258cc5 Add ability to pass multiple afflictors to no-afflictor getter 2019-08-23 09:13:40 +03:00
DarkPhoenix
67462c3278 Do not crash on attempt to export blank fitting 2019-08-23 09:07:31 +03:00
DarkPhoenix
fce8129fa2 Add support for extended target profile stats export/import 2019-08-23 08:55:13 +03:00
DarkPhoenix
707dbeecf8 Fix subsystems giving fitting bonuses to non-med RRs 2019-08-22 21:30:10 +03:00
DarkPhoenix
13a0bf9d42 Do not delay any damage besides doomsday mods 2019-08-22 19:34:38 +03:00
DarkPhoenix
3d3bf4ce2c Implement fighter ability resistance support into dps graph 2019-08-22 12:38:39 +03:00
DarkPhoenix
46ae2a006e Consider default attr value when fetching original value, too 2019-08-22 11:58:31 +03:00
DarkPhoenix
0062206f87 Bump version once again 2019-08-22 11:36:58 +03:00
DarkPhoenix
b6642aa76c Change default price source to evemarketer 2019-08-22 11:34:25 +03:00
DarkPhoenix
522f1c8314 Change cap column tooltip 2019-08-22 11:32:00 +03:00
DarkPhoenix
6fceda5f27 Ensure right-clicked item is selected on Mac in Display list panels 2019-08-22 08:47:45 +03:00
DarkPhoenix
668a947543 Use copy-paste fix only on Mac 2019-08-22 08:05:36 +03:00
DarkPhoenix
f3eadc9ef1 Do nothing on copy-paste in secondary windows 2019-08-22 07:54:21 +03:00
DarkPhoenix
7693483720 Ensure that clicked item is selected on Mac in fitting panel 2019-08-22 07:37:49 +03:00
DarkPhoenix
219e1c11dc Attempt to remove mac's copy-paste workaround 2019-08-22 07:32:15 +03:00
DarkPhoenix
99c6614d86 Change more commands to not fail on partial fails 2019-08-21 18:27:37 +03:00
DarkPhoenix
93af60c3d0 Do not fail batch imports if one of elements fails to be imported 2019-08-21 18:16:27 +03:00
DarkPhoenix
b4789bbebf Conver items to typeIDs before submitting 2019-08-21 18:04:22 +03:00
DarkPhoenix
160c2f0942 Implement additions pane pasting via ctrl-v 2019-08-21 17:39:06 +03:00
DarkPhoenix
9605efe643 Support actual item pasting functionality 2019-08-21 16:47:48 +03:00
DarkPhoenix
56e09b8528 Add context menu which pastes clipboard into additions pane (not functional yet) 2019-08-21 15:46:20 +03:00
DarkPhoenix
7dc17543df Make new context menus disablable 2019-08-21 15:14:49 +03:00
DarkPhoenix
3793721dc3 Implement copy context menu for additions pane 2019-08-21 15:07:25 +03:00
DarkPhoenix
54aa284fd9 Add extra context menu contexts to some additions panels 2019-08-21 13:39:40 +03:00
DarkPhoenix
207818537b When setting fighter's amount attribute to max or more than max, set it to -1 internally
It will show up as max amount when reading
2019-08-21 13:22:04 +03:00
DarkPhoenix
d32ff668e1 Show capacitor regen difference for selected mods 2019-08-21 12:11:22 +03:00
DarkPhoenix
1b2cd62629 Round results a little to get rid of float error 2019-08-21 11:38:14 +03:00
DarkPhoenix
2d96af9fc8 Use regular getter when no modifications needed 2019-08-21 11:36:24 +03:00
DarkPhoenix
20422d3046 Actually calculate attr without passed afflictor 2019-08-21 09:58:31 +03:00
DarkPhoenix
0e2ae0e0f0 Extend afflictor info with extra data 2019-08-21 09:37:06 +03:00
DarkPhoenix
9572a51f28 Do not crash on None value in damage pattern editor 2019-08-20 21:48:06 +03:00
DarkPhoenix
cf6f884b3b Do not crash cap sim graph when there're no drains 2019-08-20 21:43:01 +03:00
DarkPhoenix
4ca737281d Fix background color of auxiliary windows on Windows 2019-08-20 18:29:29 +03:00
DarkPhoenix
847f4e343e Update database to 1553210 2019-08-20 19:45:33 +03:00
DarkPhoenix
e0db6eb2ad Fix json to SQL script 2019-08-20 19:33:19 +03:00
DarkPhoenix
c7f625456e Remove icons-related scripts and data as phobos can dump it successfully 2019-08-20 18:14:55 +03:00
DarkPhoenix
332eb048a1 Update json to SQL script to adapt it to latest changes to Phobos 2019-08-20 18:14:24 +03:00
DarkPhoenix
e2c7f169fd Do not bump minor version 2019-08-20 17:44:59 +03:00
DarkPhoenix
4eb68c7c13 Bump version 2019-08-20 11:56:24 +03:00
DarkPhoenix
c1228b95fe Do not sort skill bonuses in traits tab 2019-08-20 09:04:50 +03:00
DarkPhoenix
2ab61f2b9e Disallow having modules onlined if the are too many fit for maxGroupOnline limitation 2019-08-20 08:47:04 +03:00
DarkPhoenix
d266aa796e Do not crash on damage/target profiles named like [this] 2019-08-20 00:47:49 +03:00
DarkPhoenix
e488497a42 Cap sim now avoids optimization when running for cap graph request 2019-08-20 00:38:29 +03:00
DarkPhoenix
e8766817f8 Do not stop simulating cap too early in case we have cap injector in fit 2019-08-19 21:36:34 +03:00
DarkPhoenix
d577b1d1c6 Implement individual point getter for cap sim graph 2019-08-19 21:06:28 +03:00
DarkPhoenix
7984b57494 Implement range getter for cap sim 2019-08-19 20:44:32 +03:00
DarkPhoenix
567e8df174 Save cap sim state on fit 2019-08-19 15:34:19 +03:00
DarkPhoenix
65e7607221 Add checkbox which controls if data from capsim is used or not 2019-08-19 13:28:50 +03:00
DarkPhoenix
6e3b536d83 Add starting shield amount to shield graph as well 2019-08-19 13:20:02 +03:00
DarkPhoenix
348c4d71df Add "starting cap" parameter to cap graph 2019-08-19 13:10:28 +03:00
DarkPhoenix
9494885f45 Do not crash on infs/nans in values, just discard plot 2019-08-19 12:48:26 +03:00
DarkPhoenix
01bda70fef Rework mainOnly parameter into conditions 2019-08-19 12:37:52 +03:00
DarkPhoenix
8b2f9ce59d Reduce RAAR repairs on cycles after 8th if we decide to not reload 2019-08-19 09:45:20 +03:00
DarkPhoenix
60a8e905b8 Expose data about reloads to cycle getter 2019-08-19 09:32:55 +03:00
DarkPhoenix
d9a4b0a359 Add logic to control reloads of ancillary mods according to parameter setting 2019-08-19 09:17:51 +03:00
DarkPhoenix
2d43a6ade5 Pull ancillary reload parameter to internal cache getter 2019-08-19 09:01:20 +03:00
DarkPhoenix
d0a56e3ee8 Add checkbox to UI which will control if RAAR/RASB will reload or not 2019-08-19 01:28:12 +03:00
DarkPhoenix
9b15f1942d Do not allow to activate most modules besides guns on targets which are at distance more than optimal + 3x falloff 2019-08-18 22:13:43 +03:00
DarkPhoenix
edd261c677 Add RR graphs 2019-08-18 21:27:20 +03:00
DarkPhoenix
3c967ba9eb Add 3 extra columns to show RR power of different fits 2019-08-17 00:49:42 +03:00
DarkPhoenix
8332ccaa7a Add RR graph stub 2019-08-17 00:29:13 +03:00
DarkPhoenix
26b1610ca5 Remote rep drones now always show amount of HP repaired, enabled or not 2019-08-17 00:03:23 +03:00
DarkPhoenix
ae1a5f4e44 Rework various RR-related functionality to use new API 2019-08-16 23:58:07 +03:00
DarkPhoenix
4594f57961 Get RR data getter to drone 2019-08-16 23:27:37 +03:00
DarkPhoenix
c1d0849f87 Consider AAR rep multiplier when getting RR params 2019-08-16 20:58:56 +03:00
DarkPhoenix
313264a49f Implement RR parameter getter 2019-08-16 20:55:25 +03:00
DarkPhoenix
a724347236 Avoid pushing graph inwards because of labels 2019-08-16 19:20:11 +03:00
DarkPhoenix
df7ad187f5 Relayout on effectivity change 2019-08-16 09:42:12 +03:00
DarkPhoenix
c142a011a0 Show shield HP/EHP based on HP selection in main panel 2019-08-16 09:31:02 +03:00
DarkPhoenix
477c43884a Add extra argument to axis selection updater 2019-08-16 08:51:58 +03:00
DarkPhoenix
f3551ce570 Move HP toggled event to global 2019-08-16 08:41:14 +03:00
DarkPhoenix
6baa9dd322 Move interpolation and limitation to separate functions 2019-08-16 08:31:58 +03:00
DarkPhoenix
302975c243 Round to 0 to avoid showing too small numbers in scientific representation 2019-08-16 00:55:29 +03:00
DarkPhoenix
73f75fb44e Do not plot labels if they are out of bounds 2019-08-15 20:42:59 +03:00
DarkPhoenix
7f651b144f Fix shield amount normalization for X marking 2019-08-15 20:40:13 +03:00
DarkPhoenix
ff98658491 Use relative position of X description 2019-08-15 20:02:51 +03:00
DarkPhoenix
53db5943b1 Do not allow to clip x marks 2019-08-15 18:35:24 +03:00
DarkPhoenix
33b7ab0d98 Limit misc parameters when fetching point 2019-08-15 18:16:14 +03:00
DarkPhoenix
f0af93f8b9 Add extra space on X to the right to have space for x mark labels 2019-08-15 17:59:35 +03:00
DarkPhoenix
379fd2353a Reset x marks on x field change 2019-08-15 17:44:36 +03:00
DarkPhoenix
98f0766425 Drop x marks on various actions 2019-08-15 17:40:29 +03:00
DarkPhoenix
be07a4735c Added vertical marker line to show exact values on graphs 2019-08-15 17:25:25 +03:00
DarkPhoenix
d736a10dc9 Draw vertical line where user clicked 2019-08-15 15:42:06 +03:00
DarkPhoenix
c5c3b9cba1 Change how we calculate min/max Ys 2019-08-15 15:25:21 +03:00
DarkPhoenix
bfa9ad4d96 Implement single point getters 2019-08-15 15:06:50 +03:00
DarkPhoenix
2f8ece9080 Connect various MPL events to support drawing vertical line with graph values 2019-08-15 12:42:23 +03:00
DarkPhoenix
c7e769e42e Move "graphs enabled" variable to top of graphs module 2019-08-15 11:49:26 +03:00
DarkPhoenix
3ab06d0832 Move MPL-related code to canvas panel 2019-08-15 09:32:52 +03:00
DarkPhoenix
251bc71f86 Add canvas panel which will house MPL-related functionality 2019-08-14 20:33:19 +03:00
DarkPhoenix
3f3870bb30 Rename just panel to control panel 2019-08-14 19:54:06 +03:00
DarkPhoenix
eb9612b9a3 Add neut vs range graph 2019-08-14 14:27:50 +03:00
DarkPhoenix
1d7efce197 Add ECM vs range graph 2019-08-14 13:10:53 +03:00
DarkPhoenix
ff60cf313e Add TP over range graph 2019-08-14 12:39:36 +03:00
DarkPhoenix
a275878ba0 Add GD graph 2019-08-14 09:46:12 +03:00
DarkPhoenix
09fb4c1d35 Add TD vs range graph 2019-08-14 09:17:40 +03:00
DarkPhoenix
483c1c35fd Allow to use custom labels for selectors 2019-08-14 08:53:26 +03:00
DarkPhoenix
810b8be92a Add damp strength vs range graph 2019-08-14 00:09:38 +03:00
DarkPhoenix
679ed7b806 Implement iterators on fit and use them in graph code 2019-08-13 22:39:25 +03:00
DarkPhoenix
e1896c0216 Set parent for all busyinfo windows to put them properly on application screen 2019-08-13 11:26:28 +03:00
DarkPhoenix
01310c166e Convert miscparams to dict in generic methods to avoid extra conversions in every getter implementation 2019-08-13 09:13:19 +03:00
DarkPhoenix
3bc93899fe Add web strength vs range graph 2019-08-13 08:28:59 +03:00
DarkPhoenix
69bd988174 Add lock time graph 2019-08-12 22:13:24 +03:00
DarkPhoenix
20bee1196a Save display number and position on it relatively client area 2019-08-12 20:56:03 +03:00
DarkPhoenix
9803da1825 Add some extra logging to help with #1605 debugging 2019-08-12 20:05:21 +03:00
DarkPhoenix
bc61e32ee7 Change values after input delay 2019-08-12 17:03:12 +03:00
DarkPhoenix
4f784e2eea Allow to use floats in damage pattern editor 2019-08-12 16:52:08 +03:00
DarkPhoenix
931d8d355f When no ESI characters are added, do not close ESI windows 2019-08-12 15:48:35 +03:00
DarkPhoenix
6912b6eb39 Fix background color of SSO char management panel 2019-08-12 12:32:02 +03:00
DarkPhoenix
163d2c9b10 Do not freeze display, as it doesn't remove flicker anyway 2019-08-12 12:29:24 +03:00
DarkPhoenix
cc8def1cf5 Try to flicker less when redrawing lists 2019-08-12 15:13:51 +03:00
DarkPhoenix
bcdefdc4ac Rework more windows to rely on auxiliary frame 2019-08-12 14:41:41 +03:00
DarkPhoenix
4f2e1be9ac Set min size of various auxiliary windows 2019-08-12 14:05:18 +03:00
DarkPhoenix
663623dec6 Do not crash on dollar sign in fit name 2019-08-12 11:56:07 +03:00
DarkPhoenix
cecf5d7e31 Make siege rapid torp bonus non-stacking penalized with damage mods 2019-08-12 11:25:07 +03:00
DarkPhoenix
8a4caeaa2d Avoid doing unnecessary recalcs 2019-08-12 04:08:54 +03:00
DarkPhoenix
f8062ba39f Do not do extra recalc if it's not needed 2019-08-12 02:13:29 +03:00
DarkPhoenix
3e6e3b0743 Migrate more windows to new window control scheme 2019-08-12 01:41:38 +03:00
DarkPhoenix
1e35eaf62a Rework how single windows are opened 2019-08-12 01:11:44 +03:00
DarkPhoenix
8a3dc2f3dc Rework how single frame of some auxiliary window is opened 2019-08-12 00:48:18 +03:00
DarkPhoenix
34a6fdc07e Rework more windows to use aux frame 2019-08-12 00:32:27 +03:00
DarkPhoenix
fc65cb6000 More windows to auxiliary frame 2019-08-11 15:52:51 +03:00
DarkPhoenix
247add778d Rework item stats to use new auxiliary frame class 2019-08-11 15:18:40 +03:00
DarkPhoenix
ab1071f1d7 Fix HAW phoenix dps effects 2019-08-11 13:05:03 +03:00
DarkPhoenix
f5cb5c3993 Open only one window of attribute and character editors 2019-08-10 11:23:01 +03:00
DarkPhoenix
ea7f122030 Rework dev tools window from frame into dialog 2019-08-10 11:02:25 +03:00
DarkPhoenix
2160cc4aaa Change how attribute editor frame is closed 2019-08-10 02:50:20 +03:00
DarkPhoenix
817e99a05d Change the way we close character editor 2019-08-10 02:42:19 +03:00
DarkPhoenix
53f5656478 Rework item stats from dialog into frame 2019-08-10 02:36:37 +03:00
DarkPhoenix
42d11bd3f1 Raise graph and target profile editor windows when we invoke them and they are already open 2019-08-10 02:10:28 +03:00
DarkPhoenix
a028ebe198 Rework how we handle all modal dialogs 2019-08-10 01:56:43 +03:00
DarkPhoenix
c315adf987 Rework target profile editor to be non-blocking window 2019-08-09 20:57:20 +03:00
DarkPhoenix
01371f227c Change the way we handle dialogs 2019-08-09 20:09:31 +03:00
DarkPhoenix
3174deed99 Change range factor for turret ammo sorting from optimal + falloff/2 to optimal + falloff 2019-08-09 16:27:03 +03:00
DarkPhoenix
6db178e4d2 Delay cap booster activation until the moment when its charge will be used efficiently 2019-08-09 16:19:20 +03:00
DarkPhoenix
eb07e03e93 Start all injectors at 0 2019-08-09 09:32:07 +03:00
DarkPhoenix
a881cd2bcc Work around wx bug - show labels even after switching graphs 2019-08-09 00:34:27 +03:00
DarkPhoenix
676b09720a Replace reduce with sum 2019-08-09 00:29:09 +03:00
DarkPhoenix
ac7b6d9ecd Change the way we detect injectors 2019-08-09 00:18:00 +03:00
DarkPhoenix
49cb81b516 Push info about injector into cap sim 2019-08-08 19:46:44 +03:00
DarkPhoenix
27e361dc5b Attempt to fix hang on preferences close on Mac 2019-08-08 12:25:50 +03:00
DarkPhoenix
4c330bfb16 Increase travis git depth, because otherwise it fails git describe when there were no tags last x commits 2019-08-08 12:16:42 +03:00
DarkPhoenix
fcb85c85a3 Rename modules to items 2019-08-08 09:50:50 +03:00
DarkPhoenix
9be1b96226 Add modules to recents when they are removed too 2019-08-08 09:45:14 +03:00
DarkPhoenix
11598f9a09 Do not add items to recents which cannot be fetched (e.g. item was removed since last pyfa start) 2019-08-08 08:57:29 +03:00
DarkPhoenix
39e23237a5 Move functionality of storing item to market service 2019-08-08 08:54:41 +03:00
DarkPhoenix
22507673aa Do not sort items in recently used 2019-08-08 08:42:06 +03:00
DarkPhoenix
7fdcd4aa15 Do proper attribute conversions on window close 2019-08-07 23:18:06 +03:00
DarkPhoenix
c559508175 Avoid unnecessary calls 2019-08-07 23:05:55 +03:00
DarkPhoenix
95621b6aab Use spinbox as info source rather than mutator 2019-08-07 23:02:15 +03:00
DarkPhoenix
fb93aa1ad5 Fix crash on closing item stats window when mutation's tab animation was in progress 2019-08-07 22:49:30 +03:00
DarkPhoenix
035c69c60a Implement mutated item copy context menu 2019-08-07 15:44:12 +03:00
DarkPhoenix
788bbb5d25 Make dark lines a bit lighter 2019-08-07 14:27:27 +03:00
DarkPhoenix
04178ca824 Add option to disable cargo export in EFT format 2019-08-07 12:57:20 +03:00
DarkPhoenix
647bdb78df Fix minflood AAR penalty 2019-08-07 12:50:07 +03:00
DarkPhoenix
ce9099a25b Remove workaround as it seems to be not needed anymore 2019-08-07 12:31:35 +03:00
DarkPhoenix
4e715750a5 Apply stronger desaturation and shift from normal ligtness onto bright/dark graph lines 2019-08-07 11:57:40 +03:00
DarkPhoenix
b330f72326 Allow to edit target profiles via context menu 2019-08-07 10:27:41 +03:00
DarkPhoenix
dd5c95d2f2 Return white line for dark backgrounds 2019-08-07 09:48:39 +03:00
DarkPhoenix
ae5e0cc71a Update bright icon again - make it more saturated 2019-08-07 09:42:47 +03:00
DarkPhoenix
cfe0e36e48 Make bright icon brighter 2019-08-07 09:41:03 +03:00
DarkPhoenix
63a362286e Add comment on why we need marker 2019-08-07 09:38:01 +03:00
DarkPhoenix
2155aa0d21 Move proportion width calcuation to display class 2019-08-07 09:24:26 +03:00
DarkPhoenix
f52fda3f03 Implement auto proportion scaling weights 2019-08-07 09:12:50 +03:00
DarkPhoenix
f315f8b85a Increase column width 2019-08-07 08:52:56 +03:00
DarkPhoenix
72e56246f4 Rework legend to show lines rather than patches 2019-08-07 08:46:55 +03:00
DarkPhoenix
5102cb35c8 Instantiate target wrappers with proper line style 2019-08-07 00:02:58 +03:00
DarkPhoenix
170853f0f4 Add line style support 2019-08-06 23:51:09 +03:00
DarkPhoenix
7bc0d88898 Add line style icons 2019-08-06 23:17:06 +03:00
DarkPhoenix
4834cfe8ca Generalize style picker popup code 2019-08-06 16:27:44 +03:00
DarkPhoenix
2c2065119b Generalize some click code 2019-08-06 16:18:57 +03:00
DarkPhoenix
636672fdce Plug plot line lightness into everything 2019-08-06 16:11:56 +03:00
DarkPhoenix
bc8c70fa9c Add lightness icons 2019-08-06 15:38:52 +03:00
DarkPhoenix
e3ac9a7722 Add line lightness column 2019-08-06 15:13:35 +03:00
DarkPhoenix
e14d3d7214 Use set colors for actual graph 2019-08-06 14:27:35 +03:00
DarkPhoenix
5b898a678b Handle changing colors via color picker 2019-08-06 14:15:13 +03:00
DarkPhoenix
e2d6baaeb1 Add color named to dict with data 2019-08-06 13:08:19 +03:00
DarkPhoenix
eb87ba1d89 Automatically assign colors 2019-08-06 13:01:40 +03:00
DarkPhoenix
0257e70c29 Code color column to properly show wrapper color 2019-08-06 12:50:11 +03:00
DarkPhoenix
70d1a3534b Rework dict with color data to use color enum 2019-08-06 12:44:59 +03:00
DarkPhoenix
16f4903eba Add various color icons 2019-08-06 12:39:10 +03:00
DarkPhoenix
6b77d72f06 Add red color icon 2019-08-06 10:08:07 +03:00
DarkPhoenix
ac5768e666 Remove color column icon and set it to static size 2019-08-06 09:58:00 +03:00
DarkPhoenix
f3bd47f347 Add color getter-setter for source wrapper 2019-08-05 20:29:41 +03:00
DarkPhoenix
1fbb47d64b Add color column 2019-08-05 20:24:28 +03:00
DarkPhoenix
3797887abc Add color definitions 2019-08-05 20:11:19 +03:00
DarkPhoenix
8870eef79b Fix vector scrolling under Windows 2019-08-05 14:15:58 +03:00
DarkPhoenix
eefcd9e738 Fix mistype 2019-08-05 13:29:53 +03:00
DarkPhoenix
5f296bbe30 Fix display of shield and cap amount 2019-08-05 11:20:18 +03:00
DarkPhoenix
d88fa1131d Rescale proportions of source and target sizers according to amount of columns they have 2019-08-05 10:53:00 +03:00
DarkPhoenix
3c6071ad88 Do not show resist column and mode picker when resists are ignored 2019-08-05 10:41:06 +03:00
DarkPhoenix
deb772f0a7 Show "Effective" prefix when resists are not ignored 2019-08-05 10:26:17 +03:00
DarkPhoenix
858719aad8 Enable "open in new tab" menu 2019-08-05 09:57:08 +03:00
DarkPhoenix
14debfd25c Change proportions between attacker and target lists 2019-08-05 08:12:44 +03:00
DarkPhoenix
510c9cafec Add context menu to change resist modes 2019-08-05 03:10:41 +03:00
DarkPhoenix
6434902f86 Do actual resist calculations 2019-08-05 02:37:54 +03:00
DarkPhoenix
16be84420b Add display of resist mode to column 2019-08-05 02:23:39 +03:00
DarkPhoenix
8f2283f9aa Implement auto primary layer detection 2019-08-05 02:14:27 +03:00
DarkPhoenix
920b84886c Get proper wrapper when requesting it by row 2019-08-04 23:56:55 +03:00
DarkPhoenix
cb2f0e40ba Add column which shows target resists 2019-08-04 23:55:42 +03:00
DarkPhoenix
14a9c9910c Implement resist modes except for auto 2019-08-04 23:30:06 +03:00
DarkPhoenix
c8d0ae8659 Make internal list of wrapper private 2019-08-04 20:04:12 +03:00
DarkPhoenix
fd541ead6c Change sorting order 2019-08-04 00:27:09 +03:00
DarkPhoenix
71e7ea0230 Fix another issue when working with sets 2019-08-04 00:24:28 +03:00
DarkPhoenix
cab2d41269 Fix calculation crash in DPS graph 2019-08-04 00:20:22 +03:00
DarkPhoenix
f5b1c79029 Do not access properties which were removed 2019-08-04 00:07:55 +03:00
DarkPhoenix
885a3f1ac9 Remove unneeded property 2019-08-04 00:00:53 +03:00
DarkPhoenix
e821b2d09c Store wrappers in graph lists 2019-08-03 23:56:44 +03:00
DarkPhoenix
1b2bff8a77 Change default graph resolution and depth 2019-08-03 17:33:56 +03:00
DarkPhoenix
d213e94860 Reorganize graph folder structure 2019-08-03 17:23:34 +03:00
DarkPhoenix
d2b71d97d2 Minor style fixes 2019-08-03 01:27:21 +03:00
DarkPhoenix
1ff7bdf1a7 Increase resolution of warp time graph 2019-08-03 01:10:58 +03:00
DarkPhoenix
f221f2df4f Swap getters and denormalizers 2019-08-03 01:04:20 +03:00
DarkPhoenix
46f365c42d Change internal interfaces a little 2019-08-03 00:55:58 +03:00
DarkPhoenix
a53c00aeda Change linear iter function 2019-08-02 23:47:48 +03:00
DarkPhoenix
044818aa65 Change add extra points function style a little 2019-08-02 23:36:18 +03:00
DarkPhoenix
a55084dbae Do not flicker when switching graphs on Windows 2019-08-02 16:51:45 +03:00
DarkPhoenix
c2c9528e80 Implement "adaptive" resolution for smooth graphs 2019-08-02 16:43:10 +03:00
DarkPhoenix
9c7ad95f6e Add float error workarounds 2019-08-02 15:40:53 +03:00
DarkPhoenix
fe9dc0a3e5 Implement point getter for time functions 2019-08-02 15:18:31 +03:00
DarkPhoenix
25712ef778 Move data preparation for x time mixin to separate function 2019-08-02 15:02:42 +03:00
DarkPhoenix
a63b543e0c Rework DPS graph to use new getters as well 2019-08-02 14:45:11 +03:00
DarkPhoenix
8ebec1f957 Fix comment 2019-08-02 10:34:16 +03:00
DarkPhoenix
0733fee878 Rework shield regen graph 2019-08-02 10:32:02 +03:00
DarkPhoenix
d52dd535a3 Rework cap regen graph 2019-08-02 10:21:20 +03:00
DarkPhoenix
cbc6475875 Split up base graph file too 2019-08-02 10:09:05 +03:00
DarkPhoenix
c6de92592c Rework mobility graph to use new getters 2019-08-02 10:02:42 +03:00
DarkPhoenix
5f97734881 Make distinction between mainParam, mainParamRange and x
- mainParam: (handle, value)
- mainParamRange: (handle (value1, value2))
- x: value
2019-08-02 09:49:15 +03:00
DarkPhoenix
62fbb7c9c8 Rework warp time graph to use new getter approach 2019-08-02 09:36:22 +03:00
DarkPhoenix
4ddbdebae4 Specify cache getter in getters 2019-08-02 00:10:38 +03:00
DarkPhoenix
542b79fa00 Do not request needed data every point calculation 2019-08-02 00:02:51 +03:00
DarkPhoenix
9f6f5c8a76 Show addition of negative amount as subtraction 2019-08-01 23:46:40 +03:00
DarkPhoenix
8591f649d1 Fix the same in another view 2019-08-01 23:41:32 +03:00
DarkPhoenix
3bbd51614d Fix modifier signs in affected tab view 2019-08-01 23:40:01 +03:00
DarkPhoenix
cb20c8588f Add missing jump portal and clone vat bay effects 2019-08-01 23:16:01 +03:00
DarkPhoenix
10b1c6ebfb Show proper warp time even when various modules which reduce ship speed to 0 are active 2019-08-01 23:03:45 +03:00
DarkPhoenix
3c6739b83a Inputs->params in few remaining functions 2019-08-01 22:41:18 +03:00
DarkPhoenix
57426f783e Rework warp time graph to have single getter (and break all other graphs for now) 2019-08-01 20:25:57 +03:00
DarkPhoenix
0788ff050d Rename inputs into params when they are actually not inputs 2019-08-01 20:15:49 +03:00
DarkPhoenix
18d59c119c Add double-click support to fit browser lite 2019-08-01 15:38:52 +03:00
DarkPhoenix
ae34cd5422 Allow to specify None as distance - it means that range will be ignored and all weapons will always hit 2019-08-01 15:28:33 +03:00
DarkPhoenix
50807e9381 Add tooltip to time input field 2019-08-01 13:09:28 +03:00
DarkPhoenix
fdb4d4d443 Rework input class 2019-08-01 13:00:29 +03:00
DarkPhoenix
ff22f12a56 Add extra option to show/hide legend 2019-08-01 12:48:29 +03:00
DarkPhoenix
15dc2a325a Change layout of fit browser lite a little 2019-08-01 12:40:20 +03:00
DarkPhoenix
ee9c1db000 Add support for ship browser lite to projected view 2019-08-01 12:26:35 +03:00
DarkPhoenix
b3b134ea45 Add support for ship browser lite to command view 2019-08-01 12:21:01 +03:00
DarkPhoenix
2e9b024390 Add browser lite handlers to graphs window 2019-08-01 11:54:33 +03:00
DarkPhoenix
ee2193e1bb Implement moving of fits back and forth in ship browser lite 2019-08-01 11:46:04 +03:00
DarkPhoenix
a582cf93bd Revert "user short names" change 2019-08-01 11:30:43 +03:00
DarkPhoenix
fc2d7cf7b8 Search by short ship name in fit browser lite as well 2019-08-01 11:26:02 +03:00
DarkPhoenix
61836dbb83 Move browser out of context menu file and do not list fits already existing in window 2019-08-01 11:23:26 +03:00
DarkPhoenix
154122388e Use short ship names along with fit names 2019-08-01 11:12:26 +03:00
DarkPhoenix
0114417018 Do actual search of fits on the left 2019-08-01 09:32:45 +03:00
DarkPhoenix
e662edc2cc Set custom window name depending on context 2019-08-01 09:09:30 +03:00
DarkPhoenix
9e6cdb2f4f Set focus to search bar when opening fit browser lite 2019-08-01 09:04:01 +03:00
DarkPhoenix
98b1fdb476 Change layout of tgt profile editor 2019-08-01 08:56:46 +03:00
DarkPhoenix
0f0e544f54 Fill window with actual data 2019-08-01 08:38:45 +03:00
DarkPhoenix
6d50f03396 Make sure buttons are aligned vertically at the center 2019-08-01 00:48:27 +03:00
DarkPhoenix
7ec9d3f122 Make sure graphs properly react to target profile updates 2019-08-01 00:21:42 +03:00
DarkPhoenix
592adb36f1 Start implementing fit browser lite 2019-07-31 20:22:18 +03:00
DarkPhoenix
d571191ec2 Add menu which allows to add target profiles to graph 2019-07-31 16:23:11 +03:00
DarkPhoenix
1f5fe47580 Rework target profile and damage pattern menus to use regular ticks 2019-07-31 15:54:09 +03:00
DarkPhoenix
1e3783c21d Move target profile context menu handling to separate package
This is needed to be able to import them separately at different times and avoid code duplication
2019-07-31 09:53:37 +03:00
DarkPhoenix
7190d91d31 Enable "add currently open fit" context menu for graphs 2019-07-31 09:13:29 +03:00
DarkPhoenix
5f697c166a Enable context menus on target list 2019-07-31 08:57:20 +03:00
DarkPhoenix
68b5fd9893 Show tooltips for target profiles too 2019-07-31 08:54:31 +03:00
DarkPhoenix
3e1a91d073 Clear plot cache on tgt profile changes 2019-07-31 08:34:46 +03:00
DarkPhoenix
c68451228a Move more logic to base class 2019-07-31 00:04:54 +03:00
DarkPhoenix
77ae235385 Offload actual set adding logic to windows calling the context menu 2019-07-30 22:01:33 +03:00
DarkPhoenix
c4009bdbd7 Start sharing some functionality between fit and target lists 2019-07-30 20:12:20 +03:00
DarkPhoenix
1eb48b00e1 Move fit-specific logic to fit list 2019-07-30 19:59:08 +03:00
DarkPhoenix
67cef93dd8 Re-use context menu infrastructure for fit list in graph window 2019-07-30 19:43:42 +03:00
DarkPhoenix
2aa274f56f Remove character editor implant set context menu hack as it's no longer needed 2019-07-30 19:20:14 +03:00
DarkPhoenix
cd20164d7a Pass calling window to context menu 2019-07-30 19:12:45 +03:00
DarkPhoenix
5a0ca503c1 Get rid of float error when converting float value to text in input boxes 2019-07-30 17:22:58 +03:00
DarkPhoenix
4c1c15e69e Rework target profile editor input boxes for better editing experience 2019-07-30 17:11:53 +03:00
DarkPhoenix
0320a16ba4 Merge branch 'master' into dps_sim_graph 2019-07-30 08:46:53 +03:00
DarkPhoenix
fcc8f3c5a7 Revert python version change used for tox 2019-07-30 08:45:56 +03:00
DarkPhoenix
fae3e8568a Merge branch 'graph_fix' 2019-07-30 08:44:40 +03:00
DarkPhoenix
4d2bb5ba87 Bump version 2019-07-30 08:22:54 +03:00
DarkPhoenix
d71cf64564 Add last known working numpy version 2019-07-30 08:11:16 +03:00
DarkPhoenix
1bb30499c2 Force MPL version to last known working 2019-07-30 07:59:19 +03:00
DarkPhoenix
d8deb98d7b Remove import of lib which is supposedly not used by older versions of
MPL
2019-07-27 23:05:14 +03:00
DarkPhoenix
e1078ef6da Try older MPL version 2019-07-27 22:20:58 +03:00
DarkPhoenix
a1d807bd45 Remove debugging stuff and try to downgrade matplotlib 2019-07-27 22:12:01 +03:00
DarkPhoenix
4f3228388c Try forcing module which is causing issues 2019-07-27 20:10:12 +03:00
DarkPhoenix
81e3edc041 Update some components in requirements file 2019-07-27 19:58:20 +03:00
DarkPhoenix
077db2ecd6 Add extra dependency to spec files 2019-07-27 19:14:31 +03:00
DarkPhoenix
6d2746ad75 Reraise exception on import 2019-07-27 19:02:10 +03:00
DarkPhoenix
0ffdae97fd Add some debug info to MPL import 2019-07-27 18:37:34 +03:00
DarkPhoenix
70fd1ac6de Update effects file 2019-07-27 12:49:39 +03:00
DarkPhoenix
78d056c6ff Merge branch 'master' into dps_sim_graph 2019-07-27 12:48:15 +03:00
DarkPhoenix
43ba63233d Merge branch 'vni_changes' 2019-07-27 12:43:54 +03:00
DarkPhoenix
adf750fe44 Adjust VNI effects 2019-07-27 12:32:56 +03:00
DarkPhoenix
250996e8ac Update icons 2019-07-27 08:58:27 +03:00
DarkPhoenix
56639a0812 Update database to 1541099 2019-07-27 08:50:47 +03:00
DarkPhoenix
c12e450648 Avoid reusing tooltips as it leads to segfaults due to some reason 2019-07-27 01:47:45 +03:00
DarkPhoenix
4fce6f7b99 Allow changing of extra attributes via profile editor 2019-07-26 19:30:10 +03:00
DarkPhoenix
27b8c12639 Add extra attributes to target profile editor 2019-07-26 17:48:19 +03:00
DarkPhoenix
5d5d9ff153 Rename pattern to profile 2019-07-26 15:55:10 +03:00
DarkPhoenix
d803c8374f Add target profile icon 2019-07-26 13:07:33 +03:00
DarkPhoenix
a5b22aa112 Denormalize infinity sig as special case, very much like 0 sig 2019-07-26 12:46:40 +03:00
DarkPhoenix
ae8fb25d3f Show plot marker if there's only one data point 2019-07-26 12:29:10 +03:00
DarkPhoenix
530dd1c03b Denormalize relative speed correctly if target has it equal to 0 2019-07-26 00:29:11 +03:00
DarkPhoenix
473b65850d Fetch target name correctly 2019-07-26 00:11:47 +03:00
DarkPhoenix
2c49bde5bf Do not consider TP multiplier as NaN for gun calculation 2019-07-26 00:00:18 +03:00
DarkPhoenix
97b32b33d3 Always show target profiles in target list 2019-07-25 23:48:24 +03:00
DarkPhoenix
1382e87133 Allow application of webs/TPs on target profiles 2019-07-25 20:49:20 +03:00
DarkPhoenix
19d03591b1 Process targets in DPS calculation code 2019-07-25 20:37:35 +03:00
DarkPhoenix
6f1321aa13 Initialize graph with ideal target profile, and plug it into UI's columns 2019-07-25 19:45:20 +03:00
DarkPhoenix
365a3798c2 Rename some remaining UI elements 2019-07-25 16:40:59 +03:00
DarkPhoenix
8d3981e1a4 Reimport extra target profile fields if they are defined 2019-07-25 16:36:22 +03:00
DarkPhoenix
e34fcb2f9c Rename multiple entities to reflect that it's target profile rather than target resists 2019-07-25 16:20:41 +03:00
DarkPhoenix
71f1c69f23 Plug new fields into actual targetResists objects 2019-07-25 10:00:51 +03:00
DarkPhoenix
072ad028a3 Add extra columns to target profile table 2019-07-24 19:50:58 +03:00
DarkPhoenix
a652e12fa4 Clear up drone/fighter DPS data on changing factor reload flag 2019-07-24 19:29:52 +03:00
DarkPhoenix
58f3618350 Add more info about various modules to misc column 2019-07-10 09:01:23 +03:00
DarkPhoenix
1b26cee9c1 Add some info about citadel modules to "Misc" column 2019-07-10 02:20:16 +03:00
DarkPhoenix
4752e5a20f Immobilize titans which use their DDs 2019-07-09 15:48:30 +03:00
DarkPhoenix
d8e277593d Add DD support to dps graph 2019-07-09 15:38:23 +03:00
DarkPhoenix
edbc341909 Use volley data in misc column to show doomsday damage 2019-07-09 07:49:17 +03:00
DarkPhoenix
5f20f249f7 Change reaper DD to show only one instance of damage 2019-07-09 07:41:10 +03:00
DarkPhoenix
a1de3b9225 Set published flag for DB TD 2019-07-09 00:37:36 +03:00
DarkPhoenix
5110e63809 Apply webs and TPs for all graph types 2019-07-08 19:53:10 +03:00
DarkPhoenix
26d4cfa2de Plug webs/TPs into x time graph 2019-07-08 19:24:43 +03:00
DarkPhoenix
ae1a9950bc Apply TP drones as well 2019-07-08 19:02:16 +03:00
DarkPhoenix
1c120f2fd6 Apply all webs including drones to target 2019-07-08 18:48:25 +03:00
DarkPhoenix
d7e45b0f76 Collect info about dromis into dictionary as well 2019-07-08 16:46:55 +03:00
DarkPhoenix
e796b748b6 Make sure dromis can be resisted 2019-07-08 16:37:43 +03:00
DarkPhoenix
a74984d37b Get resistance info of temporarily applied mods and use it during attr calculation 2019-07-08 11:42:04 +03:00
DarkPhoenix
e342f96fbe Move resistance attribute ID getter to separate function 2019-07-08 10:58:01 +03:00
DarkPhoenix
e262aa7daa Move resistance calculation to multiplication method 2019-07-08 10:27:06 +03:00
DarkPhoenix
c64d09ca54 Get data about webbing/TPing drones 2019-07-08 08:27:00 +03:00
DarkPhoenix
8def076175 Add burst projectors as webs/TPs 2019-07-08 08:16:23 +03:00
DarkPhoenix
a64fbd8976 Clear plot cache properly when target is modified 2019-07-08 07:34:09 +03:00
DarkPhoenix
eda869fe0d Skip modules which are not active 2019-07-08 07:22:21 +03:00
DarkPhoenix
04a74e278b Plug webs/TPs into calculation process 2019-07-08 00:29:23 +03:00
DarkPhoenix
6786cc7eff Expose boost/multiplier data to calculation method 2019-07-07 21:39:36 +03:00
DarkPhoenix
6984bd435f Add functions which calculate webbed/TPed stats and plug them into distance calculation 2019-07-07 21:22:23 +03:00
DarkPhoenix
ec8b771a24 Implement cache which stores data about cache TPs and webs 2019-07-07 20:16:21 +03:00
DarkPhoenix
6ce72e4fb3 Add methods which will be used to access temporarily modified values 2019-07-07 19:48:50 +03:00
DarkPhoenix
cda9ba5978 Add more columns for other graph types 2019-07-07 19:17:08 +03:00
DarkPhoenix
e2ae89f6b9 Add more columns to DPS graphs 2019-07-07 18:55:52 +03:00
DarkPhoenix
3bc3705c42 Show icons instead of names for dps/volley columns 2019-07-07 18:02:53 +03:00
DarkPhoenix
522de5ca5a Do not set column image if there's none 2019-07-07 17:27:37 +03:00
DarkPhoenix
efd8a6964e Add/remove fit list columns dynamically as graph is switched 2019-07-07 17:00:42 +03:00
DarkPhoenix
1cd10d2109 Do not call command fit refresh twice 2019-07-07 15:07:24 +03:00
DarkPhoenix
3a09f4b45c Rework FitChanged command to avoid refreshing graph multiple times in certain cases 2019-07-07 14:25:27 +03:00
DarkPhoenix
64bc2c34c2 Move addition of column by name to separate method 2019-07-07 12:21:53 +03:00
DarkPhoenix
845630437e Do not show dps/volley columns by default 2019-07-07 12:10:32 +03:00
DarkPhoenix
c4484d735a Add dps/volley columns to fit list 2019-07-07 02:50:07 +03:00
DarkPhoenix
5b74c6c5e1 Update graph info when fit name changes 2019-07-07 02:33:54 +03:00
DarkPhoenix
3e410540c9 Implement cache clear reasons to avoid clearing caches when we do not need that (esp useful for dmg time cache) 2019-07-07 02:08:04 +03:00
DarkPhoenix
5bba1dc88b Add context menu item which controls if webs/TPs are applied to the target 2019-07-07 00:50:12 +03:00
DarkPhoenix
8c0cae8bc3 Switch drone mode handling to use enums 2019-07-06 12:31:26 +03:00
DarkPhoenix
71e55a000b Drone controls now actually control how drones apply on graph 2019-07-06 03:29:09 +03:00
DarkPhoenix
7bcdf95f5c Refresh graph when graph options change 2019-07-06 03:16:31 +03:00
DarkPhoenix
4402addcb0 Add drone options to context menus 2019-07-06 03:06:47 +03:00
DarkPhoenix
3d57861481 Add ignore target resists menu 2019-07-06 02:42:11 +03:00
DarkPhoenix
5d1d2b87df Add context menu support to graph window 2019-07-06 02:30:06 +03:00
DarkPhoenix
389b5d57aa Do not restore graph type selected last time 2019-07-06 01:02:18 +03:00
DarkPhoenix
53de46bab7 Add graph settings and save selected graph type there 2019-07-05 20:15:44 +03:00
DarkPhoenix
e6dce726b7 Rework how toggling factor reload works 2019-07-05 09:10:23 +03:00
DarkPhoenix
63ca8dc559 Recalc all fits which might need that when changing factorReload flag 2019-07-05 01:08:00 +03:00
DarkPhoenix
6e083a5af8 Merge branch 'dps_sim_graph' of github.com:pyfa-org/Pyfa into dps_sim_graph 2019-07-05 00:40:30 +03:00
DarkPhoenix
ac93c5487c Change the way force reload setting is changed 2019-07-05 00:36:44 +03:00
DarkPhoenix
1f94b28b87 Add fighter bomb support 2019-07-04 19:30:02 +03:00
DarkPhoenix
78b6eb4283 Add regular and guided bombs to graphs 2019-07-04 19:14:46 +03:00
DarkPhoenix
417e478d27 Add smartbombs to calculation 2019-07-04 18:37:26 +03:00
DarkPhoenix
78d2dff0d8 Move x time graphs to new methods 2019-07-04 17:37:45 +03:00
DarkPhoenix
63c45c5060 Plug in all calculations besides where X is time 2019-07-04 17:22:23 +03:00
DarkPhoenix
c4f225003a Add fighters to dps vs range graph 2019-07-04 14:33:28 +03:00
DarkPhoenix
185cf4f625 Add drones to dps-range calculation 2019-07-04 13:40:04 +03:00
DarkPhoenix
d2b838e9d5 Rework interface between dps graph and time cache 2019-07-04 13:10:45 +03:00
DarkPhoenix
15b6a848e8 Move warp time subwarp speed calculation to separate cache as well 2019-07-04 11:40:38 +03:00
DarkPhoenix
193fcc60d8 Split time cache into separate file as well 2019-07-04 09:36:31 +03:00
DarkPhoenix
ae110371fe Split up dps graph file a little 2019-07-04 09:11:55 +03:00
DarkPhoenix
5857413285 Get rid of float error when changing vector length via scrolling 2019-07-03 20:07:19 +03:00
DarkPhoenix
4448d7e62f Plug turrets and missiles into dps vs range calculation 2019-07-03 20:03:22 +03:00
DarkPhoenix
d3ca0a961e Implement various functions to calculate damage delivery to specific targets 2019-07-03 18:06:33 +03:00
DarkPhoenix
405492d9d7 Move all the turret calculation logic into new graph 2019-07-03 11:38:21 +03:00
DarkPhoenix
d27d7656d5 Implement turret cth formula 2019-07-03 10:38:06 +03:00
DarkPhoenix
120bd9aa0c Set attacker vector to 90 degrees as well to be able to transversal match with fewer clicks 2019-07-03 10:23:12 +03:00
DarkPhoenix
6ab79ab5c0 Fix angular speed calculation 2019-07-03 10:19:33 +03:00
DarkPhoenix
b8d189c0ad Change vector behavior to be consistent with trigonometry 2019-07-03 10:14:55 +03:00
DarkPhoenix
86e04321c8 Add some calculations to angular velocity calculator 2019-07-03 08:39:44 +03:00
DarkPhoenix
6bcc906c4a Start moving some math to the new damage graph 2019-07-03 08:25:27 +03:00
DarkPhoenix
c3becec822 Refresh graph when calculation returned some error 2019-07-02 16:36:11 +03:00
DarkPhoenix
aae2e7c531 Enable all dps graphs over time 2019-07-02 16:30:24 +03:00
DarkPhoenix
52490144d3 Move some processing from intermediate method to final method to save resources when we need dps/volley, not damage 2019-07-02 14:42:03 +03:00
DarkPhoenix
c04c672f11 Fix incorrect intermediate-to-final cache conversion 2019-07-02 14:38:01 +03:00
DarkPhoenix
f51979b69a Plug new cache format into dmg vs time graph 2019-07-02 13:49:29 +03:00
DarkPhoenix
ab6b9759b0 Generate proper final dmg-time cache 2019-07-02 13:40:48 +03:00
DarkPhoenix
b3027532ff Collect all intermediate dps/volley/damage stats for all items 2019-07-02 02:08:29 +03:00
DarkPhoenix
494c9b08cb Start implementation of generic damage-time cache generator 2019-07-01 20:11:30 +03:00
DarkPhoenix
c595195519 Run special failover only on zero division errors 2019-07-01 12:32:28 +03:00
DarkPhoenix
c3efa819f4 Implement fallback for case when we convert relative value into absolute and then when converting it back to relative fails 2019-06-30 11:32:31 +03:00
DarkPhoenix
4e7580b277 Move dps vs time functionality to new graph 2019-06-29 23:49:43 +03:00
DarkPhoenix
af642a4259 Normalize to seconds when possible 2019-06-29 12:44:25 +03:00
DarkPhoenix
c365efb67e Move dmg vs time logic into new graph infrastructure 2019-06-29 12:31:30 +03:00
DarkPhoenix
fc7613451e Copy functionality from fit list to target list 2019-06-29 11:21:54 +03:00
DarkPhoenix
62b7b44120 Rework cache to store plot results based on composite key 2019-06-29 10:43:21 +03:00
DarkPhoenix
744fce2e82 Make it obvious that we're clearning cache by fitID 2019-06-29 10:31:21 +03:00
DarkPhoenix
dd55493b4e Minor stylistic fixes 2019-06-29 10:24:11 +03:00
DarkPhoenix
7e7b49d2e4 Move shield regen graph to new infrastructure 2019-06-29 10:21:16 +03:00
DarkPhoenix
24494e9b29 Rename cap graph 2019-06-29 00:28:24 +03:00
DarkPhoenix
eff0510092 Do not show time input when it's not needed 2019-06-28 22:08:19 +03:00
DarkPhoenix
988688939b Merge cap regen graph into already existing cap graph 2019-06-28 20:17:23 +03:00
DarkPhoenix
d448116e91 Transfer cap amount vs time graph to new infrastructure 2019-06-28 19:42:49 +03:00
DarkPhoenix
75ce6ffbcf Add stubs for getters 2019-06-28 19:07:16 +03:00
DarkPhoenix
60933a309f Re-enable dps graph again and add some info about how to process inputs and outputs
Real calculation hasn't been transferred yet
2019-06-28 18:56:57 +03:00
DarkPhoenix
428cb5c888 Re-enable mobility graph 2019-06-28 18:31:39 +03:00
DarkPhoenix
d195ec7e68 Move all the logic from eos graph to gui graph for warp time
Now backend graphs have to be aware of handles used in UI graphs, so why not
2019-06-28 15:44:50 +03:00
DarkPhoenix
c2017f3cb9 Re-enable DPS graph and make few fixes 2019-06-28 10:13:03 +03:00
DarkPhoenix
66ff4d827c Integrate graph frame with new APIs 2019-06-28 10:08:53 +03:00
DarkPhoenix
745914bf9e Add parameter normalization function 2019-06-28 09:24:06 +03:00
DarkPhoenix
421146eb54 More work on interfaces between gui and eos graphs 2019-06-27 20:45:21 +03:00
DarkPhoenix
ef81f9c830 Return input data in InputData objects for easier access 2019-06-27 18:52:23 +03:00
DarkPhoenix
1e760b2111 Re-enable warp graph and adapt it to new framework 2019-06-27 16:54:12 +03:00
DarkPhoenix
fe50372b12 Use unit as part of key again will be useful in warp graph 2019-06-27 13:15:22 +03:00
DarkPhoenix
7ef79eaa79 Stop using units as part of input key, they are not going to be different anyway 2019-06-26 21:39:12 +03:00
DarkPhoenix
9b282587b2 Remove limits argument as it's no longer used 2019-06-26 21:24:02 +03:00
DarkPhoenix
4af36514bc Handle vectors a in a separate function 2019-06-26 20:12:08 +03:00
DarkPhoenix
5320e99276 Restore values for vectors (just in case!) 2019-06-26 20:06:51 +03:00
DarkPhoenix
b733205541 Get rid of this smart shit and just store ranges and consts separately 2019-06-26 19:58:26 +03:00
DarkPhoenix
b125c62930 Reset stored values when switching graphs 2019-06-26 19:30:16 +03:00
DarkPhoenix
7895e4076d Add logic which transfers values when switching input fields 2019-06-26 19:29:11 +03:00
DarkPhoenix
9ec192de7d Add methods to convert input values 2019-06-26 18:59:52 +03:00
DarkPhoenix
8e41a31d1d Set vector defaults on initialization and graph switch 2019-06-26 18:03:37 +03:00
DarkPhoenix
fa4a2436aa When vectors are changed, ask to update graphs 2019-06-26 17:00:30 +03:00
DarkPhoenix
22ca78cb68 Implement method which gathers values across control panel boxes 2019-06-26 16:50:54 +03:00
DarkPhoenix
ee4a1f936b Add two classes to handle user input 2019-06-26 15:46:17 +03:00
DarkPhoenix
15a8c5750a Call layout when frame is created as well 2019-06-26 07:59:18 +03:00
DarkPhoenix
9f261f5b80 Change window size when needed 2019-06-26 07:53:22 +03:00
DarkPhoenix
b6a58b4ba6 Change vectors when needed 2019-06-25 19:53:13 +03:00
DarkPhoenix
09ca85ca81 Merge vector classes into one 2019-06-25 19:16:03 +03:00
DarkPhoenix
3aa69a6eaf Update inputs when X selection is updated 2019-06-25 17:42:15 +03:00
DarkPhoenix
2a645b1b04 Move input layout code into its own function 2019-06-25 16:56:26 +03:00
DarkPhoenix
0420f399ad Show-hide vectors and target list as needed 2019-06-25 16:37:33 +03:00
DarkPhoenix
509a45dcee Show labels for vectors separately 2019-06-25 16:02:45 +03:00
DarkPhoenix
52724d790b Change control panel layout 2019-06-25 15:25:48 +03:00
DarkPhoenix
4b960af9ab Rework code to use handle and unit to access various definitions 2019-06-25 11:40:10 +03:00
DarkPhoenix
022f0c06ee Do not show sig % except for the cases when it's used as main value range 2019-06-25 08:23:21 +03:00
DarkPhoenix
5ffd644ad9 Rework subgraph options 2019-06-24 20:24:19 +03:00
DarkPhoenix
03183827a6 Show all the needed controls on the panel 2019-06-24 16:15:35 +03:00
DarkPhoenix
5e7fcc32b6 Start adding code which uses new graph definition for layout 2019-06-24 10:33:59 +03:00
DarkPhoenix
3c0d87940b Change damage stats graph definition 2019-06-21 20:10:38 +03:00
DarkPhoenix
4cf07c4b76 Slap shit together and commit 2019-06-21 09:10:55 +03:00
DarkPhoenix
28db388fa0 Add subclass to specify direction only 2019-06-20 15:59:59 +03:00
DarkPhoenix
2c1905f041 Add vectors to panel (not yet functional) 2019-06-20 00:02:02 +03:00
DarkPhoenix
30d03f0ab5 Do not crash with Show y = 0 disabled and no fits 2019-06-18 16:38:10 +03:00
DarkPhoenix
4ca3f10bc9 Move more stuff away from the frame 2019-06-18 16:12:27 +03:00
DarkPhoenix
9cc228cfff Always run localized injectors heat effect early to apply heat bonus regardless of effect run ordering 2019-06-17 10:10:58 +03:00
DarkPhoenix
3359d8cb88 More code to control panel 2019-06-14 18:26:21 +03:00
DarkPhoenix
738d7f687d Move more code to control panel file 2019-06-14 16:38:41 +03:00
DarkPhoenix
b224196b05 Get rid of logic which handles legacy versions of matplotlib 2019-06-14 14:43:42 +03:00
DarkPhoenix
bbcc32c8cf Split graph frame into multiple files 2019-06-14 13:17:11 +03:00
DarkPhoenix
3c0b8643f6 Move graph file into graph package 2019-06-13 13:28:04 +03:00
DarkPhoenix
c85b6e4a36 Add vector class 2019-06-13 13:20:04 +03:00
DarkPhoenix
6003302e10 Merge branch 'master' into dps_sim_graph 2019-06-13 12:32:38 +03:00
DarkPhoenix
e7dd045979 Use default spool value for dps over range graph, if module has no per-module override 2019-06-06 20:14:32 +03:00
DarkPhoenix
6ca7a22c3e Add info about effective capacitor which takes into consideration neut resistance 2019-06-06 17:29:10 +03:00
DarkPhoenix
e8f09514ab Swap extra cap stats readout 2019-06-06 17:20:05 +03:00
DarkPhoenix
1a3a656879 Optimize checking long lines for speed 2019-06-06 09:09:25 +03:00
DarkPhoenix
e77ada4e8c Start searching from 1 char if strin contains CJK glyphs 2019-06-05 19:06:17 +03:00
DarkPhoenix
41b72c2789 Fix comment 2019-06-04 09:51:10 +03:00
DarkPhoenix
a4be7c5e9a Leave more time for less prioritized sources if more prioritized sources spent less time than we allocated to them 2019-06-04 09:42:44 +03:00
DarkPhoenix
fb3c183b3e Do not attempt to fetch data from unknown systems 2019-06-03 18:39:23 +03:00
DarkPhoenix
3e7dbef659 Remove debugging print 2019-06-03 18:31:48 +03:00
DarkPhoenix
89260d1d36 Always prefer primary data source, and switch evepraisal market source to use min price for items 2019-06-03 18:29:38 +03:00
DarkPhoenix
d451bda7ed Add evepraisal as price source 2019-06-03 18:02:48 +03:00
Anton Vorobyov
933c84466f Merge pull request #1993 from MaruMaruOO/master
Fix for EFS exports with ASBs.
2019-05-31 08:15:38 +03:00
MaruMaruOO
d4c9100f77 Fix for EFS exports with ASBs. 2019-05-30 20:34:55 -04:00
DarkPhoenix
c6aa72a3e3 Re-enable target list in graph panel 2019-05-29 15:29:24 +03:00
DarkPhoenix
7cf6ff04b6 Comment out target list as it caused graphical issues on windows 2019-05-28 17:47:27 +03:00
DarkPhoenix
5b575fdfe3 Make sure panel has no padding to avoid ugly border on Windows 2019-05-28 17:30:50 +03:00
DarkPhoenix
a8a5fabce7 Always add last data point to dps over time graph 2019-05-28 16:33:30 +03:00
DarkPhoenix
f41d6dd2c1 Update database to 1514398 2019-05-28 16:15:29 +03:00
DarkPhoenix
09727c102a Bump pyfa version 2019-05-28 16:06:40 +03:00
DarkPhoenix
6580734dc7 Print full connection exception in case of failure in debug mode 2019-05-28 16:03:25 +03:00
DarkPhoenix
ff34865067 Add target panel as dark code 2019-05-28 15:05:16 +03:00
DarkPhoenix
bdd400fd51 Remove conflicting shortcut 2019-05-27 19:07:58 +03:00
DarkPhoenix
1e8184a80b Make ESI browser tree panel non-scalable 2019-05-27 19:06:53 +03:00
DarkPhoenix
6a20f04c7f Merge branch 'master' into singularity 2019-05-27 19:04:27 +03:00
Anton Vorobyov
d81acc1f9c Merge pull request #1987 from AaronOpfer/esi_import_sizing
improve sizing on esi fit import browser
2019-05-27 19:03:59 +03:00
DarkPhoenix
bc84c20cb2 Update database with actual contents before and after fill, also do it in UI commands 2019-05-27 15:35:14 +03:00
DarkPhoenix
d5c5e2698e Redraw only after some delay (reuse market search delay for that) 2019-05-27 13:46:40 +03:00
Anton Vorobyov
16a78e689e Merge pull request #1988 from MaruMaruOO/master
Updates EFS exports to support local repairs, cap  warfare and ammo switching.
2019-05-27 10:32:49 +03:00
MaruMaruOO
066f29660d Added repair and cap data to EFS exports. 2019-05-27 02:18:10 -04:00
Aaron Opfer
bba0df5f50 improve sizing on esi fit import browser
On my machine, using the ESI import feature and clicking "Fetch Fits" gives the appearance of nothing happening. Turns out, this was because by default the tree on the left-hand side of the window has zero width by default and the parent window needs to be resized to see the fits. Fix this by setting a minimum size for the fitting browser tree and increase the default window size to compensate.
2019-05-26 04:33:15 -05:00
DarkPhoenix
45452ca680 Add rename mappings for faction trig guns 2019-05-25 22:04:17 +03:00
DarkPhoenix
4fbbc18f9f Accept mix of localized hints and regular names in XML importer 2019-05-25 17:43:47 +03:00
DarkPhoenix
22ec280ec2 Fix showing of neut resistance 2019-05-24 23:17:02 +03:00
DarkPhoenix
89c06b5201 Rescale contents on graph switch 2019-05-24 13:35:45 +03:00
DarkPhoenix
86d5f72988 Normalize drone tracking for Misc column 2019-05-23 15:49:49 +03:00
MaruMaruOO
c3e055a4c9 Merge branch 'master' of https://github.com/pyfa-org/Pyfa 2019-05-22 02:55:46 -04:00
MaruMaruOO
e48631956d Add typeIDs for cargo to EFS exports. 2019-05-22 02:35:47 -04:00
DarkPhoenix
2964f3b009 Do not recreate checkbox on each graphical switch
Leads to weird graphical glitches on GTK
2019-05-21 22:09:08 +03:00
DarkPhoenix
527c66dca4 Change the way radio buttons look 2019-05-21 21:29:26 +03:00
DarkPhoenix
24909f0523 Extend allowable range a little so that first reload of triglavians is included 2019-05-21 21:11:09 +03:00
DarkPhoenix
ed7494b3a4 Implement graph type selection 2019-05-21 19:47:55 +03:00
DarkPhoenix
8fae275e5a Cache graph values on GUI graphs so they do not get recalculated when graph options are changed 2019-05-21 19:06:11 +03:00
DarkPhoenix
a09a2a5f4b Add option whether 0 value should be shown or not on graphs 2019-05-21 17:51:50 +03:00
DarkPhoenix
2adc150811 Limit dmg and dps over time graphs to not hog resources on weaker machines for too long 2019-05-21 14:37:35 +03:00
DarkPhoenix
f5cad33b6c Some layout changes 2019-05-20 12:22:44 +03:00
DarkPhoenix
338cf45f65 Include "high" endpoint for damage over time graph 2019-05-20 10:41:26 +03:00
DarkPhoenix
359c60bafb Increase graph size and add some padding 2019-05-20 10:32:40 +03:00
DarkPhoenix
f808b73a5d Add axis labels 2019-05-20 07:56:04 +03:00
DarkPhoenix
8dd87cde58 When fit is removed, update projected/command contents if needed 2019-05-19 21:53:44 +03:00
DarkPhoenix
1ec78d9beb Remove fit from graph window when it gets deleted 2019-05-19 21:29:43 +03:00
DarkPhoenix
9c710285f2 Do not cycle over mods which are not dealing damage when composing cache 2019-05-19 21:04:41 +03:00
DarkPhoenix
90f745a18f Refresh cache only for changed fits rather than every fit 2019-05-19 20:54:12 +03:00
DarkPhoenix
af446579ab Get clean subwarp speed (no cloaks, propmods, webs etc) 2019-05-19 20:38:56 +03:00
DarkPhoenix
bcc11bd172 Reorder fields in graph 2019-05-19 14:11:05 +03:00
DarkPhoenix
0c31f756a8 Restore DPS vs range graph 2019-05-19 14:02:24 +03:00
DarkPhoenix
16fdd5a5e6 Fix warp graph 2019-05-19 04:07:40 +03:00
DarkPhoenix
ec1a2c66ee Restore shield graphs 2019-05-18 22:52:59 +03:00
DarkPhoenix
c3f41d68e6 Round in getYforX as well 2019-05-18 11:19:08 +03:00
DarkPhoenix
2f8701b4b2 Rework plot point processing for dmg over time graph the same way 2019-05-18 11:15:27 +03:00
DarkPhoenix
9b2d5410d6 Fix dps over time graph 2019-05-18 11:03:12 +03:00
Anton Vorobyov
1d8c9d2c40 Merge pull request #1977 from jtaylormayfield/master
Applied implant and booster skill check
2019-05-18 01:04:05 +03:00
J. Taylor Mayfield
8c0817245f Include skill prerequisites for applied implants and boosters in fit skill check. 2019-05-17 15:27:17 -05:00
DarkPhoenix
2a04e60ae0 Restore functionality of dmg vs time graph 2019-05-17 18:44:52 +03:00
DarkPhoenix
fb5eb220fd Rework graph interfaces again 2019-05-17 17:48:20 +03:00
DarkPhoenix
512f48ebdd Force frame refresh on graph redraw 2019-05-17 16:02:43 +03:00
DarkPhoenix
09db7d26a7 Rework mobility vs time graph 2019-05-17 15:38:06 +03:00
DarkPhoenix
690cf5eca1 Rework cap regen graph to use new interfaces 2019-05-17 15:11:52 +03:00
DarkPhoenix
1aee4c59c4 Merge branch 'singularity' of github.com:pyfa-org/Pyfa into singularity 2019-05-17 14:49:13 +03:00
DarkPhoenix
f1384074b5 Rework internal graph interfaces 2019-05-17 14:48:42 +03:00
DarkPhoenix
203bed06d6 Do not consider previous item heights when setting new item height 2019-05-16 22:13:01 +03:00
DarkPhoenix
0b00e28863 Fix #1974 2019-05-15 01:15:41 +03:00
DarkPhoenix
d74d331642 Merge branch 'singularity' of github.com:pyfa-org/Pyfa into singularity 2019-05-15 00:52:23 +03:00
DarkPhoenix
f075fbdc63 Do not show abyssal items in variations menu 2019-05-15 00:52:04 +03:00
DarkPhoenix
d59b6696ca Remove links to unmaintained packages 2019-05-14 14:22:09 +03:00
Anton Vorobyov
eedf6f9a39 Merge pull request #1973 from ZeroPointEnergy/pyfa_gentoo_overlay
Add link to Gentoo pyfa overlay
2019-05-14 14:19:07 +03:00
DarkPhoenix
1cca5729fc Add cap delta to tooltip on capacitor view 2019-05-14 14:15:58 +03:00
Andreas Zuber
a7b01ece22 Add link to Gentoo pyfa overlay 2019-05-14 13:08:47 +02:00
DarkPhoenix
d17e6d08d8 Fix definite integral calculation for distance traveled graph 2019-05-13 10:03:19 +03:00
DarkPhoenix
b29aaa9e20 Show ship name in legend as well (short, if needed) 2019-05-13 09:27:25 +03:00
DarkPhoenix
71ae59b2b5 Bump version 2019-05-13 06:48:35 +03:00
DarkPhoenix
51294f6cbc Change the way graphs are imported 2019-05-13 06:46:25 +03:00
DarkPhoenix
0439ace886 UK -> US spelling 2019-05-12 18:35:23 +03:00
863 changed files with 17888 additions and 21507 deletions

View File

@@ -1,5 +1,7 @@
os: linux
language: python
git:
depth: 400
python:
- 3.6
matrix:

View File

@@ -24,10 +24,8 @@ $ brew install Caskroom/cask/pyfa
### Linux Distro-specific Packages
The following is a list of pyfa packages available for certain distributions. Please note that these packages are maintained by third-parties and are not evaluated by the pyfa developers.
* Debian/Ubuntu/derivatives: https://github.com/AdamMajer/Pyfa/releases
* Arch: https://aur.archlinux.org/packages/pyfa/
* openSUSE: https://build.opensuse.org/package/show/home:rmk2/pyfa
* FreeBSD: http://www.freshports.org/games/pyfa/ (see [#484](https://github.com/pyfa-org/Pyfa/issues/484) for instructions)
* Gentoo: https://github.com/ZeroPointEnergy/gentoo-pyfa-overlay
### Dependencies
If you wish to help with development or simply need to run pyfa through a Python interpreter, the following software is required:

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, targetResists, user
#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
# If using in memory saveddata, you'll want to reflect it so the data structure is good.
if saveddata_connectionstring == "sqlite:///:memory:":

View File

@@ -62,4 +62,4 @@ def HeronFit(DB, Gamedata, Saveddata):
for _ in range(4):
fit.modules.append(mod)
return fit
return fit

View File

@@ -233,7 +233,7 @@ def defLogging():
])
class LoggerWriter(object):
class LoggerWriter:
def __init__(self, level):
# self.level is really like using log.debug(message)
# at least in my case

View File

@@ -1,7 +1,7 @@
import heapq
import time
from math import sqrt, exp
from functools import reduce
from collections import Counter
DAY = 24 * 60 * 60 * 1000
@@ -13,7 +13,7 @@ def lcm(a, b):
return n / a
class CapSimulator(object):
class CapSimulator:
"""Entity's EVE Capacitor Simulator"""
def __init__(self):
@@ -21,6 +21,7 @@ class CapSimulator(object):
self.capacitorCapacity = 100
self.capacitorRecharge = 1000
self.startingCapacity = 1000
# max simulated time.
self.t_max = DAY
@@ -41,6 +42,14 @@ class CapSimulator(object):
# relevant decimal digits of capacitor for LCM period optimization
self.stability_precision = 1
# Stores how cap sim changed cap values outside of cap regen time
self.saved_changes = ()
self.saved_changes_internal = None
# Reports if sim was stopped due to detecting stability early
self.optimize_repeats = True
self.result_optimized_repeats = None
def scale_activation(self, duration, capNeed):
for res in self.scale_resolutions:
mod = duration % res
@@ -59,7 +68,7 @@ class CapSimulator(object):
return duration, capNeed
def init(self, modules):
"""prepare modules. a list of (duration, capNeed, clipSize, disableStagger) tuples is
"""prepare modules. a list of (duration, capNeed, clipSize, disableStagger, reloadTime, isInjector) tuples is
expected, with clipSize 0 if the module has infinite ammo.
"""
self.modules = modules
@@ -67,48 +76,57 @@ class CapSimulator(object):
def reset(self):
"""Reset the simulator state"""
self.state = []
self.saved_changes_internal = {}
self.result_optimized_repeats = False
mods = {}
period = 1
disable_period = False
# Loop over modules, clearing clipSize if applicable, and group modules based on attributes
for (duration, capNeed, clipSize, disableStagger, reloadTime) in self.modules:
for (duration, capNeed, clipSize, disableStagger, reloadTime, isInjector) in self.modules:
if self.scale:
duration, capNeed = self.scale_activation(duration, capNeed)
# set clipSize to infinite if reloads are disabled unless it's
# a cap booster module.
if not self.reload and capNeed > 0:
# a cap booster module
if not self.reload and not isInjector:
clipSize = 0
reloadTime = 0
# Group modules based on their properties
if (duration, capNeed, clipSize, disableStagger, reloadTime) in mods:
mods[(duration, capNeed, clipSize, disableStagger, reloadTime)] += 1
key = (duration, capNeed, clipSize, disableStagger, reloadTime, isInjector)
if key in mods:
mods[key] += 1
else:
mods[(duration, capNeed, clipSize, disableStagger, reloadTime)] = 1
mods[key] = 1
# Loop over grouped modules, configure staggering and push to the simulation state
for (duration, capNeed, clipSize, disableStagger, reloadTime), amount in mods.items():
for (duration, capNeed, clipSize, disableStagger, reloadTime, isInjector), amount in mods.items():
# period optimization doesn't work when reloads are active.
if clipSize:
disable_period = True
# Just push multiple instances if item is injector. We do not want to stagger them as we will
# use them as needed and want them to be available right away
if isInjector:
for i in range(amount):
heapq.heappush(self.state, [0, duration, capNeed, 0, clipSize, reloadTime, isInjector])
continue
if self.stagger and not disableStagger:
# Stagger all mods if they do not need to be reloaded
if clipSize == 0:
duration = int(duration / amount)
# Stagger mods after first
else:
stagger_amount = (duration * clipSize + reloadTime) / (amount * clipSize)
for i in range(1, amount):
heapq.heappush(self.state,
[i * stagger_amount, duration,
capNeed, 0, clipSize, reloadTime])
heapq.heappush(self.state, [i * stagger_amount, duration, capNeed, 0, clipSize, reloadTime, isInjector])
# If mods are not staggered - just multiply cap use
else:
capNeed *= amount
period = lcm(period, duration)
# period optimization doesn't work when reloads are active.
if clipSize:
disable_period = True
heapq.heappush(self.state, [0, duration, capNeed, 0, clipSize, reloadTime])
heapq.heappush(self.state, [0, duration, capNeed, 0, clipSize, reloadTime, isInjector])
if disable_period:
self.period = self.t_max
@@ -119,7 +137,8 @@ class CapSimulator(object):
"""Run the simulation"""
start = time.time()
awaitingInjectors = []
awaitingInjectorsCounterWrap = Counter()
self.reset()
push = heapq.heappush
@@ -129,27 +148,36 @@ class CapSimulator(object):
stability_precision = self.stability_precision
period = self.period
activation = None
iterations = 0
capCapacity = self.capacitorCapacity
tau = self.capacitorRecharge / 5.0
cap_wrap = capCapacity # cap value at last period
cap_lowest = capCapacity # lowest cap value encountered
cap_lowest_pre = capCapacity # lowest cap value before activations
cap = capCapacity # current cap value
cap_wrap = self.startingCapacity # cap value at last period
cap_lowest = self.startingCapacity # lowest cap value encountered
cap_lowest_pre = self.startingCapacity # lowest cap value before activations
cap = self.startingCapacity # current cap value
t_wrap = self.period # point in time of next period
t_last = 0
t_max = self.t_max
while 1:
activation = pop(state)
t_now, duration, capNeed, shot, clipSize, reloadTime = activation
# Nothing to pop - might happen when no mods are activated, or when
# only cap injectors are active (and are postponed by code below)
try:
activation = pop(state)
except IndexError:
break
t_now, duration, capNeed, shot, clipSize, reloadTime, isInjector = activation
# Max time reached, stop simulation - we're stable
if t_now >= t_max:
break
cap = ((1.0 + (sqrt(cap / capCapacity) - 1.0) * exp((t_last - t_now) / tau)) ** 2) * capCapacity
# Regenerate cap from last time point
if t_now > t_last:
cap = ((1.0 + (sqrt(cap / capCapacity) - 1.0) * exp((t_last - t_now) / tau)) ** 2) * capCapacity
if t_now != t_last:
if cap < cap_lowest_pre:
@@ -157,36 +185,104 @@ class CapSimulator(object):
if t_now == t_wrap:
# history is repeating itself, so if we have more cap now than last
# time this happened, it is a stable setup.
if cap >= cap_wrap:
awaitingInjectorsCounterNow = Counter(awaitingInjectors)
if self.optimize_repeats and cap >= cap_wrap and awaitingInjectorsCounterNow == awaitingInjectorsCounterWrap:
self.result_optimized_repeats = True
break
cap_wrap = round(cap, stability_precision)
awaitingInjectorsCounterWrap = awaitingInjectorsCounterNow
t_wrap += period
cap -= capNeed
if cap > capCapacity:
cap = capCapacity
t_last = t_now
iterations += 1
t_last = t_now
# If injecting cap will "overshoot" max cap, postpone it
if isInjector and cap - capNeed > capCapacity:
awaitingInjectors.append((duration, capNeed, shot, clipSize, reloadTime, isInjector))
if cap < cap_lowest:
if cap < 0.0:
break
cap_lowest = cap
else:
# If we will need more cap than we have, but we are not at 100% -
# use awaiting cap injectors to top us up until we have enough or
# until we're full
if capNeed > cap and cap < capCapacity:
while awaitingInjectors and capNeed > cap and capCapacity > cap:
neededInjection = min(capNeed - cap, capCapacity - cap)
# Find injectors which have just enough cap or more
goodInjectors = [i for i in awaitingInjectors if -i[1] >= neededInjection]
if goodInjectors:
# Pick injector which overshoots the least
bestInjector = min(goodInjectors, key=lambda i: -i[1])
else:
# Take the one which provides the most cap
bestInjector = max(goodInjectors, key=lambda i: -i[1])
# Use injector
awaitingInjectors.remove(bestInjector)
inj_duration, inj_capNeed, inj_shot, inj_clipSize, inj_reloadTime, inj_isInjector = bestInjector
cap -= inj_capNeed
if cap > capCapacity:
cap = capCapacity
self.saved_changes_internal[t_now] = cap
# Add injector to regular state tracker
inj_t_now = t_now
inj_t_now += inj_duration
inj_shot += 1
if inj_clipSize:
if inj_shot % inj_clipSize == 0:
inj_shot = 0
inj_t_now += inj_reloadTime
push(state, [inj_t_now, inj_duration, inj_capNeed, inj_shot, inj_clipSize, inj_reloadTime, inj_isInjector])
# queue the next activation of this module
t_now += duration
shot += 1
if clipSize:
if shot % clipSize == 0:
shot = 0
t_now += reloadTime # include reload time
activation[0] = t_now
activation[3] = shot
# Apply cap modification
cap -= capNeed
if cap > capCapacity:
cap = capCapacity
self.saved_changes_internal[t_now] = cap
if cap < cap_lowest:
# Negative cap - we're unstable, simulation is over
if cap < 0.0:
break
cap_lowest = cap
# Try using awaiting injectors to top up the cap after spending some
while awaitingInjectors and cap < capCapacity:
neededInjection = capCapacity - cap
# Find injectors which do not overshoot max cap
goodInjectors = [i for i in awaitingInjectors if -i[1] <= neededInjection]
if not goodInjectors:
break
# Take the one which provides the most cap
bestInjector = max(goodInjectors, key=lambda i: -i[1])
# Use injector
awaitingInjectors.remove(bestInjector)
inj_duration, inj_capNeed, inj_shot, inj_clipSize, inj_reloadTime, inj_isInjector = bestInjector
cap -= inj_capNeed
if cap > capCapacity:
cap = capCapacity
self.saved_changes_internal[t_now] = cap
# Add injector to regular state tracker
inj_t_now = t_now
inj_t_now += inj_duration
inj_shot += 1
if inj_clipSize:
if inj_shot % inj_clipSize == 0:
inj_shot = 0
inj_t_now += inj_reloadTime
push(state, [inj_t_now, inj_duration, inj_capNeed, inj_shot, inj_clipSize, inj_reloadTime, inj_isInjector])
# queue the next activation of this module
t_now += duration
shot += 1
if clipSize:
if shot % clipSize == 0:
shot = 0
t_now += reloadTime # include reload time
activation[0] = t_now
activation[3] = shot
push(state, activation)
if activation is not None:
push(state, activation)
push(state, activation)
# update instance with relevant results.
self.t = t_last
@@ -194,7 +290,7 @@ class CapSimulator(object):
# calculate EVE's stability value
try:
avgDrain = reduce(float.__add__, [x[2] / x[1] for x in self.state], 0.0)
avgDrain = sum(x[2] / x[1] for x in self.state)
self.cap_stable_eve = 0.25 * (1.0 + sqrt(-(2.0 * avgDrain * tau - capCapacity) / capCapacity)) ** 2
except ValueError:
self.cap_stable_eve = 0.0
@@ -204,7 +300,9 @@ class CapSimulator(object):
self.cap_stable_low = cap_lowest
self.cap_stable_high = cap_lowest_pre
else:
self.cap_stable_low = \
self.cap_stable_high = 0.0
self.cap_stable_low = self.cap_stable_high = 0.0
self.saved_changes = tuple((k / 1000, max(0, self.saved_changes_internal[k])) for k in sorted(self.saved_changes_internal))
self.saved_changes_internal = None
self.runtime = time.time() - start

View File

@@ -101,3 +101,12 @@ class FitSystemSecurity(IntEnum):
LOWSEC = 1
NULLSEC = 2
WSPACE = 3
@unique
class Operator(IntEnum):
PREASSIGN = 0
PREINCREASE = 1
MULTIPLY = 2
POSTINCREASE = 3
FORCE = 4

View File

@@ -35,6 +35,7 @@ class ReadOnlyException(Exception):
pass
pyfalog.debug('Initializing gamedata')
gamedata_connectionstring = config.gamedata_connectionstring
if callable(gamedata_connectionstring):
gamedata_engine = create_engine("sqlite://", creator=gamedata_connectionstring, echo=config.debug)
@@ -45,6 +46,7 @@ gamedata_meta = MetaData()
gamedata_meta.bind = gamedata_engine
gamedata_session = sessionmaker(bind=gamedata_engine, autoflush=False, expire_on_commit=False)()
pyfalog.debug('Getting gamedata version')
# This should be moved elsewhere, maybe as an actual query. Current, without try-except, it breaks when making a new
# game db because we haven't reached gamedata_meta.create_all()
try:
@@ -60,6 +62,7 @@ except Exception as e:
config.gamedata_version = None
config.gamedata_date = None
pyfalog.debug('Initializing saveddata')
saveddata_connectionstring = config.saveddata_connectionstring
if saveddata_connectionstring is not None:
if callable(saveddata_connectionstring):
@@ -76,16 +79,19 @@ else:
# Lock controlling any changes introduced to session
sd_lock = threading.RLock()
pyfalog.debug('Importing gamedata DB scheme')
# Import all the definitions for all our database stuff
# noinspection PyPep8
from eos.db.gamedata import alphaClones, attribute, category, effect, group, item, marketGroup, metaData, metaGroup, queries, traits, unit, dynamicAttributes
pyfalog.debug('Importing saveddata DB scheme')
# noinspection PyPep8
from eos.db.saveddata import booster, cargo, character, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, loadDefaultDatabaseValues, \
miscData, mutator, module, override, price, queries, skill, targetResists, user
miscData, mutator, module, override, price, queries, skill, targetProfile, user
# Import queries
pyfalog.debug('Importing gamedata queries')
# noinspection PyPep8
from eos.db.gamedata.queries import *
pyfalog.debug('Importing saveddata queries')
# noinspection PyPep8
from eos.db.saveddata.queries import *

View File

@@ -0,0 +1,16 @@
"""
Migration 32
- added speed, sig and radius columns to targetResists table
"""
import sqlalchemy
def upgrade(saveddata_engine):
for column in ('maxVelocity', 'signatureRadius', 'radius'):
try:
saveddata_engine.execute("SELECT {} FROM targetResists LIMIT 1;".format(column))
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE targetResists ADD COLUMN {} FLOAT;".format(column))

View File

@@ -0,0 +1,30 @@
"""
Migration 33
Allow use of floats in damage pattern values
"""
tmpTable = """
CREATE TABLE "damagePatternsTemp" (
"ID" INTEGER NOT NULL,
"name" VARCHAR,
"emAmount" FLOAT,
"thermalAmount" FLOAT,
"kineticAmount" FLOAT,
"explosiveAmount" FLOAT,
"ownerID" INTEGER,
"created" DATETIME,
"modified" DATETIME,
PRIMARY KEY ("ID"),
FOREIGN KEY("ownerID") REFERENCES users ("ID")
)
"""
def upgrade(saveddata_engine):
saveddata_engine.execute(tmpTable)
saveddata_engine.execute(
'INSERT INTO damagePatternsTemp (ID, name, emAmount, thermalAmount, kineticAmount, explosiveAmount, ownerID, created, modified) '
'SELECT ID, name, emAmount, thermalAmount, kineticAmount, explosiveAmount, ownerID, created, modified FROM damagePatterns')
saveddata_engine.execute('DROP TABLE damagePatterns')
saveddata_engine.execute('ALTER TABLE damagePatternsTemp RENAME TO damagePatterns')

View File

@@ -11,7 +11,7 @@ __all__ = [
"implant",
"damagePattern",
"miscData",
"targetResists",
"targetProfile",
"override",
"implantSet",
"loadDefaultDatabaseValues"

View File

@@ -17,7 +17,7 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from sqlalchemy import Table, Column, Integer, ForeignKey, String, DateTime
from sqlalchemy import Table, Column, Integer, Float, ForeignKey, String, DateTime
from sqlalchemy.orm import mapper
import datetime
@@ -27,10 +27,10 @@ from eos.saveddata.damagePattern import DamagePattern
damagePatterns_table = Table("damagePatterns", saveddata_meta,
Column("ID", Integer, primary_key=True),
Column("name", String),
Column("emAmount", Integer),
Column("thermalAmount", Integer),
Column("kineticAmount", Integer),
Column("explosiveAmount", Integer),
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)

View File

@@ -23,7 +23,7 @@ from logbook import Logger
pyfalog = Logger(__name__)
class DatabaseCleanup(object):
class DatabaseCleanup:
def __init__(self):
pass

View File

@@ -46,6 +46,7 @@ fighter_abilities_table = Table("fightersAbilities", saveddata_meta,
mapper(Fighter, fighters_table,
properties={
"owner" : relation(Fit),
"_amount" : fighters_table.c.amount,
"_Fighter__abilities": relation(
FighterAbility,
backref="fighter",

View File

@@ -41,7 +41,7 @@ from eos.saveddata.fighter import Fighter
from eos.saveddata.fit import Fit as es_Fit
from eos.saveddata.implant import Implant
from eos.saveddata.module import Module
from eos.saveddata.targetResists import TargetResists
from eos.saveddata.targetProfile import TargetProfile
from eos.saveddata.user import User
@@ -82,7 +82,7 @@ commandFits_table = Table("commandFits", saveddata_meta,
)
class ProjectedFit(object):
class ProjectedFit:
def __init__(self, sourceID, source_fit, amount=1, active=True):
self.sourceID = sourceID
self.source_fit = source_fit
@@ -113,7 +113,7 @@ class ProjectedFit(object):
)
class CommandFit(object):
class CommandFit:
def __init__(self, boosterID, booster_fit, active=True):
self.boosterID = boosterID
self.booster_fit = booster_fit
@@ -232,7 +232,7 @@ mapper(es_Fit, fits_table,
Character,
backref="fits"),
"_Fit__damagePattern": relation(DamagePattern),
"_Fit__targetResists": relation(TargetResists),
"_Fit__targetProfile": relation(TargetProfile),
"projectedOnto": projectedFitSourceRel,
"victimOf": relationship(
ProjectedFit,

View File

@@ -19,14 +19,14 @@
import eos.db
from eos.saveddata.damagePattern import DamagePattern as es_DamagePattern
from eos.saveddata.targetResists import TargetResists as es_TargetResists
from eos.saveddata.targetProfile import TargetProfile as es_TargetProfile
class ImportError(Exception):
pass
class DefaultDatabaseValues(object):
class DefaultDatabaseValues:
def __init__(self):
pass
@@ -133,66 +133,82 @@ class DefaultDatabaseValues(object):
eos.db.save(damageProfile)
@classmethod
def importResistProfileDefaults(cls):
targetResistProfileList = [["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"]]
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 targetResistProfileRow in targetResistProfileList:
name, em, therm, kin, exp = targetResistProfileRow
resistsProfile = eos.db.eos.db.getTargetResists(name)
if resistsProfile is None:
resistsProfile = es_TargetResists(em, therm, kin, exp)
resistsProfile.name = name
eos.db.save(resistsProfile)
for targetProfileRow in targetProfileList:
name = targetProfileRow[0]
em = targetProfileRow[1]
therm = targetProfileRow[2]
kin = targetProfileRow[3]
exp = targetProfileRow[4]
try:
maxVel = targetProfileRow[5]
except IndexError:
maxVel = None
try:
sigRad = targetProfileRow[6]
except IndexError:
sigRad = None
try:
radius = targetProfileRow[7]
except:
radius = None
targetProfile = eos.db.eos.db.getTargetProfile(name)
if targetProfile is None:
targetProfile = es_TargetProfile(em, therm, kin, exp, maxVel, sigRad, radius)
targetProfile.name = name
eos.db.save(targetProfile)
@classmethod
def importRequiredDefaults(cls):

View File

@@ -24,16 +24,16 @@ from sqlalchemy import desc, select
from sqlalchemy import func
from eos.db import saveddata_session, sd_lock
from eos.db.saveddata.fit import projectedFits_table
from eos.db.saveddata.fit import fits_table, projectedFits_table
from eos.db.util import processEager, processWhere
from eos.saveddata.price import Price
from eos.saveddata.user import User
from eos.saveddata.ssocharacter import SsoCharacter
from eos.saveddata.damagePattern import DamagePattern
from eos.saveddata.targetResists import TargetResists
from eos.saveddata.targetProfile import TargetProfile
from eos.saveddata.character import Character
from eos.saveddata.implantSet import ImplantSet
from eos.saveddata.fit import Fit
from eos.saveddata.fit import Fit, FitLite
from eos.saveddata.module import Module
from eos.saveddata.miscData import MiscData
from eos.saveddata.override import Override
@@ -326,6 +326,17 @@ def getFitList(eager=None):
return fits
def getFitListLite():
with sd_lock:
stmt = select([fits_table.c.ID, fits_table.c.name, fits_table.c.shipID])
data = eos.db.saveddata_session.execute(stmt).fetchall()
fits = []
for fitID, fitName, shipID in data:
fit = FitLite(id=fitID, name=fitName, shipID=shipID)
fits.append(fit)
return fits
@cachedQuery(Price, 1, "typeID")
def getPrice(typeID):
if isinstance(typeID, int):
@@ -366,16 +377,16 @@ def clearDamagePatterns():
return deleted_rows
def getTargetResistsList(eager=None):
def getTargetProfileList(eager=None):
eager = processEager(eager)
with sd_lock:
patterns = saveddata_session.query(TargetResists).options(*eager).all()
patterns = saveddata_session.query(TargetProfile).options(*eager).all()
return patterns
def clearTargetResists():
def clearTargetProfiles():
with sd_lock:
deleted_rows = saveddata_session.query(TargetResists).delete()
deleted_rows = saveddata_session.query(TargetProfile).delete()
commit()
return deleted_rows
@@ -408,22 +419,22 @@ def getDamagePattern(lookfor, eager=None):
return pattern
@cachedQuery(TargetResists, 1, "lookfor")
def getTargetResists(lookfor, eager=None):
@cachedQuery(TargetProfile, 1, "lookfor")
def getTargetProfile(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
with sd_lock:
pattern = saveddata_session.query(TargetResists).get(lookfor)
pattern = saveddata_session.query(TargetProfile).get(lookfor)
else:
eager = processEager(eager)
with sd_lock:
pattern = saveddata_session.query(TargetResists).options(*eager).filter(
TargetResists.ID == lookfor).first()
pattern = saveddata_session.query(TargetProfile).options(*eager).filter(
TargetProfile.ID == lookfor).first()
elif isinstance(lookfor, str):
eager = processEager(eager)
with sd_lock:
pattern = saveddata_session.query(TargetResists).options(*eager).filter(
TargetResists.name == lookfor).first()
pattern = saveddata_session.query(TargetProfile).options(*eager).filter(
TargetProfile.name == lookfor).first()
else:
raise TypeError("Need integer or string as argument")
return pattern
@@ -439,11 +450,11 @@ def getImplantSet(lookfor, eager=None):
eager = processEager(eager)
with sd_lock:
pattern = saveddata_session.query(ImplantSet).options(*eager).filter(
TargetResists.ID == lookfor).first()
TargetProfile.ID == lookfor).first()
elif isinstance(lookfor, str):
eager = processEager(eager)
with sd_lock:
pattern = saveddata_session.query(ImplantSet).options(*eager).filter(TargetResists.name == lookfor).first()
pattern = saveddata_session.query(ImplantSet).options(*eager).filter(TargetProfile.name == lookfor).first()
else:
raise TypeError("Improper argument")
return pattern

View File

@@ -0,0 +1,46 @@
# ===============================================================================
# Copyright (C) 2014 Ryan Holmes
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from sqlalchemy import Table, Column, Integer, Float, ForeignKey, String, DateTime
from sqlalchemy.orm import mapper
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})

View File

@@ -1,39 +0,0 @@
# ===============================================================================
# Copyright (C) 2014 Ryan Holmes
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from sqlalchemy import Table, Column, Integer, Float, ForeignKey, String, DateTime
from sqlalchemy.orm import mapper
import datetime
from eos.db import saveddata_meta
from eos.saveddata.targetResists import TargetResists
targetResists_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("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(TargetResists, targetResists_table)

View File

@@ -394,7 +394,7 @@ class HandledProjectedDroneList(HandledDroneCargoList):
proj.projected = False
class HandledItem(object):
class HandledItem:
def preAssignItemAttr(self, *args, **kwargs):
self.itemModifiedAttributes.preAssign(*args, **kwargs)
@@ -411,7 +411,7 @@ class HandledItem(object):
self.itemModifiedAttributes.force(*args, **kwargs)
class HandledCharge(object):
class HandledCharge:
def preAssignChargeAttr(self, *args, **kwargs):
self.chargeModifiedAttributes.preAssign(*args, **kwargs)

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@
# ===============================================================================
class EqBase(object):
class EqBase:
ID = None
def __eq__(self, other):

View File

@@ -243,6 +243,20 @@ class Item(EqBase):
self.__overrides = None
self.__priceObj = None
def getShortName(self, charLimit=12):
if len(self.name) <= charLimit:
return self.name
splitName = self.name.strip().split(' ')
if len(splitName) == 1:
return self.name
shortName = ''
for word in splitName:
try:
shortName += word[0].capitalize()
except IndexError:
pass
return shortName
@property
def attributes(self):
if not self.__moved:
@@ -580,8 +594,7 @@ class DynamicItemItem(EqBase):
class MarketGroup(EqBase):
def __repr__(self):
return "MarketGroup(ID={}, name={}, parent={}) at {}".format(
self.ID, self.name, getattr(self.parent, "name", None), self.name, hex(id(self))
)
self.ID, self.name, getattr(self.parent, "name", None), hex(id(self)))
class MetaGroup(EqBase):

View File

@@ -1,123 +0,0 @@
# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
import itertools
class Graph:
def __init__(self, fit, function, data=None):
self.fit = fit
self.data = {}
if data is not None:
for name, d in data.items():
self.setData(Data(name, d))
self.function = function
def clearData(self):
self.data.clear()
def setData(self, data):
self.data[data.name] = data
def getIterator(self):
pointNames = []
pointIterators = []
for data in self.data.values():
pointNames.append(data.name)
pointIterators.append(data)
return self._iterator(pointNames, pointIterators)
def _iterator(self, pointNames, pointIterators):
for pointValues in itertools.product(*pointIterators):
point = {}
for i in range(len(pointValues)):
point[pointNames[i]] = pointValues[i]
yield point, self.function(point)
class Data:
def __init__(self, name, dataString, step=None):
self.name = name
self.step = step
self.data = self.parseString(dataString)
def parseString(self, dataString):
if not isinstance(dataString, str):
return Constant(dataString),
dataList = []
for data in dataString.split(";"):
if isinstance(data, str) and "-" in data:
# Dealing with a range
dataList.append(Range(data, self.step))
else:
dataList.append(Constant(data))
return dataList
def __iter__(self):
for data in self.data:
for value in data:
yield value
def isConstant(self):
return len(self.data) == 1 and self.data[0].isConstant()
class Constant:
def __init__(self, const):
if isinstance(const, str):
self.value = None if const == "" else float(const)
else:
self.value = const
def __iter__(self):
yield self.value
@staticmethod
def isConstant():
return True
class Range:
def __init__(self, string, step):
start, end = string.split("-")
self.start = float(start)
self.end = float(end)
self.step = step
def __iter__(self):
current = start = self.start
end = self.end
step = self.step or (end - start) / 200
i = 0
while current < end:
current = start + i * step
i += 1
yield current
@staticmethod
def isConstant():
return False

View File

@@ -1,24 +0,0 @@
import math
from logbook import Logger
from eos.graph import Graph
pyfalog = Logger(__name__)
class FitCapAmountTimeGraph(Graph):
defaults = {"time": 0}
def __init__(self, fit, data=None):
Graph.__init__(self, fit, self.calcAmount, data if data is not None else self.defaults)
self.fit = fit
def calcAmount(self, data):
time = data["time"]
maxCap = self.fit.ship.getModifiedItemAttr('capacitorCapacity')
regenTime = self.fit.ship.getModifiedItemAttr('rechargeRate') / 1000
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
cap = maxCap * (1 + math.exp(5 * -time / regenTime) * -1) ** 2
return cap

View File

@@ -1,25 +0,0 @@
import math
from logbook import Logger
from eos.graph import Graph
pyfalog = Logger(__name__)
class FitCapRegenAmountGraph(Graph):
defaults = {"percentage": '0-100'}
def __init__(self, fit, data=None):
Graph.__init__(self, fit, self.calcRegen, data if data is not None else self.defaults)
self.fit = fit
def calcRegen(self, data):
perc = data['percentage']
maxCap = self.fit.ship.getModifiedItemAttr('capacitorCapacity')
regenTime = self.fit.ship.getModifiedItemAttr('rechargeRate') / 1000
currentCap = maxCap * perc / 100
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
regen = 10 * maxCap / regenTime * (math.sqrt(currentCap / maxCap) - currentCap / maxCap)
return regen

View File

@@ -1,26 +0,0 @@
import math
from logbook import Logger
from eos.graph import Graph
pyfalog = Logger(__name__)
class FitDistanceTimeGraph(Graph):
defaults = {"time": 0}
def __init__(self, fit, data=None):
Graph.__init__(self, fit, self.calcDistance, data if data is not None else self.defaults)
self.fit = fit
def calcDistance(self, data):
time = data["time"]
maxSpeed = self.fit.ship.getModifiedItemAttr('maxVelocity')
mass = self.fit.ship.getModifiedItemAttr('mass')
agility = self.fit.ship.getModifiedItemAttr('agility')
# Definite integral of:
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
distance = maxSpeed * time + (maxSpeed * agility * mass * math.exp((-time * 1000000) / (agility * mass)) / 1000000)
return distance

View File

@@ -1,113 +0,0 @@
# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from logbook import Logger
from eos.graph import Graph
from eos.utils.spoolSupport import SpoolType, SpoolOptions
pyfalog = Logger(__name__)
class FitDmgTimeGraph(Graph):
defaults = {"time": 0}
def __init__(self, fit, data=None):
Graph.__init__(self, fit, self.calcDmg, data if data is not None else self.defaults)
self.fit = fit
self.__cache = {}
def calcDmg(self, data):
time = data["time"] * 1000
closestTime = max((t for t in self.__cache if t <= time), default=None)
if closestTime is None:
return 0
return self.__cache[closestTime]
def recalc(self):
def addDmg(addedTime, addedDmg):
if addedDmg == 0:
return
if addedTime not in self.__cache:
prevTime = max((t for t in self.__cache if t < addedTime), default=None)
if prevTime is None:
self.__cache[addedTime] = 0
else:
self.__cache[addedTime] = self.__cache[prevTime]
for time in (t for t in self.__cache if t >= addedTime):
self.__cache[time] += addedDmg
self.__cache.clear()
fit = self.fit
# We'll handle calculations in milliseconds
maxTime = self.data["time"].data[0].end * 1000
for mod in fit.modules:
cycleParams = mod.getCycleParameters(reloadOverride=True)
if cycleParams is None:
continue
currentTime = 0
nonstopCycles = 0
for cycleTime, inactiveTime in cycleParams.iterCycles():
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
for volleyTime, volley in volleyParams.items():
if currentTime + volleyTime <= maxTime and volleyTime <= cycleTime:
addDmg(currentTime + volleyTime, volley.total)
currentTime += cycleTime
currentTime += inactiveTime
if inactiveTime == 0:
nonstopCycles += 1
else:
nonstopCycles = 0
if currentTime > maxTime:
break
for drone in fit.drones:
cycleParams = drone.getCycleParameters(reloadOverride=True)
if cycleParams is None:
continue
currentTime = 0
volleyParams = drone.getVolleyParameters()
for cycleTime, inactiveTime in cycleParams.iterCycles():
for volleyTime, volley in volleyParams.items():
if currentTime + volleyTime <= maxTime and volleyTime <= cycleTime:
addDmg(currentTime + volleyTime, volley.total)
currentTime += cycleTime
currentTime += inactiveTime
if currentTime > maxTime:
break
for fighter in fit.fighters:
cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True)
if cycleParams is None:
continue
volleyParams = fighter.getVolleyParametersPerEffect()
for effectID, abilityCycleParams in cycleParams.items():
if effectID not in volleyParams:
continue
currentTime = 0
abilityVolleyParams = volleyParams[effectID]
for cycleTime, inactiveTime in abilityCycleParams.iterCycles():
for volleyTime, volley in abilityVolleyParams.items():
if currentTime + volleyTime <= maxTime and volleyTime <= cycleTime:
addDmg(currentTime + volleyTime, volley.total)
currentTime += cycleTime
currentTime += inactiveTime
if currentTime > maxTime:
break

View File

@@ -1,209 +0,0 @@
# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from math import exp, log, radians, sin, inf
from logbook import Logger
from eos.const import FittingHardpoint, FittingModuleState
from eos.graph import Graph
pyfalog = Logger(__name__)
class FitDpsRangeGraph(Graph):
defaults = {
"angle" : 0,
"distance" : 0,
"signatureRadius": None,
"velocity" : 0
}
def __init__(self, fit, data=None):
Graph.__init__(self, fit, self.calcDps, data if data is not None else self.defaults)
self.fit = fit
def calcDps(self, data):
ew = {'signatureRadius': [], 'velocity': []}
fit = self.fit
total = 0
distance = data["distance"] * 1000
abssort = lambda _val: -abs(_val - 1)
for mod in fit.modules:
if not mod.isEmpty and mod.state >= FittingModuleState.ACTIVE:
if "remoteTargetPaintFalloff" in mod.item.effects or "structureModuleEffectTargetPainter" in mod.item.effects:
ew['signatureRadius'].append(
1 + (mod.getModifiedItemAttr("signatureRadiusBonus") / 100) * self.calculateModuleMultiplier(
mod, data))
if "remoteWebifierFalloff" in mod.item.effects or "structureModuleEffectStasisWebifier" in mod.item.effects:
if distance <= mod.getModifiedItemAttr("maxRange"):
ew['velocity'].append(1 + (mod.getModifiedItemAttr("speedFactor") / 100))
elif mod.getModifiedItemAttr("falloffEffectiveness") > 0:
# I am affected by falloff
ew['velocity'].append(
1 + (mod.getModifiedItemAttr("speedFactor") / 100) * self.calculateModuleMultiplier(mod,
data))
ew['signatureRadius'].sort(key=abssort)
ew['velocity'].sort(key=abssort)
for attr, values in ew.items():
val = data[attr]
try:
for i in range(len(values)):
bonus = values[i]
val *= 1 + (bonus - 1) * exp(- i ** 2 / 7.1289)
data[attr] = val
except Exception as e:
pyfalog.critical("Caught exception in calcDPS.")
pyfalog.critical(e)
for mod in fit.modules:
dps = mod.getDps(targetResists=fit.targetResists).total
if mod.hardpoint == FittingHardpoint.TURRET:
if mod.state >= FittingModuleState.ACTIVE:
total += dps * self.calculateTurretMultiplier(mod, data)
elif mod.hardpoint == FittingHardpoint.MISSILE:
if mod.state >= FittingModuleState.ACTIVE and mod.maxRange is not None and mod.maxRange >= distance:
total += dps * self.calculateMissileMultiplier(mod, data)
if distance <= fit.extraAttributes["droneControlRange"]:
for drone in fit.drones:
multiplier = 1 if drone.getModifiedItemAttr("maxVelocity") > 1 else self.calculateTurretMultiplier(
drone, data)
dps = drone.getDps(targetResists=fit.targetResists).total
total += dps * multiplier
# this is janky as fuck
for fighter in fit.fighters:
if not fighter.active:
continue
fighterDpsMap = fighter.getDpsPerEffect(targetResists=fit.targetResists)
for ability in fighter.abilities:
if ability.dealsDamage and ability.active:
if ability.effectID not in fighterDpsMap:
continue
multiplier = self.calculateFighterMissileMultiplier(ability, data)
dps = fighterDpsMap[ability.effectID].total
total += dps * multiplier
return total
@staticmethod
def calculateMissileMultiplier(mod, data):
targetSigRad = data["signatureRadius"]
targetVelocity = data["velocity"]
explosionRadius = mod.getModifiedChargeAttr("aoeCloudSize")
targetSigRad = explosionRadius if targetSigRad is None else targetSigRad
explosionVelocity = mod.getModifiedChargeAttr("aoeVelocity")
damageReductionFactor = mod.getModifiedChargeAttr("aoeDamageReductionFactor")
sigRadiusFactor = targetSigRad / explosionRadius
if targetVelocity:
velocityFactor = (explosionVelocity / explosionRadius * targetSigRad / targetVelocity) ** damageReductionFactor
else:
velocityFactor = 1
return min(sigRadiusFactor, velocityFactor, 1)
def calculateTurretMultiplier(self, mod, data):
# Source for most of turret calculation info: http://wiki.eveonline.com/en/wiki/Falloff
chanceToHit = self.calculateTurretChanceToHit(mod, data)
if chanceToHit > 0.01:
# AvgDPS = Base Damage * [ ( ChanceToHit^2 + ChanceToHit + 0.0499 ) / 2 ]
multiplier = (chanceToHit ** 2 + chanceToHit + 0.0499) / 2
else:
# All hits are wreckings
multiplier = chanceToHit * 3
dmgScaling = mod.getModifiedItemAttr("turretDamageScalingRadius")
if dmgScaling:
targetSigRad = data["signatureRadius"]
multiplier = min(1, (float(targetSigRad) / dmgScaling) ** 2)
return multiplier
@staticmethod
def calculateFighterMissileMultiplier(ability, data):
prefix = ability.attrPrefix
targetSigRad = data["signatureRadius"]
targetVelocity = data["velocity"]
explosionRadius = ability.fighter.getModifiedItemAttr("{}ExplosionRadius".format(prefix))
explosionVelocity = ability.fighter.getModifiedItemAttr("{}ExplosionVelocity".format(prefix))
damageReductionFactor = ability.fighter.getModifiedItemAttr("{}ReductionFactor".format(prefix), None)
# the following conditionals are because CCP can't keep a decent naming convention, as if fighter implementation
# wasn't already fucked.
if damageReductionFactor is None:
damageReductionFactor = ability.fighter.getModifiedItemAttr("{}DamageReductionFactor".format(prefix))
damageReductionSensitivity = ability.fighter.getModifiedItemAttr("{}ReductionSensitivity".format(prefix), None)
if damageReductionSensitivity is None:
damageReductionSensitivity = ability.fighter.getModifiedItemAttr(
"{}DamageReductionSensitivity".format(prefix))
targetSigRad = explosionRadius if targetSigRad is None else targetSigRad
sigRadiusFactor = targetSigRad / explosionRadius
if targetVelocity:
velocityFactor = (explosionVelocity / explosionRadius * targetSigRad / targetVelocity) ** (
log(damageReductionFactor) / log(damageReductionSensitivity))
else:
velocityFactor = 1
return min(sigRadiusFactor, velocityFactor, 1)
def calculateTurretChanceToHit(self, mod, data):
distance = data["distance"] * 1000
tracking = mod.getModifiedItemAttr("trackingSpeed")
turretOptimal = mod.maxRange
turretFalloff = mod.falloff
turretSigRes = mod.getModifiedItemAttr("optimalSigRadius")
targetSigRad = data["signatureRadius"]
targetSigRad = turretSigRes if targetSigRad is None else targetSigRad
transversal = sin(radians(data["angle"])) * data["velocity"]
# Angular velocity is calculated using range from ship center to target center.
# We do not know target radius but we know attacker radius
angDistance = distance + self.fit.ship.getModifiedItemAttr('radius', 0)
if angDistance == 0 and transversal == 0:
angularVelocity = 0
elif angDistance == 0 and transversal != 0:
angularVelocity = inf
else:
angularVelocity = transversal / angDistance
trackingEq = (((angularVelocity / tracking) *
(turretSigRes / targetSigRad)) ** 2)
rangeEq = ((max(0, distance - turretOptimal)) / turretFalloff) ** 2
return 0.5 ** (trackingEq + rangeEq)
@staticmethod
def calculateModuleMultiplier(mod, data):
# Simplified formula, we make some assumptions about the module
# This is basically the calculateTurretChanceToHit without tracking values
distance = data["distance"] * 1000
turretOptimal = mod.maxRange
turretFalloff = mod.falloff
rangeEq = ((max(0, distance - turretOptimal)) / turretFalloff) ** 2
return 0.5 ** rangeEq

View File

@@ -1,112 +0,0 @@
# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from logbook import Logger
from eos.graph import Graph
from eos.utils.spoolSupport import SpoolType, SpoolOptions
pyfalog = Logger(__name__)
class FitDpsTimeGraph(Graph):
defaults = {"time": 0}
def __init__(self, fit, data=None):
Graph.__init__(self, fit, self.calcDps, data if data is not None else self.defaults)
self.fit = fit
self.__cache = []
def calcDps(self, data):
time = data["time"] * 1000
entries = (e for e in self.__cache if e[0] <= time < e[1])
dps = sum(e[2] for e in entries)
return dps
def recalc(self):
def addDmg(addedTimeStart, addedTimeFinish, addedDmg):
if addedDmg == 0:
return
addedDps = 1000 * addedDmg / (addedTimeFinish - addedTimeStart)
self.__cache.append((addedTimeStart, addedTimeFinish, addedDps))
self.__cache = []
fit = self.fit
# We'll handle calculations in milliseconds
maxTime = self.data["time"].data[0].end * 1000
for mod in fit.modules:
cycleParams = mod.getCycleParameters(reloadOverride=True)
if cycleParams is None:
continue
currentTime = 0
nonstopCycles = 0
for cycleTime, inactiveTime in cycleParams.iterCycles():
cycleDamage = 0
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
for volleyTime, volley in volleyParams.items():
if currentTime + volleyTime <= maxTime and volleyTime <= cycleTime:
cycleDamage += volley.total
addDmg(currentTime, currentTime + cycleTime, cycleDamage)
currentTime += cycleTime
currentTime += inactiveTime
if inactiveTime > 0:
nonstopCycles = 0
else:
nonstopCycles += 1
if currentTime > maxTime:
break
for drone in fit.drones:
cycleParams = drone.getCycleParameters(reloadOverride=True)
if cycleParams is None:
continue
currentTime = 0
for cycleTime, inactiveTime in cycleParams.iterCycles():
cycleDamage = 0
volleyParams = drone.getVolleyParameters()
for volleyTime, volley in volleyParams.items():
if currentTime + volleyTime <= maxTime and volleyTime <= cycleTime:
cycleDamage += volley.total
addDmg(currentTime, currentTime + cycleTime, cycleDamage)
currentTime += cycleTime
currentTime += inactiveTime
if currentTime > maxTime:
break
for fighter in fit.fighters:
cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True)
if cycleParams is None:
continue
volleyParams = fighter.getVolleyParametersPerEffect()
for effectID, abilityCycleParams in cycleParams.items():
if effectID not in volleyParams:
continue
abilityVolleyParams = volleyParams[effectID]
currentTime = 0
for cycleTime, inactiveTime in abilityCycleParams.iterCycles():
cycleDamage = 0
for volleyTime, volley in abilityVolleyParams.items():
if currentTime + volleyTime <= maxTime and volleyTime <= cycleTime:
cycleDamage += volley.total
addDmg(currentTime, currentTime + cycleTime, cycleDamage)
currentTime += cycleTime
currentTime += inactiveTime
if currentTime > maxTime:
break

View File

@@ -1,29 +0,0 @@
import math
from logbook import Logger
from eos.graph import Graph
pyfalog = Logger(__name__)
class FitShieldAmountTimeGraph(Graph):
defaults = {"time": 0}
def __init__(self, fit, data=None):
Graph.__init__(self, fit, self.calcAmount, data if data is not None else self.defaults)
self.fit = fit
import gui.mainFrame
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def calcAmount(self, data):
time = data["time"]
maxShield = self.fit.ship.getModifiedItemAttr('shieldCapacity')
regenTime = self.fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate (shield is similar to cap)
shield = maxShield * (1 + math.exp(5 * -time / regenTime) * -1) ** 2
useEhp = self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective
if self.fit.damagePattern is not None and useEhp:
shield = self.fit.damagePattern.effectivify(self.fit, shield, 'shield')
return shield

View File

@@ -1,49 +0,0 @@
# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
import math
from logbook import Logger
from eos.graph import Graph
pyfalog = Logger(__name__)
class FitShieldRegenAmountGraph(Graph):
defaults = {"percentage": '0-100'}
def __init__(self, fit, data=None):
Graph.__init__(self, fit, self.calcRegen, data if data is not None else self.defaults)
self.fit = fit
import gui.mainFrame
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def calcRegen(self, data):
perc = data["percentage"]
maxShield = self.fit.ship.getModifiedItemAttr('shieldCapacity')
regenTime = self.fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000
currentShield = maxShield * perc / 100
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate (shield is similar to cap)
regen = 10 * maxShield / regenTime * (math.sqrt(currentShield / maxShield) - currentShield / maxShield)
useEhp = self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective
if self.fit.damagePattern is not None and useEhp:
regen = self.fit.damagePattern.effectivify(self.fit, regen, 'shield')
return regen

View File

@@ -1,25 +0,0 @@
import math
from logbook import Logger
from eos.graph import Graph
pyfalog = Logger(__name__)
class FitSpeedTimeGraph(Graph):
defaults = {"time": 0}
def __init__(self, fit, data=None):
Graph.__init__(self, fit, self.calcSpeed, data if data is not None else self.defaults)
self.fit = fit
def calcSpeed(self, data):
time = data["time"]
maxSpeed = self.fit.ship.getModifiedItemAttr('maxVelocity')
mass = self.fit.ship.getModifiedItemAttr('mass')
agility = self.fit.ship.getModifiedItemAttr('agility')
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
speed = maxSpeed * (1 - math.exp((-time * 1000000) / (agility * mass)))
return speed

View File

@@ -1,61 +0,0 @@
import math
from logbook import Logger
from eos.graph import Graph
pyfalog = Logger(__name__)
AU_METERS = 149597870700
class FitWarpTimeDistanceGraph(Graph):
defaults = {"distance": 0}
def __init__(self, fit, data=None):
Graph.__init__(self, fit, self.calcTime, data if data is not None else self.defaults)
self.fit = fit
def calcTime(self, data):
distance = data["distance"]
if distance == 0:
return 0
maxWarpDistance = self.fit.maxWarpDistance
if distance > maxWarpDistance:
return None
maxSubwarpSpeed = self.fit.ship.getModifiedItemAttr('maxVelocity')
maxWarpSpeed = self.fit.warpSpeed
time = calculate_time_in_warp(maxWarpSpeed, maxSubwarpSpeed, distance * AU_METERS)
return time
# Taken from https://wiki.eveuniversity.org/Warp_time_calculation#Implementation
# with minor modifications
# Warp speed in AU/s, subwarp speed in m/s, distance in m
def calculate_time_in_warp(max_warp_speed, max_subwarp_speed, warp_dist):
k_accel = max_warp_speed
k_decel = min(max_warp_speed / 3, 2)
warp_dropout_speed = max_subwarp_speed / 2
max_ms_warp_speed = max_warp_speed * AU_METERS
accel_dist = AU_METERS
decel_dist = max_ms_warp_speed / k_decel
minimum_dist = accel_dist + decel_dist
cruise_time = 0
if minimum_dist > warp_dist:
max_ms_warp_speed = warp_dist * k_accel * k_decel / (k_accel + k_decel)
else:
cruise_time = (warp_dist - minimum_dist) / max_ms_warp_speed
accel_time = math.log(max_ms_warp_speed / k_accel) / k_accel
decel_time = math.log(max_ms_warp_speed / warp_dropout_speed) / k_decel
total_time = cruise_time + accel_time + decel_time
return total_time

View File

@@ -18,49 +18,88 @@
# ===============================================================================
import collections
from copy import copy
from math import exp
from eos.const import Operator
# TODO: This needs to be moved out, we shouldn't have *ANY* dependencies back to other modules/methods inside eos.
# This also breaks writing any tests. :(
from eos.db.gamedata.queries import getAttributeInfo
defaultValuesCache = {}
cappingAttrKeyCache = {}
resistanceCache = {}
class ItemAttrShortcut(object):
def getAttrDefault(key, fallback=None):
try:
default = defaultValuesCache[key]
except KeyError:
attrInfo = getAttributeInfo(key)
if attrInfo is None:
default = defaultValuesCache[key] = None
else:
default = defaultValuesCache[key] = attrInfo.defaultValue
if default is None:
default = fallback
return default
def getResistanceAttrID(modifyingItem, effect):
# If it doesn't exist on the effect, check the modifying module's attributes.
# If it's there, cache it and return
if effect.resistanceID:
return effect.resistanceID
cacheKey = (modifyingItem.item.ID, effect.ID)
try:
return resistanceCache[cacheKey]
except KeyError:
attrPrefix = effect.getattr('prefix')
if attrPrefix:
resistanceID = int(modifyingItem.getModifiedItemAttr('{}ResistanceID'.format(attrPrefix))) or None
if not resistanceID:
resistanceID = int(modifyingItem.getModifiedItemAttr('{}RemoteResistanceID'.format(attrPrefix))) or None
else:
resistanceID = int(modifyingItem.getModifiedItemAttr("remoteResistanceID")) or None
resistanceCache[cacheKey] = resistanceID
return resistanceID
class ItemAttrShortcut:
def getModifiedItemAttr(self, key, default=0):
return_value = self.itemModifiedAttributes.get(key)
return return_value or default
def getModifiedItemAttrExtended(self, key, extraMultipliers=None, ignoreAfflictors=(), default=0):
return_value = self.itemModifiedAttributes.getExtended(key, extraMultipliers=extraMultipliers, ignoreAfflictors=ignoreAfflictors)
return return_value or default
def getItemBaseAttrValue(self, key, default=0):
"""
Gets base value in this order:
Mutated value > override value > attribute value
"""
return_value = self.itemModifiedAttributes.getOriginal(key)
return return_value or default
def getChargeBaseAttrValue(self, key, default=0):
"""
Gets base value in this order:
Mutated value > override value > attribute value
"""
return_value = self.chargeModifiedAttributes.getOriginal(key)
return return_value or default
class ChargeAttrShortcut:
class ChargeAttrShortcut(object):
def getModifiedChargeAttr(self, key, default=0):
return_value = self.chargeModifiedAttributes.get(key)
return return_value or default
def getModifiedChargeAttrExtended(self, key, extraMultipliers=None, ignoreAfflictors=(), default=0):
return_value = self.chargeModifiedAttributes.getExtended(key, extraMultipliers=extraMultipliers, ignoreAfflictors=ignoreAfflictors)
return return_value or default
def getChargeBaseAttrValue(self, key, default=0):
return_value = self.chargeModifiedAttributes.getOriginal(key)
return return_value or default
class ModifiedAttributeDict(collections.MutableMapping):
overrides_enabled = False
class CalculationPlaceholder(object):
class CalculationPlaceholder:
def __init__(self):
pass
@@ -74,6 +113,10 @@ class ModifiedAttributeDict(collections.MutableMapping):
# Final modified values
self.__modified = {}
# Affected by entities
# Format:
# {attr name: {modifying fit: (
# modifying item, operation, stacking group, pre-resist amount,
# post-resist amount, affects result or not)}}
self.__affectedBy = {}
# Overrides (per item)
self.__overrides = {}
@@ -144,24 +187,74 @@ class ModifiedAttributeDict(collections.MutableMapping):
def __getitem__(self, key):
# Check if we have final calculated value
key_value = self.__modified.get(key)
if key_value is self.CalculationPlaceholder:
key_value = self.__modified[key] = self.__calculateValue(key)
if key_value is not None:
return key_value
val = self.__modified.get(key)
if val is self.CalculationPlaceholder:
val = self.__modified[key] = self.__calculateValue(key)
if val is not None:
return val
# Then in values which are not yet calculated
if self.__intermediary:
val = self.__intermediary.get(key)
else:
val = None
if val is not None:
return val
# Original value is the least priority
return self.getOriginal(key)
def getExtended(self, key, extraMultipliers=None, ignoreAfflictors=None, default=0):
"""
Here we consider couple of parameters. If they affect final result, we do
not store result, and if they are - we do.
"""
# Here we do not have support for preAssigns/forceds, as doing them would
# mean that we have to store all of them in a list which increases memory use,
# and we do not actually need those operators atm
preIncreaseAdjustment = 0
multiplierAdjustment = 1
ignorePenalizedMultipliers = {}
postIncreaseAdjustment = 0
for fit, afflictors in self.getAfflictions(key).items():
for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in afflictors:
if afflictor in ignoreAfflictors:
if operator == Operator.MULTIPLY:
if stackingGroup is None:
multiplierAdjustment /= postResAmount
else:
ignorePenalizedMultipliers.setdefault(stackingGroup, []).append(postResAmount)
elif operator == Operator.PREINCREASE:
preIncreaseAdjustment -= postResAmount
elif operator == Operator.POSTINCREASE:
postIncreaseAdjustment -= postResAmount
# If we apply no customizations - use regular getter
if (
not extraMultipliers and
preIncreaseAdjustment == 0 and multiplierAdjustment == 1 and
postIncreaseAdjustment == 0 and len(ignorePenalizedMultipliers) == 0
):
return self.get(key, default=default)
# Try to calculate custom values
val = self.__calculateValue(
key, extraMultipliers=extraMultipliers, preIncAdj=preIncreaseAdjustment, multAdj=multiplierAdjustment,
postIncAdj=postIncreaseAdjustment, ignorePenMult=ignorePenalizedMultipliers)
if val is not None:
return val
# Then the same fallbacks as in regular getter
if self.__intermediary:
val = self.__intermediary.get(key)
else:
# Original value is the least priority
return self.getOriginal(key)
val = None
if val is not None:
return val
val = self.getOriginal(key)
if val is not None:
return val
return default
def __delitem__(self, key):
if key in self.__modified:
@@ -181,6 +274,9 @@ class ModifiedAttributeDict(collections.MutableMapping):
if self.original:
val = self.original.get(key, val)
if val is None:
val = getAttrDefault(key, fallback=None)
if val is None and val != default:
val = default
@@ -208,7 +304,7 @@ class ModifiedAttributeDict(collections.MutableMapping):
keys.update(iter(self.__intermediary.keys()))
return len(keys)
def __calculateValue(self, key):
def __calculateValue(self, key, extraMultipliers=None, preIncAdj=None, multAdj=None, postIncAdj=None, ignorePenMult=None):
# It's possible that various attributes are capped by other attributes,
# it's defined by reference maxAttributeID
try:
@@ -245,29 +341,52 @@ class ModifiedAttributeDict(collections.MutableMapping):
preIncrease = self.__preIncreases.get(key, 0)
multiplier = self.__multipliers.get(key, 1)
penalizedMultiplierGroups = self.__penalizedMultipliers.get(key, {})
# Add extra multipliers to the group, not modifying initial data source
if extraMultipliers is not None:
penalizedMultiplierGroups = copy(penalizedMultiplierGroups)
for stackGroup, operationsData in extraMultipliers.items():
multipliers = []
for mult, resAttrID in operationsData:
if not resAttrID:
multipliers.append(mult)
continue
resAttrInfo = getAttributeInfo(resAttrID)
if not resAttrInfo:
multipliers.append(mult)
continue
resMult = self.fit.ship.itemModifiedAttributes[resAttrInfo.attributeName]
if resMult is None or resMult == 1:
multipliers.append(mult)
continue
mult = (mult - 1) * resMult + 1
multipliers.append(mult)
penalizedMultiplierGroups[stackGroup] = penalizedMultiplierGroups.get(stackGroup, []) + multipliers
postIncrease = self.__postIncreases.get(key, 0)
# Grab initial value, priorities are:
# Results of ongoing calculation > preAssign > original > 0
try:
default = defaultValuesCache[key]
except KeyError:
attrInfo = getAttributeInfo(key)
if attrInfo is None:
default = defaultValuesCache[key] = 0.0
else:
dv = attrInfo.defaultValue
default = defaultValuesCache[key] = dv if dv is not None else 0.0
default = getAttrDefault(key, fallback=0.0)
val = self.__intermediary.get(key, self.__preAssigns.get(key, self.getOriginal(key, default)))
# We'll do stuff in the following order:
# preIncrease > multiplier > stacking penalized multipliers > postIncrease
val += preIncrease
if preIncAdj is not None:
val += preIncAdj
val *= multiplier
if multAdj is not None:
val *= multAdj
# Each group is penalized independently
# Things in different groups will not be stack penalized between each other
for penalizedMultipliers in penalizedMultiplierGroups.values():
for penaltyGroup, penalizedMultipliers in penalizedMultiplierGroups.items():
if ignorePenMult is not None and penaltyGroup in ignorePenMult:
# Avoid modifying source and remove multipliers we were asked to remove for this calc
penalizedMultipliers = penalizedMultipliers[:]
for ignoreMult in ignorePenMult[penaltyGroup]:
try:
penalizedMultipliers.remove(ignoreMult)
except ValueError:
pass
# A quick explanation of how this works:
# 1: Bonuses and penalties are calculated seperately, so we'll have to filter each of them
l1 = [_val for _val in penalizedMultipliers if _val > 1]
@@ -285,6 +404,8 @@ class ModifiedAttributeDict(collections.MutableMapping):
bonus = l[i]
val *= 1 + (bonus - 1) * exp(- i ** 2 / 7.1289)
val += postIncrease
if postIncAdj is not None:
val += postIncAdj
# Cap value if we have cap defined
if cappingValue is not None:
@@ -311,7 +432,7 @@ class ModifiedAttributeDict(collections.MutableMapping):
def iterAfflictions(self):
return self.__affectedBy.__iter__()
def __afflict(self, attributeName, operation, bonus, used=True):
def __afflict(self, attributeName, operator, stackingGroup, preResAmount, postResAmount, used=True):
"""Add modifier to list of things affecting current item"""
# Do nothing if no fit is assigned
fit = self.fit
@@ -337,13 +458,13 @@ class ModifiedAttributeDict(collections.MutableMapping):
modifier = fit.getModifier()
# Add current affliction to list
affs.append((modifier, operation, bonus, used))
affs.append((modifier, operator, stackingGroup, preResAmount, postResAmount, used))
def preAssign(self, attributeName, value):
def preAssign(self, attributeName, value, **kwargs):
"""Overwrites original value of the entity with given one, allowing further modification"""
self.__preAssigns[attributeName] = value
self.__placehold(attributeName)
self.__afflict(attributeName, "=", value, value != self.getOriginal(attributeName))
self.__afflict(attributeName, Operator.PREASSIGN, None, value, value, value != self.getOriginal(attributeName))
def increase(self, attributeName, increase, position="pre", skill=None, **kwargs):
"""Increase value of given attribute by given number"""
@@ -356,8 +477,10 @@ class ModifiedAttributeDict(collections.MutableMapping):
# Increases applied before multiplications and after them are
# written in separate maps
if position == "pre":
operator = Operator.PREINCREASE
tbl = self.__preIncreases
elif position == "post":
operator = Operator.POSTINCREASE
tbl = self.__postIncreases
else:
raise ValueError("position should be either pre or post")
@@ -365,9 +488,9 @@ class ModifiedAttributeDict(collections.MutableMapping):
tbl[attributeName] = 0
tbl[attributeName] += increase
self.__placehold(attributeName)
self.__afflict(attributeName, "+", increase, increase != 0)
self.__afflict(attributeName, operator, None, increase, increase, increase != 0)
def multiply(self, attributeName, multiplier, stackingPenalties=False, penaltyGroup="default", skill=None, resist=True, *args, **kwargs):
def multiply(self, attributeName, multiplier, stackingPenalties=False, penaltyGroup="default", skill=None, **kwargs):
"""Multiply value of given attribute by given factor"""
if multiplier is None: # See GH issue 397
return
@@ -375,6 +498,15 @@ class ModifiedAttributeDict(collections.MutableMapping):
if skill:
multiplier *= self.__handleSkill(skill)
preResMultiplier = multiplier
resisted = False
# Goddammit CCP, make up your mind where you want this information >.< See #1139
if 'effect' in kwargs:
resistFactor = ModifiedAttributeDict.getResistance(self.fit, kwargs['effect']) or 1
if resistFactor != 1:
resisted = True
multiplier = (multiplier - 1) * resistFactor + 1
# If we're asked to do stacking penalized multiplication, append values
# to per penalty group lists
if stackingPenalties:
@@ -395,53 +527,46 @@ class ModifiedAttributeDict(collections.MutableMapping):
afflictPenal = ""
if stackingPenalties:
afflictPenal += "s"
if resist:
if resisted:
afflictPenal += "r"
self.__afflict(attributeName, "%s*" % afflictPenal, multiplier, multiplier != 1)
self.__afflict(
attributeName, Operator.MULTIPLY, penaltyGroup if stackingPenalties else None,
preResMultiplier, multiplier, multiplier != 1)
def boost(self, attributeName, boostFactor, skill=None, *args, **kwargs):
def boost(self, attributeName, boostFactor, skill=None, **kwargs):
"""Boost value by some percentage"""
if skill:
boostFactor *= self.__handleSkill(skill)
resist = None
# Goddammit CCP, make up your mind where you want this information >.< See #1139
if 'effect' in kwargs:
resist = ModifiedAttributeDict.getResistance(self.fit, kwargs['effect']) or 1
boostFactor *= resist
# We just transform percentage boost into multiplication factor
self.multiply(attributeName, 1 + boostFactor / 100.0, resist=(True if resist else False), *args, **kwargs)
self.multiply(attributeName, 1 + boostFactor / 100.0, **kwargs)
def force(self, attributeName, value):
def force(self, attributeName, value, **kwargs):
"""Force value to attribute and prohibit any changes to it"""
self.__forced[attributeName] = value
self.__placehold(attributeName)
self.__afflict(attributeName, "\u2263", value)
self.__afflict(attributeName, Operator.FORCE, None, value, value)
@staticmethod
def getResistance(fit, effect):
remoteResistID = effect.resistanceID
# If it doesn't exist on the effect, check the modifying modules attributes. If it's there, set it on the
# effect for this session so that we don't have to look here again (won't always work when it's None, but
# will catch most)
# Resistances are applicable only to projected effects
if isinstance(effect.type, (tuple, list)):
effectType = effect.type
else:
effectType = (effect.type,)
if 'projected' not in effectType:
return 1
remoteResistID = getResistanceAttrID(modifyingItem=fit.getModifier(), effect=effect)
if not remoteResistID:
mod = fit.getModifier()
effect.resistanceID = int(mod.getModifiedItemAttr("remoteResistanceID")) or None
remoteResistID = effect.resistanceID
return 1
attrInfo = getAttributeInfo(remoteResistID)
# Get the attribute of the resist
resist = fit.ship.itemModifiedAttributes[attrInfo.attributeName] or None
return resist or 1.0
return resist or 1
class Affliction(object):
class Affliction:
def __init__(self, affliction_type, amount):
self.type = affliction_type
self.amount = amount

View File

@@ -24,7 +24,7 @@ from sqlalchemy.orm import reconstructor
pyfalog = Logger(__name__)
class BoosterSideEffect(object):
class BoosterSideEffect:
def __init__(self, effect):
"""Initialize from the program"""

View File

@@ -32,7 +32,7 @@ from eos.effectHandlerHelpers import HandledItem, HandledImplantList
pyfalog = Logger(__name__)
class Character(object):
class Character:
__itemList = None
__itemIDMap = None
__itemNameMap = None

View File

@@ -21,7 +21,7 @@ import re
import eos.db
class DamagePattern(object):
class DamagePattern:
DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive")
def __init__(self, *args, **kwargs):

View File

@@ -25,7 +25,7 @@ import eos.db
from eos.effectHandlerHelpers import HandledCharge, HandledItem
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
from eos.utils.cycles import CycleInfo
from eos.utils.stats import DmgTypes
from eos.utils.stats import DmgTypes, RRTypes
pyfalog = Logger(__name__)
@@ -68,7 +68,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
""" Build object. Assumes proper and valid item already set """
self.__charge = None
self.__baseVolley = None
self.__baseRemoteReps = None
self.__baseRRAmount = None
self.__miningyield = None
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.__item.attributes
@@ -131,7 +131,14 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def hasAmmo(self):
return self.charge is not None
def getVolleyParameters(self, targetResists=None):
def isDealingDamage(self):
volleyParams = self.getVolleyParameters()
for volley in volleyParams.values():
if volley.total > 0:
return True
return False
def getVolleyParameters(self, targetProfile=None):
if not self.dealsDamage or self.amountActive <= 0:
return {0: DmgTypes(0, 0, 0, 0)}
if self.__baseVolley is None:
@@ -143,17 +150,17 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
volley = DmgTypes(
em=self.__baseVolley.em * (1 - getattr(targetResists, "emAmount", 0)),
thermal=self.__baseVolley.thermal * (1 - getattr(targetResists, "thermalAmount", 0)),
kinetic=self.__baseVolley.kinetic * (1 - getattr(targetResists, "kineticAmount", 0)),
explosive=self.__baseVolley.explosive * (1 - getattr(targetResists, "explosiveAmount", 0)))
em=self.__baseVolley.em * (1 - getattr(targetProfile, "emAmount", 0)),
thermal=self.__baseVolley.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=self.__baseVolley.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=self.__baseVolley.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)))
return {0: volley}
def getVolley(self, targetResists=None):
return self.getVolleyParameters(targetResists=targetResists)[0]
def getVolley(self, targetProfile=None):
return self.getVolleyParameters(targetProfile=targetProfile)[0]
def getDps(self, targetResists=None):
volley = self.getVolley(targetResists=targetResists)
def getDps(self, targetProfile=None):
volley = self.getVolley(targetProfile=targetProfile)
if not volley:
return DmgTypes(0, 0, 0, 0)
cycleParams = self.getCycleParameters()
@@ -167,41 +174,52 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
explosive=volley.explosive * dpsFactor)
return dps
def isRemoteRepping(self, ignoreState=False):
repParams = self.getRepAmountParameters(ignoreState=ignoreState)
for rrData in repParams.values():
if rrData:
return True
return False
def getRepAmountParameters(self, ignoreState=False):
amount = self.amount if ignoreState else self.amountActive
if amount <= 0:
return {}
if self.__baseRRAmount is None:
self.__baseRRAmount = {}
hullAmount = self.getModifiedItemAttr("structureDamageAmount", 0)
armorAmount = self.getModifiedItemAttr("armorDamageAmount", 0)
shieldAmount = self.getModifiedItemAttr("shieldBonus", 0)
if shieldAmount:
self.__baseRRAmount[0] = RRTypes(
shield=shieldAmount * amount,
armor=0, hull=0, capacitor=0)
if armorAmount or hullAmount:
self.__baseRRAmount[self.cycleTime] = RRTypes(
shield=0, armor=armorAmount * amount,
hull=hullAmount * amount, capacitor=0)
return self.__baseRRAmount
def getRemoteReps(self, ignoreState=False):
rrDuringCycle = RRTypes(0, 0, 0, 0)
cycleParams = self.getCycleParameters()
if cycleParams is None:
return rrDuringCycle
repAmountParams = self.getRepAmountParameters(ignoreState=ignoreState)
avgCycleTime = cycleParams.averageTime
if len(repAmountParams) == 0 or avgCycleTime == 0:
return rrDuringCycle
for rrAmount in repAmountParams.values():
rrDuringCycle += rrAmount
rrFactor = 1 / (avgCycleTime / 1000)
rrDuringCycle *= rrFactor
return rrDuringCycle
def getCycleParameters(self, reloadOverride=None):
cycleTime = self.cycleTime
if cycleTime == 0:
return None
return CycleInfo(self.cycleTime, 0, math.inf)
def getRemoteReps(self, ignoreState=False):
if self.amountActive <= 0 and not ignoreState:
return (None, 0)
if self.__baseRemoteReps is None:
rrShield = self.getModifiedItemAttr("shieldBonus", 0)
rrArmor = self.getModifiedItemAttr("armorDamageAmount", 0)
rrHull = self.getModifiedItemAttr("structureDamageAmount", 0)
if rrShield:
rrType = "Shield"
rrAmount = rrShield
elif rrArmor:
rrType = "Armor"
rrAmount = rrArmor
elif rrHull:
rrType = "Hull"
rrAmount = rrHull
else:
rrType = None
rrAmount = 0
if rrAmount:
droneAmount = self.amount if ignoreState else self.amountActive
cycleParams = self.getCycleParameters()
if cycleParams is None:
rrType = None
rrAmount = 0
else:
rrAmount *= droneAmount / (cycleParams.averageTime / 1000)
self.__baseRemoteReps = (rrType, rrAmount)
return self.__baseRemoteReps
return CycleInfo(self.cycleTime, 0, math.inf, False)
@property
def miningStats(self):
@@ -263,7 +281,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def clear(self):
self.__baseVolley = None
self.__baseRemoteReps = None
self.__baseRRAmount = None
self.__miningyield = None
self.itemModifiedAttributes.clear()
self.chargeModifiedAttributes.clear()
@@ -301,11 +319,17 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
projected is False and effect.isType("passive")):
# See GH issue #765
if effect.getattr('grouped'):
effect.handler(fit, self, context)
try:
effect.handler(fit, self, context, effect=effect)
except:
effect.handler(fit, self, context)
else:
i = 0
while i != self.amountActive:
effect.handler(fit, self, context)
try:
effect.handler(fit, self, context, effect=effect)
except:
effect.handler(fit, self, context)
i += 1
if self.charge:

View File

@@ -51,7 +51,7 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
# -1 is a placeholder that represents max squadron size, which we may not know yet as ships may modify this with
# their effects. If user changes this, it is then overridden with user value.
self.amount = -1
self._amount = -1
self.__abilities = self.__getAbilities()
@@ -136,12 +136,15 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return self.__slot
@property
def amountActive(self):
return int(self.getModifiedItemAttr("fighterSquadronMaxSize")) if self.amount == -1 else self.amount
def amount(self):
return int(self.getModifiedItemAttr("fighterSquadronMaxSize")) if self._amount == -1 else self._amount
@amountActive.setter
def amountActive(self, i):
self.amount = int(max(min(i, self.getModifiedItemAttr("fighterSquadronMaxSize")), 0))
@amount.setter
def amount(self, amount):
amount = max(0, int(amount))
if amount >= self.getModifiedItemAttr("fighterSquadronMaxSize"):
amount = -1
self._amount = amount
@property
def fighterSquadronMaxSize(self):
@@ -175,8 +178,16 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def hasAmmo(self):
return self.charge is not None
def getVolleyParametersPerEffect(self, targetResists=None):
if not self.active or self.amountActive <= 0:
def isDealingDamage(self):
volleyParams = self.getVolleyParametersPerEffect()
for effectData in volleyParams.values():
for volley in effectData.values():
if volley.total > 0:
return True
return False
def getVolleyParametersPerEffect(self, targetProfile=None):
if not self.active or self.amount <= 0:
return {}
if self.__baseVolley is None:
self.__baseVolley = {}
@@ -188,14 +199,21 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
adjustedVolley[effectID] = {}
for volleyTime, volleyValue in effectData.items():
adjustedVolley[effectID][volleyTime] = DmgTypes(
em=volleyValue.em * (1 - getattr(targetResists, "emAmount", 0)),
thermal=volleyValue.thermal * (1 - getattr(targetResists, "thermalAmount", 0)),
kinetic=volleyValue.kinetic * (1 - getattr(targetResists, "kineticAmount", 0)),
explosive=volleyValue.explosive * (1 - getattr(targetResists, "explosiveAmount", 0)))
em=volleyValue.em * (1 - getattr(targetProfile, "emAmount", 0)),
thermal=volleyValue.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=volleyValue.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=volleyValue.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)))
return adjustedVolley
def getVolley(self, targetResists=None):
volleyParams = self.getVolleyParametersPerEffect(targetResists=targetResists)
def getVolleyPerEffect(self, targetProfile=None):
volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile)
volleyMap = {}
for effectID, volleyData in volleyParams.items():
volleyMap[effectID] = volleyData[0]
return volleyMap
def getVolley(self, targetProfile=None):
volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile)
em = 0
therm = 0
kin = 0
@@ -207,30 +225,30 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
exp += volleyData[0].explosive
return DmgTypes(em, therm, kin, exp)
def getDps(self, targetResists=None):
def getDps(self, targetProfile=None):
em = 0
thermal = 0
kinetic = 0
explosive = 0
for dps in self.getDpsPerEffect(targetResists=targetResists).values():
for dps in self.getDpsPerEffect(targetProfile=targetProfile).values():
em += dps.em
thermal += dps.thermal
kinetic += dps.kinetic
explosive += dps.explosive
return DmgTypes(em=em, thermal=thermal, kinetic=kinetic, explosive=explosive)
def getDpsPerEffect(self, targetResists=None):
if not self.active or self.amountActive <= 0:
def getDpsPerEffect(self, targetProfile=None):
if not self.active or self.amount <= 0:
return {}
cycleParams = self.getCycleParametersPerEffectOptimizedDps(targetResists=targetResists)
cycleParams = self.getCycleParametersPerEffectOptimizedDps(targetProfile=targetProfile)
dpsMap = {}
for ability in self.abilities:
if ability.effectID in cycleParams:
cycleTime = cycleParams[ability.effectID].averageTime
dpsMap[ability.effectID] = ability.getDps(targetResists=targetResists, cycleTimeOverride=cycleTime)
dpsMap[ability.effectID] = ability.getDps(targetProfile=targetProfile, cycleTimeOverride=cycleTime)
return dpsMap
def getCycleParametersPerEffectOptimizedDps(self, targetResists=None, reloadOverride=None):
def getCycleParametersPerEffectOptimizedDps(self, targetProfile=None, reloadOverride=None):
cycleParamsInfinite = self.getCycleParametersPerEffectInfinite()
cycleParamsReload = self.getCycleParametersPerEffect(reloadOverride=reloadOverride)
dpsMapOnlyInfinite = {}
@@ -239,25 +257,28 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
for ability in self.abilities:
if ability.effectID in cycleParamsInfinite:
cycleTime = cycleParamsInfinite[ability.effectID].averageTime
dpsMapOnlyInfinite[ability.effectID] = ability.getDps(targetResists=targetResists, cycleTimeOverride=cycleTime)
dpsMapOnlyInfinite[ability.effectID] = ability.getDps(targetProfile=targetProfile, cycleTimeOverride=cycleTime)
if ability.effectID in cycleParamsReload:
cycleTime = cycleParamsReload[ability.effectID].averageTime
dpsMapAllWithReloads[ability.effectID] = ability.getDps(targetResists=targetResists, cycleTimeOverride=cycleTime)
dpsMapAllWithReloads[ability.effectID] = ability.getDps(targetProfile=targetProfile, cycleTimeOverride=cycleTime)
totalOnlyInfinite = sum(i.total for i in dpsMapOnlyInfinite.values())
totalAllWithReloads = sum(i.total for i in dpsMapAllWithReloads.values())
return cycleParamsInfinite if totalOnlyInfinite >= totalAllWithReloads else cycleParamsReload
def getCycleParametersPerEffectInfinite(self):
return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.numShots == 0 and a.cycleTime > 0}
return {
a.effectID: CycleInfo(a.cycleTime, 0, math.inf, False)
for a in self.abilities
if a.numShots == 0 and a.cycleTime > 0}
def getCycleParametersPerEffect(self, reloadOverride=None):
factorReload = reloadOverride if reloadOverride is not None else self.owner.factorReload
# Assume it can cycle infinitely
if not factorReload:
return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.cycleTime > 0}
return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf, False) for a in self.abilities if a.cycleTime > 0}
limitedAbilities = [a for a in self.abilities if a.numShots > 0 and a.cycleTime > 0]
if len(limitedAbilities) == 0:
return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.cycleTime > 0}
return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf, False) for a in self.abilities if a.cycleTime > 0}
validAbilities = [a for a in self.abilities if a.cycleTime > 0]
if len(validAbilities) == 0:
return {}
@@ -285,13 +306,13 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
sequence = []
if extraShotTime is not None:
if regularShots > 0:
sequence.append(CycleInfo(ability.cycleTime, 0, regularShots))
sequence.append(CycleInfo(extraShotTime, refuelTime, 1))
sequence.append(CycleInfo(ability.cycleTime, 0, regularShots, False))
sequence.append(CycleInfo(extraShotTime, refuelTime, 1, True))
else:
regularShotsNonReload = regularShots - 1
if regularShotsNonReload > 0:
sequence.append(CycleInfo(ability.cycleTime, 0, regularShotsNonReload))
sequence.append(CycleInfo(ability.cycleTime, refuelTime, 1))
sequence.append(CycleInfo(ability.cycleTime, 0, regularShotsNonReload, False))
sequence.append(CycleInfo(ability.cycleTime, refuelTime, 1, True))
cycleParams[ability.effectID] = CycleSequence(sequence, math.inf)
return cycleParams
@@ -321,7 +342,7 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if falloff is not None:
return falloff
@validates("ID", "itemID", "chargeID", "amount", "amountActive")
@validates("ID", "itemID", "chargeID", "amount")
def validator(self, key, val):
map = {
"ID" : lambda _val: isinstance(_val, int),
@@ -329,7 +350,6 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
"chargeID": lambda _val: isinstance(_val, int),
"amount" : lambda _val: isinstance(_val, int) and _val >= -1,
}
if not map[key](val):
raise ValueError(str(val) + " is not a valid value for " + key)
else:
@@ -379,16 +399,22 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if effect.runTime == runTime and effect.activeByDefault and \
((projected and effect.isType("projected")) or not projected):
if ability.grouped:
effect.handler(fit, self, context)
try:
effect.handler(fit, self, context, effect=effect)
except:
effect.handler(fit, self, context)
else:
i = 0
while i != self.amountActive:
effect.handler(fit, self, context)
while i != self.amount:
try:
effect.handler(fit, self, context, effect=effect)
except:
effect.handler(fit, self, context)
i += 1
def __deepcopy__(self, memo):
copy = Fighter(self.item)
copy.amount = self.amount
copy._amount = self._amount
copy.active = self.active
for ability in self.abilities:
copyAbility = next(filter(lambda a: a.effectID == ability.effectID, copy.abilities))
@@ -396,11 +422,11 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return copy
def rebase(self, item):
amount = self.amount
amount = self._amount
active = self.active
abilityEffectStates = {a.effectID: a.active for a in self.abilities}
Fighter.__init__(self, item)
self.amount = amount
self._amount = amount
self.active = active
for ability in self.abilities:
if ability.effectID in abilityEffectStates:

View File

@@ -26,7 +26,7 @@ from eos.utils.stats import DmgTypes
pyfalog = Logger(__name__)
class FighterAbility(object):
class FighterAbility:
# We aren't able to get data on the charges that can be stored with fighters. So we hardcode that data here, keyed
# with the fighter squadron role
@@ -114,7 +114,7 @@ class FighterAbility(object):
speed = self.fighter.getModifiedItemAttr("{}Duration".format(self.attrPrefix))
return speed
def getVolley(self, targetResists=None):
def getVolley(self, targetProfile=None):
if not self.dealsDamage or not self.active:
return DmgTypes(0, 0, 0, 0)
if self.attrPrefix == "fighterAbilityLaunchBomb":
@@ -127,16 +127,16 @@ class FighterAbility(object):
therm = self.fighter.getModifiedItemAttr("{}DamageTherm".format(self.attrPrefix), 0)
kin = self.fighter.getModifiedItemAttr("{}DamageKin".format(self.attrPrefix), 0)
exp = self.fighter.getModifiedItemAttr("{}DamageExp".format(self.attrPrefix), 0)
dmgMult = self.fighter.amountActive * self.fighter.getModifiedItemAttr("{}DamageMultiplier".format(self.attrPrefix), 1)
dmgMult = self.fighter.amount * self.fighter.getModifiedItemAttr("{}DamageMultiplier".format(self.attrPrefix), 1)
volley = DmgTypes(
em=em * dmgMult * (1 - getattr(targetResists, "emAmount", 0)),
thermal=therm * dmgMult * (1 - getattr(targetResists, "thermalAmount", 0)),
kinetic=kin * dmgMult * (1 - getattr(targetResists, "kineticAmount", 0)),
explosive=exp * dmgMult * (1 - getattr(targetResists, "explosiveAmount", 0)))
em=em * dmgMult * (1 - getattr(targetProfile, "emAmount", 0)),
thermal=therm * dmgMult * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=kin * dmgMult * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=exp * dmgMult * (1 - getattr(targetProfile, "explosiveAmount", 0)))
return volley
def getDps(self, targetResists=None, cycleTimeOverride=None):
volley = self.getVolley(targetResists=targetResists)
def getDps(self, targetProfile=None, cycleTimeOverride=None):
volley = self.getVolley(targetProfile=targetProfile)
if not volley:
return DmgTypes(0, 0, 0, 0)
cycleTime = cycleTimeOverride if cycleTimeOverride is not None else self.cycleTime
@@ -149,5 +149,4 @@ class FighterAbility(object):
return dps
def clear(self):
self.__dps = None
self.__volley = None
pass

View File

@@ -36,13 +36,26 @@ from eos.saveddata.character import Character
from eos.saveddata.citadel import Citadel
from eos.saveddata.module import Module
from eos.saveddata.ship import Ship
from eos.utils.stats import DmgTypes
from eos.utils.stats import DmgTypes, RRTypes
pyfalog = Logger(__name__)
class Fit(object):
class FitLite:
def __init__(self, id=None, name=None, shipID=None, shipName=None, shipNameShort=None):
self.ID = id
self.name = name
self.shipID = shipID
self.shipName = shipName
self.shipNameShort = shipNameShort
def __repr__(self):
return 'FitLite(ID={})'.format(self.ID)
class Fit:
"""Represents a fitting, with modules, ship, implants, etc."""
PEAK_RECHARGE = 0.25
@@ -130,6 +143,7 @@ class Fit(object):
self.__capState = None
self.__capUsed = None
self.__capRecharge = None
self.__savedCapSimData = {}
self.__calculatedTargets = []
self.factorReload = False
self.boostsFits = set()
@@ -137,13 +151,25 @@ class Fit(object):
self.ecmProjectedStr = 1
self.commandBonuses = {}
@property
def targetResists(self):
return self.__targetResists
def clearFactorReloadDependentData(self):
# Here we clear all data known to rely on cycle parameters
# (which, in turn, relies on factor reload flag)
self.__weaponDpsMap.clear()
self.__droneDps = None
self.__remoteRepMap.clear()
self.__capStable = None
self.__capState = None
self.__capUsed = None
self.__capRecharge = None
self.__savedCapSimData.clear()
@targetResists.setter
def targetResists(self, targetResists):
self.__targetResists = targetResists
@property
def targetProfile(self):
return self.__targetProfile
@targetProfile.setter
def targetProfile(self, targetProfile):
self.__targetProfile = targetProfile
self.__weaponDpsMap = {}
self.__weaponVolleyMap = {}
self.__droneDps = None
@@ -437,6 +463,7 @@ class Fit(object):
self.__capState = None
self.__capUsed = None
self.__capRecharge = None
self.__savedCapSimData.clear()
self.ecmProjectedStr = 1
# self.commandBonuses = {}
@@ -1071,7 +1098,7 @@ class Fit(object):
def fighterBayUsed(self):
amount = 0
for f in self.fighters:
amount += f.item.volume * f.amountActive
amount += f.item.volume * f.amount
return amount
@@ -1167,9 +1194,15 @@ class Fit(object):
return self.__capRecharge
def calculateCapRecharge(self, percent=PEAK_RECHARGE):
capacity = self.ship.getModifiedItemAttr("capacitorCapacity")
rechargeRate = self.ship.getModifiedItemAttr("rechargeRate") / 1000.0
@property
def capDelta(self):
return (self.__capRecharge or 0) - (self.__capUsed or 0)
def calculateCapRecharge(self, percent=PEAK_RECHARGE, capacity=None, rechargeRate=None):
if capacity is None:
capacity = self.ship.getModifiedItemAttr("capacitorCapacity")
if rechargeRate is None:
rechargeRate = self.ship.getModifiedItemAttr("rechargeRate") / 1000.0
return 10 / rechargeRate * sqrt(percent) * (1 - sqrt(percent)) * capacity
def calculateShieldRecharge(self, percent=PEAK_RECHARGE):
@@ -1199,29 +1232,39 @@ class Fit(object):
drains = []
capUsed = 0
capAdded = 0
for mod in self.modules:
if mod.state >= FittingModuleState.ACTIVE:
if (mod.getModifiedItemAttr("capacitorNeed") or 0) != 0:
cycleTime = mod.rawCycleTime or 0
reactivationTime = mod.getModifiedItemAttr("moduleReactivationDelay") or 0
fullCycleTime = cycleTime + reactivationTime
reloadTime = mod.reloadTime
if fullCycleTime > 0:
capNeed = mod.capUse
if capNeed > 0:
capUsed += capNeed
else:
capAdded -= capNeed
for mod in self.activeModulesIter():
if (mod.getModifiedItemAttr("capacitorNeed") or 0) != 0:
cycleTime = mod.rawCycleTime or 0
reactivationTime = mod.getModifiedItemAttr("moduleReactivationDelay") or 0
fullCycleTime = cycleTime + reactivationTime
reloadTime = mod.reloadTime
if fullCycleTime > 0:
capNeed = mod.capUse
if capNeed > 0:
capUsed += capNeed
else:
capAdded -= capNeed
# If this is a turret, don't stagger activations
disableStagger = mod.hardpoint == FittingHardpoint.TURRET
# If this is a turret, don't stagger activations
disableStagger = mod.hardpoint == FittingHardpoint.TURRET
drains.append((int(fullCycleTime), mod.getModifiedItemAttr("capacitorNeed") or 0,
mod.numShots or 0, disableStagger, reloadTime))
drains.append((
int(fullCycleTime),
mod.getModifiedItemAttr("capacitorNeed") or 0,
mod.numShots or 0,
disableStagger,
reloadTime,
mod.item.group.name == 'Capacitor Booster'))
for fullCycleTime, capNeed, clipSize, reloadTime in self.iterDrains():
# Stagger incoming effects for cap simulation
drains.append((int(fullCycleTime), capNeed, clipSize, False, reloadTime))
drains.append((
int(fullCycleTime),
capNeed,
clipSize,
# Stagger incoming effects for cap simulation
False,
reloadTime,
False))
if capNeed > 0:
capUsed += capNeed / (fullCycleTime / 1000.0)
else:
@@ -1232,17 +1275,8 @@ class Fit(object):
def simulateCap(self):
drains, self.__capUsed, self.__capRecharge = self.__generateDrain()
self.__capRecharge += self.calculateCapRecharge()
if len(drains) > 0:
sim = capSim.CapSimulator()
sim.init(drains)
sim.capacitorCapacity = self.ship.getModifiedItemAttr("capacitorCapacity")
sim.capacitorRecharge = self.ship.getModifiedItemAttr("rechargeRate")
sim.stagger = True
sim.scale = False
sim.t_max = 6 * 60 * 60 * 1000
sim.reload = self.factorReload
sim.run()
sim = self.__runCapSim(drains=drains)
if sim is not None:
capState = (sim.cap_stable_low + sim.cap_stable_high) / (2 * sim.capacitorCapacity)
self.__capStable = capState > 0
self.__capState = min(100, capState * 100) if self.__capStable else sim.t / 1000.0
@@ -1250,23 +1284,55 @@ class Fit(object):
self.__capStable = True
self.__capState = 100
def getCapSimData(self, startingCap):
if startingCap not in self.__savedCapSimData:
self.__runCapSim(startingCap=startingCap, tMax=3600, optimizeRepeats=False)
return self.__savedCapSimData[startingCap]
def __runCapSim(self, drains=None, startingCap=None, tMax=None, optimizeRepeats=True):
if drains is None:
drains, nil, nil = self.__generateDrain()
if tMax is None:
tMax = 6 * 60 * 60 * 1000
else:
tMax *= 1000
if len(drains) > 0:
sim = capSim.CapSimulator()
sim.init(drains)
sim.capacitorCapacity = self.ship.getModifiedItemAttr("capacitorCapacity")
sim.capacitorRecharge = self.ship.getModifiedItemAttr("rechargeRate")
sim.startingCapacity = startingCap = self.ship.getModifiedItemAttr("capacitorCapacity") if startingCap is None else startingCap
sim.stagger = True
sim.scale = False
sim.t_max = tMax
sim.reload = self.factorReload
sim.optimize_repeats = optimizeRepeats
sim.run()
# We do not want to store partial results
if not sim.result_optimized_repeats:
self.__savedCapSimData[startingCap] = sim.saved_changes
return sim
else:
self.__savedCapSimData[startingCap] = []
return None
def getCapRegenGainFromMod(self, mod):
"""Return how much cap regen do we gain from having this module"""
currentRegen = self.calculateCapRecharge()
nomodRegen = self.calculateCapRecharge(
capacity=self.ship.getModifiedItemAttrExtended("capacitorCapacity", ignoreAfflictors=[mod]),
rechargeRate=self.ship.getModifiedItemAttrExtended("rechargeRate", ignoreAfflictors=[mod]) / 1000.0)
return currentRegen - nomodRegen
def getRemoteReps(self, spoolOptions=None):
if spoolOptions not in self.__remoteRepMap:
remoteReps = {}
remoteReps = RRTypes(0, 0, 0, 0)
for module in self.modules:
rrType, rrAmount = module.getRemoteReps(spoolOptions=spoolOptions)
if rrType:
if rrType not in remoteReps:
remoteReps[rrType] = 0
remoteReps[rrType] += rrAmount
remoteReps += module.getRemoteReps(spoolOptions=spoolOptions)
for drone in self.drones:
rrType, rrAmount = drone.getRemoteReps()
if rrType:
if rrType not in remoteReps:
remoteReps[rrType] = 0
remoteReps[rrType] += rrAmount
remoteReps += drone.getRemoteReps()
self.__remoteRepMap[spoolOptions] = remoteReps
@@ -1361,47 +1427,47 @@ class Fit(object):
for tankType in localAdjustment:
dict = self.extraAttributes.getAfflictions(tankType)
if self in dict:
for mod, _, amount, used in dict[self]:
for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in dict[self]:
if not used:
continue
if mod.projected:
if afflictor.projected:
continue
if mod.item.group.name not in groupAttrMap:
if afflictor.item.group.name not in groupAttrMap:
continue
usesCap = True
try:
if mod.capUse:
capUsed -= mod.capUse
if afflictor.capUse:
capUsed -= afflictor.capUse
else:
usesCap = False
except AttributeError:
usesCap = False
# Normal Repairers
if usesCap and not mod.charge:
cycleTime = mod.rawCycleTime
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
if usesCap and not afflictor.charge:
cycleTime = afflictor.rawCycleTime
amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name])
localAdjustment[tankType] -= amount / (cycleTime / 1000.0)
repairers.append(mod)
repairers.append(afflictor)
# Ancillary Armor reps etc
elif usesCap and mod.charge:
cycleTime = mod.rawCycleTime
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
if mod.charge.name == "Nanite Repair Paste":
multiplier = mod.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
elif usesCap and afflictor.charge:
cycleTime = afflictor.rawCycleTime
amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name])
if afflictor.charge.name == "Nanite Repair Paste":
multiplier = afflictor.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
else:
multiplier = 1
localAdjustment[tankType] -= amount * multiplier / (cycleTime / 1000.0)
repairers.append(mod)
repairers.append(afflictor)
# Ancillary Shield boosters etc
elif not usesCap and mod.item.group.name in ("Ancillary Shield Booster", "Ancillary Remote Shield Booster"):
cycleTime = mod.rawCycleTime
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
if self.factorReload and mod.charge:
reloadtime = mod.reloadTime
elif not usesCap and afflictor.item.group.name in ("Ancillary Shield Booster", "Ancillary Remote Shield Booster"):
cycleTime = afflictor.rawCycleTime
amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name])
if self.factorReload and afflictor.charge:
reloadtime = afflictor.reloadTime
else:
reloadtime = 0.0
offdutycycle = reloadtime / ((max(mod.numShots, 1) * cycleTime) + reloadtime)
offdutycycle = reloadtime / ((max(afflictor.numShots, 1) * cycleTime) + reloadtime)
localAdjustment[tankType] -= amount * offdutycycle / (cycleTime / 1000.0)
# Sort repairers by efficiency. We want to use the most efficient repairers first
@@ -1413,35 +1479,35 @@ class Fit(object):
# Most efficient first, as we sorted earlier.
# calculate how much the repper can rep stability & add to total
totalPeakRecharge = self.capRecharge
for mod in repairers:
for afflictor in repairers:
if capUsed > totalPeakRecharge:
break
if self.factorReload and mod.charge:
reloadtime = mod.reloadTime
if self.factorReload and afflictor.charge:
reloadtime = afflictor.reloadTime
else:
reloadtime = 0.0
cycleTime = mod.rawCycleTime
capPerSec = mod.capUse
cycleTime = afflictor.rawCycleTime
capPerSec = afflictor.capUse
if capPerSec is not None and cycleTime is not None:
# Check how much this repper can work
sustainability = min(1, (totalPeakRecharge - capUsed) / capPerSec)
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name])
# Add the sustainable amount
if not mod.charge:
localAdjustment[groupStoreMap[mod.item.group.name]] += sustainability * amount / (
if not afflictor.charge:
localAdjustment[groupStoreMap[afflictor.item.group.name]] += sustainability * amount / (
cycleTime / 1000.0)
else:
if mod.charge.name == "Nanite Repair Paste":
multiplier = mod.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
if afflictor.charge.name == "Nanite Repair Paste":
multiplier = afflictor.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
else:
multiplier = 1
ondutycycle = (max(mod.numShots, 1) * cycleTime) / (
(max(mod.numShots, 1) * cycleTime) + reloadtime)
ondutycycle = (max(afflictor.numShots, 1) * cycleTime) / (
(max(afflictor.numShots, 1) * cycleTime) + reloadtime)
localAdjustment[groupStoreMap[
mod.item.group.name]] += sustainability * amount * ondutycycle * multiplier / (
afflictor.item.group.name]] += sustainability * amount * ondutycycle * multiplier / (
cycleTime / 1000.0)
capUsed += capPerSec
@@ -1482,8 +1548,8 @@ class Fit(object):
weaponDps = DmgTypes(0, 0, 0, 0)
for mod in self.modules:
weaponVolley += mod.getVolley(spoolOptions=spoolOptions, targetResists=self.targetResists)
weaponDps += mod.getDps(spoolOptions=spoolOptions, targetResists=self.targetResists)
weaponVolley += mod.getVolley(spoolOptions=spoolOptions, targetProfile=self.targetProfile)
weaponDps += mod.getDps(spoolOptions=spoolOptions, targetProfile=self.targetProfile)
self.__weaponVolleyMap[spoolOptions] = weaponVolley
self.__weaponDpsMap[spoolOptions] = weaponDps
@@ -1493,12 +1559,12 @@ class Fit(object):
droneDps = DmgTypes(0, 0, 0, 0)
for drone in self.drones:
droneVolley += drone.getVolley(targetResists=self.targetResists)
droneDps += drone.getDps(targetResists=self.targetResists)
droneVolley += drone.getVolley(targetProfile=self.targetProfile)
droneDps += drone.getDps(targetProfile=self.targetProfile)
for fighter in self.fighters:
droneVolley += fighter.getVolley(targetResists=self.targetResists)
droneDps += fighter.getDps(targetResists=self.targetResists)
droneVolley += fighter.getVolley(targetProfile=self.targetProfile)
droneDps += fighter.getDps(targetProfile=self.targetProfile)
self.__droneDps = droneDps
self.__droneVolley = droneVolley
@@ -1533,6 +1599,28 @@ class Fit(object):
secstatus = FitSystemSecurity.NULLSEC
return secstatus
def activeModulesIter(self):
for mod in self.modules:
if mod.state >= FittingModuleState.ACTIVE:
yield mod
def activeDronesIter(self):
for drone in self.drones:
if drone.amountActive > 0:
yield drone
def activeFightersIter(self):
for fighter in self.fighters:
if fighter.active:
yield fighter
def activeFighterAbilityIter(self):
for fighter in self.activeFightersIter():
for ability in fighter.abilities:
if ability.active:
yield fighter, ability
def __deepcopy__(self, memo=None):
fitCopy = Fit()
# Character and owner are not copied
@@ -1542,7 +1630,7 @@ class Fit(object):
fitCopy.mode = deepcopy(self.mode)
fitCopy.name = "%s copy" % self.name
fitCopy.damagePattern = self.damagePattern
fitCopy.targetResists = self.targetResists
fitCopy.targetProfile = self.targetProfile
fitCopy.implantLocation = self.implantLocation
fitCopy.systemSecurity = self.systemSecurity
fitCopy.notes = self.notes

View File

@@ -22,7 +22,7 @@ from copy import deepcopy
from eos.effectHandlerHelpers import HandledImplantList
class ImplantSet(object):
class ImplantSet:
def __init__(self, name=None):
self.name = name
self.__implants = HandledImplantList()

View File

@@ -30,7 +30,7 @@ from eos.saveddata.mutator import Mutator
from eos.utils.cycles import CycleInfo, CycleSequence
from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import calculateSpoolup, resolveSpoolOptions
from eos.utils.stats import DmgTypes
from eos.utils.stats import DmgTypes, RRTypes
pyfalog = Logger(__name__)
@@ -134,7 +134,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.__charge = None
self.__baseVolley = None
self.__baseRemoteReps = None
self.__baseRRAmount = None
self.__miningyield = None
self.__reloadTime = None
self.__reloadForce = None
@@ -414,17 +414,31 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return self.__miningyield
def getVolleyParameters(self, spoolOptions=None, targetResists=None, ignoreState=False):
def isDealingDamage(self, ignoreState=False):
volleyParams = self.getVolleyParameters(ignoreState=ignoreState)
for volley in volleyParams.values():
if volley.total > 0:
return True
return False
def getVolleyParameters(self, spoolOptions=None, targetProfile=None, ignoreState=False):
if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState):
return {0: DmgTypes(0, 0, 0, 0)}
if self.__baseVolley is None:
self.__baseVolley = {}
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
dmgMult = self.getModifiedItemAttr("damageMultiplier", 1)
dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0) or self.getModifiedItemAttr("doomsdayWarningDuration", 0)
# Some delay attributes have non-0 default value, so we have to pick according to effects
if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(self.item.effects):
dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0)
elif {'doomsdayBeamDOT', 'doomsdaySlash', 'doomsdayConeDOT'}.intersection(self.item.effects):
dmgDelay = self.getModifiedItemAttr("doomsdayWarningDuration", 0)
else:
dmgDelay = 0
dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0)
dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0)
if dmgDuration != 0 and dmgSubcycle != 0:
# Reaper DD can damage each target only once
if dmgDuration != 0 and dmgSubcycle != 0 and 'doomsdaySlash' not in self.item.effects:
subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle))
else:
subcycles = 1
@@ -443,24 +457,24 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
adjustedVolley = {}
for volleyTime, volleyValue in self.__baseVolley.items():
adjustedVolley[volleyTime] = DmgTypes(
em=volleyValue.em * spoolMultiplier * (1 - getattr(targetResists, "emAmount", 0)),
thermal=volleyValue.thermal * spoolMultiplier * (1 - getattr(targetResists, "thermalAmount", 0)),
kinetic=volleyValue.kinetic * spoolMultiplier * (1 - getattr(targetResists, "kineticAmount", 0)),
explosive=volleyValue.explosive * spoolMultiplier * (1 - getattr(targetResists, "explosiveAmount", 0)))
em=volleyValue.em * spoolMultiplier * (1 - getattr(targetProfile, "emAmount", 0)),
thermal=volleyValue.thermal * spoolMultiplier * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=volleyValue.kinetic * spoolMultiplier * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=volleyValue.explosive * spoolMultiplier * (1 - getattr(targetProfile, "explosiveAmount", 0)))
return adjustedVolley
def getVolley(self, spoolOptions=None, targetResists=None, ignoreState=False):
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetResists=targetResists, ignoreState=ignoreState)
def getVolley(self, spoolOptions=None, targetProfile=None, ignoreState=False):
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
if len(volleyParams) == 0:
return DmgTypes(0, 0, 0, 0)
return volleyParams[min(volleyParams)]
def getDps(self, spoolOptions=None, targetResists=None, ignoreState=False):
def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False):
dmgDuringCycle = DmgTypes(0, 0, 0, 0)
cycleParams = self.getCycleParameters()
if cycleParams is None:
return dmgDuringCycle
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetResists=targetResists, ignoreState=ignoreState)
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
avgCycleTime = cycleParams.averageTime
if len(volleyParams) == 0 or avgCycleTime == 0:
return dmgDuringCycle
@@ -474,56 +488,75 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
explosive=dmgDuringCycle.explosive * dpsFactor)
return dps
def getRemoteReps(self, spoolOptions=None, ignoreState=False):
def isRemoteRepping(self, ignoreState=False):
repParams = self.getRepAmountParameters(ignoreState=ignoreState)
for rrData in repParams.values():
if rrData:
return True
return False
def getRepAmountParameters(self, spoolOptions=None, ignoreState=False):
if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState):
return None, 0
def getBaseRemoteReps(module):
remoteModuleGroups = {
"Remote Armor Repairer": "Armor",
"Ancillary Remote Armor Repairer": "Armor",
"Mutadaptive Remote Armor Repairer": "Armor",
"Remote Hull Repairer": "Hull",
"Remote Shield Booster": "Shield",
"Ancillary Remote Shield Booster": "Shield",
"Remote Capacitor Transmitter": "Capacitor"}
rrType = remoteModuleGroups.get(module.item.group.name, None)
if not rrType:
return None, 0
return {}
remoteModuleGroups = {
"Remote Armor Repairer": "Armor",
"Ancillary Remote Armor Repairer": "Armor",
"Mutadaptive Remote Armor Repairer": "Armor",
"Remote Hull Repairer": "Hull",
"Remote Shield Booster": "Shield",
"Ancillary Remote Shield Booster": "Shield",
"Remote Capacitor Transmitter": "Capacitor"}
rrType = remoteModuleGroups.get(self.item.group.name)
if rrType is None:
return {}
if self.__baseRRAmount is None:
self.__baseRRAmount = {}
shieldAmount = 0
armorAmount = 0
hullAmount = 0
capacitorAmount = 0
if rrType == "Hull":
rrAmount = module.getModifiedItemAttr("structureDamageAmount", 0)
hullAmount += self.getModifiedItemAttr("structureDamageAmount", 0)
elif rrType == "Armor":
rrAmount = module.getModifiedItemAttr("armorDamageAmount", 0)
if self.item.group.name == "Ancillary Remote Armor Repairer" and self.charge:
mult = self.getModifiedItemAttr("chargedArmorDamageMultiplier", 1)
else:
mult = 1
armorAmount += self.getModifiedItemAttr("armorDamageAmount", 0) * mult
elif rrType == "Shield":
rrAmount = module.getModifiedItemAttr("shieldBonus", 0)
shieldAmount += self.getModifiedItemAttr("shieldBonus", 0)
elif rrType == "Capacitor":
rrAmount = module.getModifiedItemAttr("powerTransferAmount", 0)
capacitorAmount += self.getModifiedItemAttr("powerTransferAmount", 0)
rrDelay = 0 if rrType == "Shield" else self.rawCycleTime
self.__baseRRAmount[rrDelay] = RRTypes(shield=shieldAmount, armor=armorAmount, hull=hullAmount, capacitor=capacitorAmount)
spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self)
spoolBoost = calculateSpoolup(
self.getModifiedItemAttr("repairMultiplierBonusMax", 0),
self.getModifiedItemAttr("repairMultiplierBonusPerCycle", 0),
self.rawCycleTime / 1000, spoolType, spoolAmount)[0]
spoolMultiplier = 1 + spoolBoost
adjustedRRAmount = {}
for rrTime, rrAmount in self.__baseRRAmount.items():
if spoolMultiplier == 1:
adjustedRRAmount[rrTime] = rrAmount
else:
return None, 0
if rrAmount:
cycleParams = self.getCycleParameters()
if cycleParams is None:
return None, 0
rrAmount *= 1 / (cycleParams.averageTime / 1000)
if module.item.group.name == "Ancillary Remote Armor Repairer" and module.charge:
rrAmount *= module.getModifiedItemAttr("chargedArmorDamageMultiplier", 1)
adjustedRRAmount[rrTime] = rrAmount * spoolMultiplier
return adjustedRRAmount
return rrType, rrAmount
if self.__baseRemoteReps is None:
self.__baseRemoteReps = getBaseRemoteReps(self)
rrType, rrAmount = self.__baseRemoteReps
if rrType and rrAmount and self.item.group.name == "Mutadaptive Remote Armor Repairer":
spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self)
spoolBoost = calculateSpoolup(
self.getModifiedItemAttr("repairMultiplierBonusMax", 0),
self.getModifiedItemAttr("repairMultiplierBonusPerCycle", 0),
self.rawCycleTime / 1000, spoolType, spoolAmount)[0]
rrAmount *= (1 + spoolBoost)
return rrType, rrAmount
def getRemoteReps(self, spoolOptions=None, ignoreState=False, reloadOverride=None):
rrDuringCycle = RRTypes(0, 0, 0, 0)
cycleParams = self.getCycleParameters(reloadOverride=reloadOverride)
if cycleParams is None:
return rrDuringCycle
repAmountParams = self.getRepAmountParameters(spoolOptions=spoolOptions, ignoreState=ignoreState)
avgCycleTime = cycleParams.averageTime
if len(repAmountParams) == 0 or avgCycleTime == 0:
return rrDuringCycle
for rrAmount in repAmountParams.values():
rrDuringCycle += rrAmount
rrFactor = 1 / (avgCycleTime / 1000)
rps = rrDuringCycle * rrFactor
return rps
def getSpoolData(self, spoolOptions=None):
weaponMultMax = self.getModifiedItemAttr("damageMultiplierBonusMax", 0)
@@ -650,30 +683,41 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def canHaveState(self, state=None, projectedOnto=None):
"""
Check with other modules if there are restrictions that might not allow this module to be activated
Check with other modules if there are restrictions that might not allow this module to be activated.
Returns True if state is allowed, or max state module can have if current state is invalid.
"""
# If we're going to set module to offline or online for local modules or offline for projected,
# it should be fine for all cases
# If we're going to set module to offline, it should be fine for all cases
item = self.item
if (state <= FittingModuleState.ONLINE and projectedOnto is None) or (state <= FittingModuleState.OFFLINE):
if state <= FittingModuleState.OFFLINE:
return True
# Check if the local module is over it's max limit; if it's not, we're fine
maxGroupOnline = self.getModifiedItemAttr("maxGroupOnline", None)
maxGroupActive = self.getModifiedItemAttr("maxGroupActive", None)
if maxGroupActive is None and projectedOnto is None:
if maxGroupOnline is None and maxGroupActive is None and projectedOnto is None:
return True
# Following is applicable only to local modules, we do not want to limit projected
if projectedOnto is None:
currOnline = 0
currActive = 0
group = item.group.name
maxState = None
for mod in self.owner.modules:
currItem = getattr(mod, "item", None)
if mod.state >= FittingModuleState.ACTIVE and currItem is not None and currItem.group.name == group:
currActive += 1
if currActive > maxGroupActive:
break
return currActive <= maxGroupActive
if currItem is not None and currItem.group.name == group:
if mod.state >= FittingModuleState.ONLINE:
currOnline += 1
if mod.state >= FittingModuleState.ACTIVE:
currActive += 1
if maxGroupOnline is not None and currOnline > maxGroupOnline:
if maxState is None or maxState > FittingModuleState.OFFLINE:
maxState = FittingModuleState.OFFLINE
break
if maxGroupActive is not None and currActive > maxGroupActive:
if maxState is None or maxState > FittingModuleState.ONLINE:
maxState = FittingModuleState.ONLINE
return True if maxState is None else maxState
# For projected, we're checking if ship is vulnerable to given item
else:
# Do not allow to apply offensive modules on ship with offensive module immunite, with few exceptions
@@ -684,10 +728,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
"energyNosferatuFalloff",
"energyNeutralizerFalloff"}
if not offensiveNonModifiers.intersection(set(item.effects)):
return False
return FittingModuleState.OFFLINE
# If assistive modules are not allowed, do not let to apply these altogether
if item.assistive and projectedOnto.ship.getModifiedItemAttr("disallowAssistance") == 1:
return False
return FittingModuleState.OFFLINE
return True
def isValidCharge(self, charge):
@@ -780,7 +824,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def clear(self):
self.__baseVolley = None
self.__baseRemoteReps = None
self.__baseRRAmount = None
self.__miningyield = None
self.__reloadTime = None
self.__reloadForce = None
@@ -873,14 +917,14 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
early_cycles = cycles_until_reload - final_cycles
# Single cycle until effect cannot run anymore
if early_cycles == 0:
return CycleInfo(active_time, 0, 1)
return CycleInfo(active_time, 0, 1, False)
# Multiple cycles with the same parameters
if forced_inactive_time == 0:
return CycleInfo(active_time, 0, cycles_until_reload)
return CycleInfo(active_time, 0, cycles_until_reload, False)
# Multiple cycles with different parameters
return CycleSequence((
CycleInfo(active_time, forced_inactive_time, early_cycles),
CycleInfo(active_time, 0, final_cycles)
CycleInfo(active_time, forced_inactive_time, early_cycles, False),
CycleInfo(active_time, 0, final_cycles, False)
), 1)
# Module cycles the same way all the time in 3 cases:
# 1) caller doesn't want to take into account reload time
@@ -891,7 +935,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
cycles_until_reload == math.inf or
forced_inactive_time >= reload_time
):
return CycleInfo(active_time, forced_inactive_time, math.inf)
isInactivityReload = factorReload and forced_inactive_time >= reload_time
return CycleInfo(active_time, forced_inactive_time, math.inf, isInactivityReload)
# We've got to take reload into consideration
else:
final_cycles = 1
@@ -899,10 +944,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
# If effect has to reload after each its cycle, then its parameters
# are the same all the time
if early_cycles == 0:
return CycleInfo(active_time, reload_time, math.inf)
return CycleInfo(active_time, reload_time, math.inf, True)
return CycleSequence((
CycleInfo(active_time, forced_inactive_time, early_cycles),
CycleInfo(active_time, reload_time, final_cycles)
CycleInfo(active_time, forced_inactive_time, early_cycles, False),
CycleInfo(active_time, reload_time, final_cycles, True)
), math.inf)
@property

View File

@@ -42,7 +42,7 @@ class PriceStatus(IntEnum):
fetchTimeout = 4
class Price(object):
class Price:
def __init__(self, typeID):
self.typeID = typeID
self.time = 0

View File

@@ -24,7 +24,7 @@ import time
# from tomorrow import threads
class SsoCharacter(object):
class SsoCharacter:
def __init__(self, charID, name, client, accessToken=None, refreshToken=None):
self.characterID = charID
self.characterName = name

View File

@@ -17,25 +17,77 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
import math
import re
from logbook import Logger
import eos.db
pyfalog = Logger(__name__)
class TargetResists(object):
class TargetProfile:
# also determined import/export order - VERY IMPORTANT
DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive")
def __init__(self, *args, **kwargs):
self.update(*args, **kwargs)
def update(self, emAmount=0, thermalAmount=0, kineticAmount=0, explosiveAmount=0):
def update(self, emAmount=0, thermalAmount=0, kineticAmount=0, explosiveAmount=0, maxVelocity=None, signatureRadius=None, radius=None):
self.emAmount = emAmount
self.thermalAmount = thermalAmount
self.kineticAmount = kineticAmount
self.explosiveAmount = explosiveAmount
self._maxVelocity = maxVelocity
self._signatureRadius = signatureRadius
self._radius = radius
_idealTarget = None
@classmethod
def getIdeal(cls):
if cls._idealTarget is None:
cls._idealTarget = cls(
emAmount=0,
thermalAmount=0,
kineticAmount=0,
explosiveAmount=0,
maxVelocity=0,
signatureRadius=None,
radius=0)
cls._idealTarget.name = 'Ideal Target'
cls._idealTarget.ID = -1
return cls._idealTarget
@property
def maxVelocity(self):
return self._maxVelocity or 0
@maxVelocity.setter
def maxVelocity(self, val):
self._maxVelocity = val
@property
def signatureRadius(self):
if self._signatureRadius is None or self._signatureRadius == -1:
return math.inf
return self._signatureRadius
@signatureRadius.setter
def signatureRadius(self, val):
if val is not None and math.isinf(val):
val = None
self._signatureRadius = val
@property
def radius(self):
return self._radius or 0
@radius.setter
def radius(self, val):
self._radius = val
@classmethod
def importPatterns(cls, text):
@@ -46,7 +98,7 @@ class TargetResists(object):
# When we import damage profiles, we create new ones and update old ones. To do this, get a list of current
# patterns to allow lookup
lookup = {}
current = eos.db.getTargetResistsList()
current = eos.db.getTargetProfileList()
for pattern in current:
lookup[pattern.name] = pattern
@@ -56,20 +108,22 @@ class TargetResists(object):
continue
line = line.split('#', 1)[0] # allows for comments
type, data = line.rsplit('=', 1)
type, data = type.strip(), data.split(',')
type, data = type.strip(), [d.strip() for d in data.split(',')]
except:
pyfalog.warning("Data isn't in correct format, continue to next line.")
continue
if type != "TargetResists":
if type not in ("TargetProfile", "TargetResists"):
continue
numPatterns += 1
name, data = data[0], data[1:5]
name, dataRes, dataMisc = data[0], data[1:5], data[5:8]
fields = {}
for index, val in enumerate(data):
val = float(val)
for index, val in enumerate(dataRes):
val = float(val) if val else 0
if math.isinf(val):
val = 0
try:
assert 0 <= val <= 100
fields["%sAmount" % cls.DAMAGE_TYPES[index]] = val / 100
@@ -77,13 +131,24 @@ class TargetResists(object):
pyfalog.warning("Caught unhandled exception in import patterns.")
continue
if len(fields) == 4: # Avoid possible blank lines
if len(dataMisc) == 3:
for index, val in enumerate(dataMisc):
try:
fieldName = ("maxVelocity", "signatureRadius", "radius")[index]
except IndexError:
break
val = float(val) if val else 0
if fieldName != "signatureRadius" and math.isinf(val):
val = 0
fields[fieldName] = val
if len(fields) in (4, 7): # Avoid possible blank lines
if name.strip() in lookup:
pattern = lookup[name.strip()]
pattern.update(**fields)
eos.db.save(pattern)
else:
pattern = TargetResists(**fields)
pattern = TargetProfile(**fields)
pattern.name = name.strip()
eos.db.save(pattern)
patterns.append(pattern)
@@ -92,25 +157,30 @@ class TargetResists(object):
return patterns, numPatterns
EXPORT_FORMAT = "TargetResists = %s,%.1f,%.1f,%.1f,%.1f\n"
EXPORT_FORMAT = "TargetProfile = %s,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f\n"
@classmethod
def exportPatterns(cls, *patterns):
out = "# Exported from pyfa\n#\n"
out += "# Values are in following format:\n"
out += "# TargetResists = [name],[EM %],[Thermal %],[Kinetic %],[Explosive %]\n\n"
out += "# TargetProfile = [name],[EM %],[Thermal %],[Kinetic %],[Explosive %],[Max velocity m/s],[Signature radius m],[Radius m]\n\n"
for dp in patterns:
out += cls.EXPORT_FORMAT % (
dp.name,
dp.emAmount * 100,
dp.thermalAmount * 100,
dp.kineticAmount * 100,
dp.explosiveAmount * 100
dp.explosiveAmount * 100,
dp.maxVelocity,
dp.signatureRadius,
dp.radius
)
return out.strip()
def __deepcopy__(self, memo):
p = TargetResists(self.emAmount, self.thermalAmount, self.kineticAmount, self.explosiveAmount)
p = TargetProfile(
self.emAmount, self.thermalAmount, self.kineticAmount, self.explosiveAmount,
self._maxVelocity, self._signatureRadius, self._radius)
p.name = "%s copy" % self.name
return p

View File

@@ -24,7 +24,7 @@ import string
from sqlalchemy.orm import validates
class User(object):
class User:
def __init__(self, username, password=None, admin=False):
self.username = username
if password is not None:

View File

@@ -6,10 +6,11 @@ from utils.repr import makeReprStr
class CycleInfo:
def __init__(self, activeTime, inactiveTime, quantity):
def __init__(self, activeTime, inactiveTime, quantity, isInactivityReload):
self.activeTime = activeTime
self.inactiveTime = inactiveTime
self.quantity = quantity
self.isInactivityReload = isInactivityReload
@property
def averageTime(self):
@@ -18,7 +19,7 @@ class CycleInfo:
def iterCycles(self):
i = 0
while i < self.quantity:
yield self.activeTime, self.inactiveTime
yield self.activeTime, self.inactiveTime, self.isInactivityReload
i += 1
def _getCycleQuantity(self):
@@ -28,7 +29,7 @@ class CycleInfo:
return (self.activeTime + self.inactiveTime) * self.quantity
def __repr__(self):
spec = ['activeTime', 'inactiveTime', 'quantity']
spec = ['activeTime', 'inactiveTime', 'quantity', 'isInactivityReload']
return makeReprStr(self, spec)
@@ -47,8 +48,8 @@ class CycleSequence:
i = 0
while i < self.quantity:
for cycleInfo in self.sequence:
for cycleTime, inactiveTime in cycleInfo.iterCycles():
yield cycleTime, inactiveTime
for cycleTime, inactiveTime, isInactivityReload in cycleInfo.iterCycles():
yield cycleTime, inactiveTime, isInactivityReload
i += 1
def _getCycleQuantity(self):

View File

@@ -18,7 +18,7 @@ keepDigits = int(sys.float_info.dig / 2)
def floatUnerr(value):
"""Round possible float number error, killing some precision in process."""
if value == 0:
if value in (0, math.inf):
return value
# Find round factor, taking into consideration that we want to keep at least
# predefined amount of significant digits

View File

@@ -18,6 +18,10 @@
# ===============================================================================
from eos.utils.float import floatUnerr
from utils.repr import makeReprStr
class DmgTypes:
"""Container for damage data stats."""
@@ -39,12 +43,14 @@ class DmgTypes:
def __eq__(self, other):
if not isinstance(other, DmgTypes):
return NotImplemented
return all((
self.em == other.em,
self.thermal == other.thermal,
self.kinetic == other.kinetic,
self.explosive == other.explosive,
self.total == other.total))
# Round for comparison's sake because often damage profiles are
# generated from data which includes float errors
return (
floatUnerr(self.em) == floatUnerr(other.em) and
floatUnerr(self.thermal) == floatUnerr(other.thermal) and
floatUnerr(self.kinetic) == floatUnerr(other.kinetic) and
floatUnerr(self.explosive) == floatUnerr(other.explosive) and
floatUnerr(self.total) == floatUnerr(other.total))
def __bool__(self):
return any((
@@ -68,3 +74,122 @@ class DmgTypes:
self.explosive += other.explosive
self._calcTotal()
return self
def __mul__(self, mul):
return type(self)(
em=self.em * mul,
thermal=self.thermal * mul,
kinetic=self.kinetic * mul,
explosive=self.explosive * mul)
def __imul__(self, mul):
if mul == 1:
return
self.em *= mul
self.thermal *= mul
self.kinetic *= mul
self.explosive *= mul
self._calcTotal()
return self
def __truediv__(self, div):
return type(self)(
em=self.em / div,
thermal=self.thermal / div,
kinetic=self.kinetic / div,
explosive=self.explosive / div)
def __itruediv__(self, div):
if div == 1:
return
self.em /= div
self.thermal /= div
self.kinetic /= div
self.explosive /= div
self._calcTotal()
return self
def __repr__(self):
spec = ['em', 'thermal', 'kinetic', 'explosive', 'total']
return makeReprStr(self, spec)
class RRTypes:
"""Container for tank data stats."""
def __init__(self, shield, armor, hull, capacitor):
self.shield = shield
self.armor = armor
self.hull = hull
self.capacitor = capacitor
# Iterator is needed to support tuple-style unpacking
def __iter__(self):
yield self.shield
yield self.armor
yield self.hull
yield self.capacitor
def __eq__(self, other):
if not isinstance(other, RRTypes):
return NotImplemented
# 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))
def __bool__(self):
return any((self.shield, self.armor, self.hull, self.capacitor))
def __add__(self, other):
return type(self)(
shield=self.shield + other.shield,
armor=self.armor + other.armor,
hull=self.hull + other.hull,
capacitor=self.capacitor + other.capacitor)
def __iadd__(self, other):
self.shield += other.shield
self.armor += other.armor
self.hull += other.hull
self.capacitor += other.capacitor
return self
def __mul__(self, mul):
return type(self)(
shield=self.shield * mul,
armor=self.armor * mul,
hull=self.hull * mul,
capacitor=self.capacitor * mul)
def __imul__(self, mul):
if mul == 1:
return
self.shield *= mul
self.armor *= mul
self.hull *= mul
self.capacitor *= mul
return self
def __truediv__(self, div):
return type(self)(
shield=self.shield / div,
armor=self.armor / div,
hull=self.hull / div,
capacitor=self.capacitor / div)
def __itruediv__(self, div):
if div == 1:
return self
self.shield /= div
self.armor /= div
self.hull /= div
self.capacitor /= div
return self
def __repr__(self):
spec = ['shield', 'armor', 'hull', 'capacitor']
return makeReprStr(self, spec)

BIN
eve.db

Binary file not shown.

22
graphs/__init__.py Normal file
View File

@@ -0,0 +1,22 @@
# =============================================================================
# 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 .gui.canvasPanel import graphFrame_enabled
from .gui.frame import GraphFrame

65
graphs/calc.py Normal file
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/>.
# =============================================================================
import math
def calculateRangeFactor(srcOptimalRange, srcFalloffRange, distance, restrictedRange=True):
"""Range strength/chance factor, applicable to guns, ewar, RRs, etc."""
if distance is None:
return 1
if srcFalloffRange > 0:
# Most modules cannot be activated when at 3x falloff range, with few exceptions like guns
if restrictedRange and distance > srcOptimalRange + 3 * srcFalloffRange:
return 0
return 0.5 ** ((max(0, distance - srcOptimalRange) / srcFalloffRange) ** 2)
elif distance <= srcOptimalRange:
return 1
else:
return 0
# 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

28
graphs/data/__init__.py Normal file
View File

@@ -0,0 +1,28 @@
# =============================================================================
# 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 . import fitDamageStats
from . import fitEwarStats
from . import fitRemoteReps
from . import fitShieldRegen
from . import fitCapacitor
from . import fitMobility
from . import fitWarpTime
from . import fitLockTime

View File

@@ -0,0 +1,23 @@
# =============================================================================
# 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 .cache import FitDataCache
from .defs import XDef, YDef, VectorDef, Input, InputCheckbox
from .getter import PointGetter, SmoothPointGetter
from .graph import FitGraph

View File

@@ -18,26 +18,14 @@
# =============================================================================
class Graph(object):
views = []
@classmethod
def register(cls):
Graph.views.append(cls)
class FitDataCache:
def __init__(self):
self.name = ""
self._data = {}
def getFields(self, fit, fields):
raise NotImplementedError()
def clearForFit(self, fitID):
if fitID in self._data:
del self._data[fitID]
def getIcons(self):
return None
@property
def redrawOnEffectiveChange(self):
return False
# noinspection PyUnresolvedReferences
from gui.builtinGraphs import *
def clearAll(self):
self._data.clear()

134
graphs/data/base/defs.py Normal file
View File

@@ -0,0 +1,134 @@
# =============================================================================
# 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 collections import namedtuple
VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit', 'label'))
class YDef:
def __init__(self, handle, unit, label, selectorLabel=None):
self.handle = handle
self.unit = unit
self.label = label
self._selectorLabel = selectorLabel
@property
def selectorLabel(self):
if self._selectorLabel is not None:
return self._selectorLabel
return self.label
def __hash__(self):
return hash((self.handle, self.unit, self.label, self._selectorLabel))
def __eq__(self, other):
if not isinstance(other, YDef):
return False
return all((
self.handle == other.handle,
self.unit == other.unit,
self.label == other.label,
self._selectorLabel == other._selectorLabel))
class XDef:
def __init__(self, handle, unit, label, mainInput, selectorLabel=None):
self.handle = handle
self.unit = unit
self.label = label
self.mainInput = mainInput
self._selectorLabel = selectorLabel
@property
def selectorLabel(self):
if self._selectorLabel is not None:
return self._selectorLabel
return self.label
def __hash__(self):
return hash((self.handle, self.unit, self.label, self.mainInput, self._selectorLabel))
def __eq__(self, other):
if not isinstance(other, XDef):
return False
return all((
self.handle == other.handle,
self.unit == other.unit,
self.label == other.label,
self.mainInput == other.mainInput,
self._selectorLabel == other._selectorLabel))
class Input:
def __init__(self, handle, unit, label, iconID, defaultValue, defaultRange, mainTooltip=None, secondaryTooltip=None, conditions=()):
self.handle = handle
self.unit = unit
self.label = label
self.iconID = iconID
self.defaultValue = defaultValue
self.defaultRange = defaultRange
self.mainTooltip = mainTooltip
self.secondaryTooltip = secondaryTooltip
# Format: ((x condition, y condition), (x condition, y condition), ...)
self.conditions = tuple(conditions)
def __hash__(self):
return hash((self.handle, self.unit, self.label, self.iconID, self.defaultValue, self.defaultRange, self.mainTooltip, self.secondaryTooltip, self.conditions))
def __eq__(self, other):
if not isinstance(other, Input):
return False
return all((
self.handle == other.handle,
self.unit == other.unit,
self.label == other.label,
self.iconID == other.iconID,
self.defaultValue == other.defaultValue,
self.defaultRange == other.defaultRange,
self.mainTooltip == other.mainTooltip,
self.secondaryTooltip == other.secondaryTooltip,
self.conditions == other.conditions))
class InputCheckbox:
def __init__(self, handle, label, defaultValue, conditions=()):
self.handle = handle
self.label = label
self.defaultValue = defaultValue
# Format: ((x condition, y condition), (x condition, y condition), ...)
self.conditions = tuple(conditions)
def __hash__(self):
return hash((self.handle, self.label, self.defaultValue, self.conditions))
def __eq__(self, other):
if not isinstance(other, Input):
return False
return all((
self.handle == other.handle,
self.label == other.label,
self.defaultValue == other.defaultValue,
self.conditions == other.conditions))

View File

@@ -0,0 +1,95 @@
# =============================================================================
# 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 math
from abc import ABCMeta, abstractmethod
class PointGetter(metaclass=ABCMeta):
def __init__(self, graph):
self.graph = graph
@abstractmethod
def getRange(self, xRange, miscParams, src, tgt):
raise NotImplementedError
@abstractmethod
def getPoint(self, x, miscParams, src, tgt):
raise NotImplementedError
class SmoothPointGetter(PointGetter, metaclass=ABCMeta):
_baseResolution = 200
_extraDepth = 0
def getRange(self, xRange, miscParams, src, tgt):
xs = []
ys = []
commonData = self._getCommonData(miscParams=miscParams, src=src, tgt=tgt)
def addExtraPoints(x1, y1, x2, y2, depth):
if depth <= 0 or y1 == y2:
return
newX = (x1 + x2) / 2
newY = self._calculatePoint(x=newX, miscParams=miscParams, src=src, tgt=tgt, commonData=commonData)
addExtraPoints(x1=prevX, y1=prevY, x2=newX, y2=newY, depth=depth - 1)
xs.append(newX)
ys.append(newY)
addExtraPoints(x1=newX, y1=newY, x2=x2, y2=y2, depth=depth - 1)
prevX = None
prevY = None
# Go through X points defined by our resolution setting
for x in self._xIterLinear(xRange):
y = self._calculatePoint(x=x, miscParams=miscParams, src=src, tgt=tgt, commonData=commonData)
if prevX is not None and prevY is not None:
# And if Y values of adjacent data points are not equal, add extra points
# depending on extra depth setting
addExtraPoints(x1=prevX, y1=prevY, x2=x, y2=y, depth=self._extraDepth)
prevX = x
prevY = y
xs.append(x)
ys.append(y)
return xs, ys
def getPoint(self, x, miscParams, src, tgt):
commonData = self._getCommonData(miscParams=miscParams, src=src, tgt=tgt)
return self._calculatePoint(x=x, miscParams=miscParams, src=src, tgt=tgt, commonData=commonData)
def _xIterLinear(self, xRange):
xLow = min(xRange)
xHigh = max(xRange)
# Resolution defines amount of ranges between points here,
# not amount of points
step = (xHigh - xLow) / self._baseResolution
if step == 0 or math.isnan(step):
yield xLow
else:
for i in range(self._baseResolution + 1):
yield xLow + step * i
def _getCommonData(self, miscParams, src, tgt):
return {}
@abstractmethod
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
raise NotImplementedError

298
graphs/data/base/graph.py Normal file
View File

@@ -0,0 +1,298 @@
# =============================================================================
# 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 math
from abc import ABCMeta, abstractmethod
from collections import OrderedDict
from eos.utils.float import floatUnerr
from service.const import GraphCacheCleanupReason
class FitGraph(metaclass=ABCMeta):
# UI stuff
views = []
viewMap = {}
@classmethod
def register(cls):
FitGraph.views.append(cls)
FitGraph.viewMap[cls.internalName] = cls
def __init__(self):
# Format: {(fit ID, target type, target ID): {(xSpec, ySpec): (xs, ys)}}
self._plotCache = {}
# Format: {(fit ID, target type, target ID): {(xSpec, ySpec): {x: y}}}
self._pointCache = {}
@property
@abstractmethod
def name(self):
raise NotImplementedError
@property
@abstractmethod
def internalName(self):
raise NotImplementedError
@property
@abstractmethod
def yDefs(self):
raise NotImplementedError
@property
def yDefMap(self):
return OrderedDict(((y.handle, y.unit), y) for y in self.yDefs)
@property
@abstractmethod
def xDefs(self):
raise NotImplementedError
@property
def xDefMap(self):
return OrderedDict(((x.handle, x.unit), x) for x in self.xDefs)
@property
def inputs(self):
raise NotImplementedError
@property
def inputMap(self):
return OrderedDict(((i.handle, i.unit), i) for i in self.inputs)
checkboxes = ()
@property
def checkboxesMap(self):
return OrderedDict((ec.handle, ec) for ec in self.checkboxes)
hasTargets = False
srcVectorDef = None
tgtVectorDef = None
srcExtraCols = ()
tgtExtraCols = ()
usesHpEffectivity = False
def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, src, tgt=None):
cacheKey = self._makeCacheKey(src=src, tgt=tgt)
try:
plotData = self._plotCache[cacheKey][(ySpec, xSpec)]
except KeyError:
plotData = self._calcPlotPoints(
mainInput=mainInput, miscInputs=miscInputs,
xSpec=xSpec, ySpec=ySpec, src=src, tgt=tgt)
self._plotCache.setdefault(cacheKey, {})[(ySpec, xSpec)] = plotData
return plotData
def getPoint(self, x, miscInputs, xSpec, ySpec, src, tgt=None):
cacheKey = self._makeCacheKey(src=src, tgt=tgt)
try:
y = self._pointCache[cacheKey][(ySpec, xSpec)][x]
except KeyError:
y = self._calcPoint(x=x, miscInputs=miscInputs, xSpec=xSpec, ySpec=ySpec, src=src, tgt=tgt)
self._pointCache.setdefault(cacheKey, {}).setdefault((ySpec, xSpec), {})[x] = y
return y
def clearCache(self, reason, extraData=None):
caches = (self._plotCache, self._pointCache)
plotKeysToClear = set()
# If fit changed - clear plots which concern this fit
if reason in (GraphCacheCleanupReason.fitChanged, GraphCacheCleanupReason.fitRemoved):
for cache in caches:
for cacheKey in cache:
cacheFitID, cacheTgtType, cacheTgtID = cacheKey
if extraData == cacheFitID:
plotKeysToClear.add(cacheKey)
elif cacheTgtType == 'fit' and extraData == cacheTgtID:
plotKeysToClear.add(cacheKey)
# Same for profile
elif reason in (GraphCacheCleanupReason.profileChanged, GraphCacheCleanupReason.profileRemoved):
for cache in caches:
for cacheKey in cache:
cacheFitID, cacheTgtType, cacheTgtID = cacheKey
if cacheTgtType == 'profile' and extraData == cacheTgtID:
plotKeysToClear.add(cacheKey)
# Target fit resist mode changed
elif reason == GraphCacheCleanupReason.resistModeChanged:
for cache in caches:
for cacheKey in cache:
cacheFitID, cacheTgtType, cacheTgtID = cacheKey
if cacheTgtType == 'fit' and extraData == cacheTgtID:
plotKeysToClear.add(cacheKey)
# Wipe out whole plot cache otherwise
else:
for cache in caches:
for cacheKey in cache:
plotKeysToClear.add(cacheKey)
# Do actual cleanup
for cache in caches:
for cacheKey in plotKeysToClear:
try:
del cache[cacheKey]
except KeyError:
pass
# Process any internal caches graphs might have
self._clearInternalCache(reason, extraData)
def _makeCacheKey(self, src, tgt):
if tgt is not None and tgt.isFit:
tgtType = 'fit'
tgtItemID = tgt.item.ID
elif tgt is not None and tgt.isProfile:
tgtType = 'profile'
tgtItemID = tgt.item.ID
else:
tgtType = None
tgtItemID = None
cacheKey = (src.item.ID, tgtType, tgtItemID)
return cacheKey
def _clearInternalCache(self, reason, extraData):
return
# Calculation stuff
def _calcPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, src, tgt):
mainParamRange = self._normalizeMain(mainInput=mainInput, src=src, tgt=tgt)
miscParams = self._normalizeMisc(miscInputs=miscInputs, src=src, tgt=tgt)
mainParamRange = self._limitMain(mainParamRange=mainParamRange, src=src, tgt=tgt)
miscParams = self._limitMisc(miscParams=miscParams, src=src, tgt=tgt)
xs, ys = self._getPlotPoints(
xRange=mainParamRange[1], miscParams=miscParams,
xSpec=xSpec, ySpec=ySpec, src=src, tgt=tgt)
ys = self._denormalizeValues(values=ys, axisSpec=ySpec, src=src, tgt=tgt)
# Sometimes x denormalizer may fail (e.g. during conversion of 0 ship speed to %).
# If both inputs and outputs are in %, do some extra processing to at least have
# proper graph which shows the same value over whole specified relative parameter
# range
try:
xs = self._denormalizeValues(values=xs, axisSpec=xSpec, src=src, tgt=tgt)
except ZeroDivisionError:
if mainInput.unit == xSpec.unit == '%' and len(set(floatUnerr(y) for y in ys)) == 1:
xs = [min(mainInput.value), max(mainInput.value)]
ys = [ys[0], ys[0]]
else:
raise
else:
# Same for NaN which means we tried to denormalize infinity values, which might be the
# case for the ideal target profile with infinite signature radius
if mainInput.unit == xSpec.unit == '%' and all(math.isnan(x) for x in xs):
xs = [min(mainInput.value), max(mainInput.value)]
ys = [ys[0], ys[0]]
return xs, ys
def _calcPoint(self, x, miscInputs, xSpec, ySpec, src, tgt):
x = self._normalizeValue(value=x, axisSpec=xSpec, src=src, tgt=tgt)
miscParams = self._normalizeMisc(miscInputs=miscInputs, src=src, tgt=tgt)
miscParams = self._limitMisc(miscParams=miscParams, src=src, tgt=tgt)
y = self._getPoint(x=x, miscParams=miscParams, xSpec=xSpec, ySpec=ySpec, src=src, tgt=tgt)
y = self._denormalizeValue(value=y, axisSpec=ySpec, src=src, tgt=tgt)
return y
_normalizers = {}
def _normalizeMain(self, mainInput, src, tgt):
key = (mainInput.handle, mainInput.unit)
if key in self._normalizers:
normalizer = self._normalizers[key]
mainParamRange = (mainInput.handle, tuple(normalizer(v, src, tgt) for v in mainInput.value))
else:
mainParamRange = (mainInput.handle, mainInput.value)
return mainParamRange
def _normalizeMisc(self, miscInputs, src, tgt):
miscParams = {}
for miscInput in miscInputs:
key = (miscInput.handle, miscInput.unit)
if key in self._normalizers:
normalizer = self._normalizers[key]
miscParams[miscInput.handle] = normalizer(miscInput.value, src, tgt)
else:
miscParams[miscInput.handle] = miscInput.value
return miscParams
def _normalizeValue(self, value, axisSpec, src, tgt):
key = (axisSpec.handle, axisSpec.unit)
if key in self._normalizers:
normalizer = self._normalizers[key]
value = normalizer(value, src, tgt)
return value
_limiters = {}
def _limitMain(self, mainParamRange, src, tgt):
mainHandle, mainValue = mainParamRange
if mainHandle in self._limiters:
limiter = self._limiters[mainHandle]
mainParamRange = (mainHandle, tuple(self.__limitToRange(v, limiter(src, tgt)) for v in mainValue))
return mainParamRange
def _limitMisc(self, miscParams, src, tgt):
for miscHandle in miscParams:
if miscHandle in self._limiters:
limiter = self._limiters[miscHandle]
miscValue = miscParams[miscHandle]
miscParams[miscHandle] = self.__limitToRange(miscValue, limiter(src, tgt))
return miscParams
@staticmethod
def __limitToRange(val, limitRange):
if val is None:
return None
val = max(val, min(limitRange))
val = min(val, max(limitRange))
return val
_getters = {}
def _getPlotPoints(self, xRange, miscParams, xSpec, ySpec, src, tgt):
try:
getterClass = self._getters[(xSpec.handle, ySpec.handle)]
except KeyError:
return [], []
else:
getter = getterClass(graph=self)
return getter.getRange(xRange=xRange, miscParams=miscParams, src=src, tgt=tgt)
def _getPoint(self, x, miscParams, xSpec, ySpec, src, tgt):
try:
getterClass = self._getters[(xSpec.handle, ySpec.handle)]
except KeyError:
return [], []
else:
getter = getterClass(graph=self)
return getter.getPoint(x=x, miscParams=miscParams, src=src, tgt=tgt)
_denormalizers = {}
def _denormalizeValues(self, values, axisSpec, src, tgt):
key = (axisSpec.handle, axisSpec.unit)
if key in self._denormalizers:
denormalizer = self._denormalizers[key]
values = [denormalizer(v, src, tgt) for v in values]
return values
def _denormalizeValue(self, value, axisSpec, src, tgt):
key = (axisSpec.handle, axisSpec.unit)
if key in self._denormalizers:
denormalizer = self._denormalizers[key]
value = denormalizer(value, src, tgt)
return value

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 FitCapacitorGraph
FitCapacitorGraph.register()

View File

@@ -0,0 +1,197 @@
# =============================================================================
# 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 math
from graphs.data.base import SmoothPointGetter
class Time2CapAmountGetter(SmoothPointGetter):
def getRange(self, xRange, miscParams, src, tgt):
# Use smooth getter when we're not using cap sim
if not miscParams['useCapsim']:
return super().getRange(xRange=xRange, miscParams=miscParams, src=src, tgt=tgt)
capAmountT0 = miscParams['capAmountT0'] or 0
capSimDataRaw = src.item.getCapSimData(startingCap=capAmountT0)
# Same here, no cap sim data - use smooth getter which considers only regen
if not capSimDataRaw:
return super().getRange(xRange=xRange, miscParams=miscParams, src=src, tgt=tgt)
capSimDataMaxTime = capSimDataRaw[-1][0]
minTime, maxTime = xRange
maxTime = min(maxTime, capSimDataMaxTime)
maxPointXDistance = (maxTime - minTime) / self._baseResolution
capSimDataInRange = {k: v for k, v in capSimDataRaw if minTime <= k <= maxTime}
prevTime = minTime
xs = []
ys = []
capSimDataBefore = {k: v for k, v in capSimDataRaw if k < minTime}
# When time range lies to the right of last cap sim data point, return nothing
if len(capSimDataBefore) > 0 and max(capSimDataBefore) == capSimDataMaxTime:
return xs, ys
maxCapAmount = src.item.ship.getModifiedItemAttr('capacitorCapacity')
capRegenTime = src.item.ship.getModifiedItemAttr('rechargeRate') / 1000
def plotCapRegen(prevTime, prevCap, currentTime):
subrangeAmount = math.ceil((currentTime - prevTime) / maxPointXDistance)
subrangeLength = (currentTime - prevTime) / subrangeAmount
for i in range(1, subrangeAmount + 1):
subrangeTime = prevTime + subrangeLength * i
subrangeCap = calculateCapAmount(
maxCapAmount=maxCapAmount,
capRegenTime=capRegenTime,
capAmountT0=prevCap,
time=subrangeTime - prevTime)
xs.append(subrangeTime)
ys.append(subrangeCap)
# Calculate starting cap for first value seen in our range
if capSimDataBefore:
timeBefore = max(capSimDataBefore)
capBefore = capSimDataBefore[timeBefore]
prevCap = calculateCapAmount(
maxCapAmount=maxCapAmount,
capRegenTime=capRegenTime,
capAmountT0=capBefore,
time=prevTime - timeBefore)
else:
prevCap = calculateCapAmount(
maxCapAmount=maxCapAmount,
capRegenTime=capRegenTime,
capAmountT0=capAmountT0,
time=prevTime)
xs.append(prevTime)
ys.append(prevCap)
for currentTime in sorted(capSimDataInRange):
if currentTime > prevTime:
plotCapRegen(prevTime=prevTime, prevCap=prevCap, currentTime=currentTime)
currentCap = capSimDataInRange[currentTime]
xs.append(currentTime)
ys.append(currentCap)
prevTime = currentTime
prevCap = currentCap
if maxTime > prevTime:
plotCapRegen(prevTime=prevTime, prevCap=prevCap, currentTime=maxTime)
return xs, ys
def getPoint(self, x, miscParams, src, tgt):
# Use smooth getter when we're not using cap sim
if not miscParams['useCapsim']:
return super().getPoint(x=x, miscParams=miscParams, src=src, tgt=tgt)
capAmountT0 = miscParams['capAmountT0'] or 0
capSimDataRaw = src.item.getCapSimData(startingCap=capAmountT0)
# Same here, no cap sim data - use smooth getter which considers only regen
if not capSimDataRaw:
return super().getPoint(x=x, miscParams=miscParams, src=src, tgt=tgt)
currentTime = x
capSimDataBefore = {k: v for k, v in capSimDataRaw if k <= currentTime}
capSimDataMaxTime = capSimDataRaw[-1][0]
# When time range lies to the right of last cap sim data point, return nothing
if len(capSimDataBefore) > 0 and max(capSimDataBefore) == capSimDataMaxTime:
return None
maxCapAmount = src.item.ship.getModifiedItemAttr('capacitorCapacity')
capRegenTime = src.item.ship.getModifiedItemAttr('rechargeRate') / 1000
if capSimDataBefore:
timeBefore = max(capSimDataBefore)
capBefore = capSimDataBefore[timeBefore]
if timeBefore == currentTime:
currentCap = capBefore
else:
currentCap = calculateCapAmount(
maxCapAmount=maxCapAmount,
capRegenTime=capRegenTime,
capAmountT0=capBefore,
time=currentTime - timeBefore)
else:
currentCap = calculateCapAmount(
maxCapAmount=maxCapAmount,
capRegenTime=capRegenTime,
capAmountT0=capAmountT0,
time=currentTime)
return currentCap
def _getCommonData(self, miscParams, src, tgt):
return {
'maxCapAmount': src.item.ship.getModifiedItemAttr('capacitorCapacity'),
'capRegenTime': src.item.ship.getModifiedItemAttr('rechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
capAmount = calculateCapAmount(
maxCapAmount=commonData['maxCapAmount'],
capRegenTime=commonData['capRegenTime'],
capAmountT0=miscParams['capAmountT0'] or 0,
time=time)
return capAmount
class Time2CapRegenGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxCapAmount': src.item.ship.getModifiedItemAttr('capacitorCapacity'),
'capRegenTime': src.item.ship.getModifiedItemAttr('rechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
capAmount = calculateCapAmount(
maxCapAmount=commonData['maxCapAmount'],
capRegenTime=commonData['capRegenTime'],
capAmountT0=miscParams['capAmountT0'] or 0,
time=time)
capRegen = calculateCapRegen(
maxCapAmount=commonData['maxCapAmount'],
capRegenTime=commonData['capRegenTime'],
currentCapAmount=capAmount)
return capRegen
# Useless, but valid combination of x and y
class CapAmount2CapAmountGetter(SmoothPointGetter):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
capAmount = x
return capAmount
class CapAmount2CapRegenGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxCapAmount': src.item.ship.getModifiedItemAttr('capacitorCapacity'),
'capRegenTime': src.item.ship.getModifiedItemAttr('rechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
capAmount = x
capRegen = calculateCapRegen(
maxCapAmount=commonData['maxCapAmount'],
capRegenTime=commonData['capRegenTime'],
currentCapAmount=capAmount)
return capRegen
def calculateCapAmount(maxCapAmount, capRegenTime, capAmountT0, time):
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
return maxCapAmount * (1 + math.exp(5 * -time / capRegenTime) * (math.sqrt(capAmountT0 / maxCapAmount) - 1)) ** 2
def calculateCapRegen(maxCapAmount, capRegenTime, currentCapAmount):
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
return 10 * maxCapAmount / capRegenTime * (math.sqrt(currentCapAmount / maxCapAmount) - currentCapAmount / maxCapAmount)

View File

@@ -0,0 +1,62 @@
# =============================================================================
# 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 graphs.data.base import FitGraph, XDef, YDef, Input, InputCheckbox
from .getter import CapAmount2CapAmountGetter, CapAmount2CapRegenGetter, Time2CapAmountGetter, Time2CapRegenGetter
class FitCapacitorGraph(FitGraph):
# UI stuff
internalName = 'capacitorGraph'
name = 'Capacitor'
xDefs = [
XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')),
XDef(handle='capAmount', unit='GJ', label='Cap amount', mainInput=('capAmount', '%')),
XDef(handle='capAmount', unit='%', label='Cap amount', mainInput=('capAmount', '%'))]
yDefs = [
YDef(handle='capAmount', unit='GJ', label='Cap amount'),
YDef(handle='capRegen', unit='GJ/s', label='Cap regen')]
inputs = [
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), conditions=[
(('time', 's'), None)]),
Input(handle='capAmount', unit='%', label='Cap amount', iconID=1668, defaultValue=25, defaultRange=(0, 100), conditions=[
(('capAmount', 'GJ'), None),
(('capAmount', '%'), None)]),
Input(handle='capAmountT0', unit='%', label='Starting cap amount', iconID=1668, defaultValue=100, defaultRange=(0, 100), conditions=[
(('time', 's'), None)])]
checkboxes = [InputCheckbox(handle='useCapsim', label='Use capacitor simulator', defaultValue=True, conditions=[
(('time', 's'), ('capAmount', 'GJ'))])]
srcExtraCols = ('CapAmount', 'CapTime')
# Calculation stuff
_normalizers = {
('capAmount', '%'): lambda v, src, tgt: v / 100 * src.item.ship.getModifiedItemAttr('capacitorCapacity'),
('capAmountT0', '%'): lambda v, src, tgt: None if v is None else v / 100 * src.item.ship.getModifiedItemAttr('capacitorCapacity')}
_limiters = {
'time': lambda src, tgt: (0, 3600),
'capAmount': lambda src, tgt: (0, src.item.ship.getModifiedItemAttr('capacitorCapacity')),
'capAmountT0': lambda src, tgt: (0, src.item.ship.getModifiedItemAttr('capacitorCapacity'))}
_getters = {
('time', 'capAmount'): Time2CapAmountGetter,
('time', 'capRegen'): Time2CapRegenGetter,
('capAmount', 'capAmount'): CapAmount2CapAmountGetter,
('capAmount', 'capRegen'): CapAmount2CapRegenGetter}
_denormalizers = {('capAmount', '%'): lambda v, src, tgt: v * 100 / src.item.ship.getModifiedItemAttr('capacitorCapacity')}

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 FitDamageStatsGraph
FitDamageStatsGraph.register()

View File

@@ -0,0 +1,22 @@
# =============================================================================
# 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 .projected import ProjectedDataCache
from .time import TimeCache

View File

@@ -0,0 +1,121 @@
# =============================================================================
# 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 collections import namedtuple
from eos.modifiedAttributeDict import getResistanceAttrID
from graphs.data.base import FitDataCache
ModProjData = namedtuple('ModProjData', ('boost', 'optimal', 'falloff', 'stackingGroup', 'resAttrID'))
MobileProjData = namedtuple('MobileProjData', ('boost', 'optimal', 'falloff', 'stackingGroup', 'resAttrID', 'speed', 'radius'))
class ProjectedDataCache(FitDataCache):
def getProjModData(self, src):
try:
projectedData = self._data[src.item.ID]['modules']
except KeyError:
# Format of items for both: (boost strength, optimal, falloff, stacking group, resistance attr ID)
webMods = []
tpMods = []
projectedData = self._data.setdefault(src.item.ID, {})['modules'] = (webMods, tpMods)
for mod in src.item.activeModulesIter():
for webEffectName in ('remoteWebifierFalloff', 'structureModuleEffectStasisWebifier'):
if webEffectName in mod.item.effects:
webMods.append(ModProjData(
mod.getModifiedItemAttr('speedFactor'),
mod.maxRange or 0,
mod.falloff or 0,
'default',
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects[webEffectName])))
if 'doomsdayAOEWeb' in mod.item.effects:
webMods.append(ModProjData(
mod.getModifiedItemAttr('speedFactor'),
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0,
'default',
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects['doomsdayAOEWeb'])))
for tpEffectName in ('remoteTargetPaintFalloff', 'structureModuleEffectTargetPainter'):
if tpEffectName in mod.item.effects:
tpMods.append(ModProjData(
mod.getModifiedItemAttr('signatureRadiusBonus'),
mod.maxRange or 0,
mod.falloff or 0,
'default',
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects[tpEffectName])))
if 'doomsdayAOEPaint' in mod.item.effects:
tpMods.append(ModProjData(
mod.getModifiedItemAttr('signatureRadiusBonus'),
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0,
'default',
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects['doomsdayAOEPaint'])))
return projectedData
def getProjDroneData(self, src):
try:
projectedData = self._data[src.item.ID]['drones']
except KeyError:
# Format of items for both: (boost strength, optimal, falloff, stacking group, resistance attr ID, drone speed, drone radius)
webDrones = []
tpDrones = []
projectedData = self._data.setdefault(src.item.ID, {})['drones'] = (webDrones, tpDrones)
for drone in src.item.activeDronesIter():
if 'remoteWebifierEntity' in drone.item.effects:
webDrones.extend(drone.amountActive * (MobileProjData(
drone.getModifiedItemAttr('speedFactor'),
drone.maxRange or 0,
drone.falloff or 0,
'default',
getResistanceAttrID(modifyingItem=drone, effect=drone.item.effects['remoteWebifierEntity']),
drone.getModifiedItemAttr('maxVelocity'),
drone.getModifiedItemAttr('radius')),))
if 'remoteTargetPaintEntity' in drone.item.effects:
tpDrones.extend(drone.amountActive * (MobileProjData(
drone.getModifiedItemAttr('signatureRadiusBonus'),
drone.maxRange or 0,
drone.falloff or 0,
'default',
getResistanceAttrID(modifyingItem=drone, effect=drone.item.effects['remoteTargetPaintEntity']),
drone.getModifiedItemAttr('maxVelocity'),
drone.getModifiedItemAttr('radius')),))
return projectedData
def getProjFighterData(self, src):
try:
projectedData = self._data[src.item.ID]['fighters']
except KeyError:
# Format of items for both: (boost strength, optimal, falloff, stacking group, resistance attr ID, fighter speed, fighter radius)
webFighters = []
tpFighters = []
projectedData = self._data.setdefault(src.item.ID, {})['fighters'] = (webFighters, tpFighters)
for fighter, ability in src.item.activeFighterAbilityIter():
if ability.effect.name == 'fighterAbilityStasisWebifier':
webFighters.append(MobileProjData(
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierSpeedPenalty') * fighter.amount,
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierOptimalRange'),
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierFalloffRange'),
'default',
getResistanceAttrID(modifyingItem=fighter, effect=fighter.item.effects['fighterAbilityStasisWebifier']),
fighter.getModifiedItemAttr('maxVelocity'),
fighter.getModifiedItemAttr('radius')))
return projectedData

254
graphs/data/fitDamageStats/cache/time.py vendored Normal file
View File

@@ -0,0 +1,254 @@
# =============================================================================
# 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 copy import copy
from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import SpoolOptions, SpoolType
from eos.utils.stats import DmgTypes
from graphs.data.base import FitDataCache
class TimeCache(FitDataCache):
# Whole data getters
def getDpsData(self, src):
"""Return DPS data in {time: {key: dps}} format."""
return self._data[src.item.ID]['finalDps']
def getVolleyData(self, src):
"""Return volley data in {time: {key: volley}} format."""
return self._data[src.item.ID]['finalVolley']
def getDmgData(self, src):
"""Return inflicted damage data in {time: {key: damage}} format."""
return self._data[src.item.ID]['finalDmg']
# Specific data point getters
def getDpsDataPoint(self, src, time):
"""Get DPS data by specified time in {key: dps} format."""
return self._getDataPoint(src=src, time=time, dataFunc=self.getDpsData)
def getVolleyDataPoint(self, src, time):
"""Get volley data by specified time in {key: volley} format."""
return self._getDataPoint(src=src, time=time, dataFunc=self.getVolleyData)
def getDmgDataPoint(self, src, time):
"""Get inflicted damage data by specified time in {key: dmg} format."""
return self._getDataPoint(src=src, time=time, dataFunc=self.getDmgData)
# Preparation functions
def prepareDpsData(self, src, maxTime):
self._prepareDpsVolleyData(src=src, maxTime=maxTime)
def prepareVolleyData(self, src, maxTime):
self._prepareDpsVolleyData(src=src, maxTime=maxTime)
def prepareDmgData(self, src, maxTime):
# Time is none means that time parameter has to be ignored,
# we do not need cache for that
if maxTime is None:
return
self._generateInternalForm(src=src, maxTime=maxTime)
fitCache = self._data[src.item.ID]
# Final cache has been generated already, don't do anything
if 'finalDmg' in fitCache:
return
intCache = fitCache['internalDmg']
changesByTime = {}
for key, dmgMap in intCache.items():
for time in dmgMap:
changesByTime.setdefault(time, []).append(key)
# Here we convert cache to following format:
# {time: {key: damage done by key at this time}}
finalCache = fitCache['finalDmg'] = {}
timeDmgData = {}
for time in sorted(changesByTime):
timeDmgData = copy(timeDmgData)
for key in changesByTime[time]:
keyDmg = intCache[key][time]
if key in timeDmgData:
timeDmgData[key] = timeDmgData[key] + keyDmg
else:
timeDmgData[key] = keyDmg
finalCache[time] = timeDmgData
# We do not need internal cache once we have final
del fitCache['internalDmg']
# Private stuff
def _prepareDpsVolleyData(self, src, maxTime):
# Time is none means that time parameter has to be ignored,
# we do not need cache for that
if maxTime is None:
return True
self._generateInternalForm(src=src, maxTime=maxTime)
fitCache = self._data[src.item.ID]
# Final cache has been generated already, don't do anything
if 'finalDps' in fitCache and 'finalVolley' in fitCache:
return
# Convert cache from segments with assigned values into points
# which are located at times when dps/volley values change
pointCache = {}
for key, dmgList in fitCache['internalDpsVolley'].items():
pointData = pointCache[key] = {}
prevDps = None
prevVolley = None
prevTimeEnd = None
for timeStart, timeEnd, dps, volley in dmgList:
# First item
if not pointData:
pointData[timeStart] = (dps, volley)
# Gap between items
elif floatUnerr(prevTimeEnd) < floatUnerr(timeStart):
pointData[prevTimeEnd] = (DmgTypes(0, 0, 0, 0), DmgTypes(0, 0, 0, 0))
pointData[timeStart] = (dps, volley)
# Changed value
elif dps != prevDps or volley != prevVolley:
pointData[timeStart] = (dps, volley)
prevDps = dps
prevVolley = volley
prevTimeEnd = timeEnd
# We have data in another form, do not need old one any longer
del fitCache['internalDpsVolley']
changesByTime = {}
for key, dmgMap in pointCache.items():
for time in dmgMap:
changesByTime.setdefault(time, []).append(key)
# Here we convert cache to following format:
# {time: {key: (dps, volley}}
finalDpsCache = fitCache['finalDps'] = {}
finalVolleyCache = fitCache['finalVolley'] = {}
timeDpsData = {}
timeVolleyData = {}
for time in sorted(changesByTime):
timeDpsData = copy(timeDpsData)
timeVolleyData = copy(timeVolleyData)
for key in changesByTime[time]:
dps, volley = pointCache[key][time]
timeDpsData[key] = dps
timeVolleyData[key] = volley
finalDpsCache[time] = timeDpsData
finalVolleyCache[time] = timeVolleyData
def _generateInternalForm(self, src, maxTime):
if self._isTimeCacheValid(src=src, maxTime=maxTime):
return
fitCache = self._data[src.item.ID] = {'maxTime': maxTime}
intCacheDpsVolley = fitCache['internalDpsVolley'] = {}
intCacheDmg = fitCache['internalDmg'] = {}
def addDpsVolley(ddKey, addedTimeStart, addedTimeFinish, addedVolleys):
if not addedVolleys:
return
volleySum = sum(addedVolleys, DmgTypes(0, 0, 0, 0))
if volleySum.total > 0:
addedDps = volleySum / (addedTimeFinish - addedTimeStart)
# We can take "just best" volley, no matter target resistances, because all
# known items have the same damage type ratio throughout their cycle - and
# applying resistances doesn't change final outcome
bestVolley = max(addedVolleys, key=lambda v: v.total)
ddCacheDps = intCacheDpsVolley.setdefault(ddKey, [])
ddCacheDps.append((addedTimeStart, addedTimeFinish, addedDps, bestVolley))
def addDmg(ddKey, addedTime, addedDmg):
if addedDmg.total == 0:
return
intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg
# Modules
for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
cycleParams = mod.getCycleParameters(reloadOverride=True)
if cycleParams is None:
continue
currentTime = 0
nonstopCycles = 0
for cycleTimeMs, inactiveTimeMs, isInactivityReload in cycleParams.iterCycles():
cycleVolleys = []
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
for volleyTimeMs, volley in volleyParams.items():
cycleVolleys.append(volley)
addDmg(mod, currentTime + volleyTimeMs / 1000, volley)
addDpsVolley(mod, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
if inactiveTimeMs > 0:
nonstopCycles = 0
else:
nonstopCycles += 1
if currentTime > maxTime:
break
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
# Drones
for drone in src.item.activeDronesIter():
if not drone.isDealingDamage():
continue
cycleParams = drone.getCycleParameters(reloadOverride=True)
if cycleParams is None:
continue
currentTime = 0
volleyParams = drone.getVolleyParameters()
for cycleTimeMs, inactiveTimeMs, isInactivityReload in cycleParams.iterCycles():
cycleVolleys = []
for volleyTimeMs, volley in volleyParams.items():
cycleVolleys.append(volley)
addDmg(drone, currentTime + volleyTimeMs / 1000, volley)
addDpsVolley(drone, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
if currentTime > maxTime:
break
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
# Fighters
for fighter in src.item.activeFightersIter():
if not fighter.isDealingDamage():
continue
cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True)
if cycleParams is None:
continue
volleyParams = fighter.getVolleyParametersPerEffect()
for effectID, abilityCycleParams in cycleParams.items():
if effectID not in volleyParams:
continue
currentTime = 0
abilityVolleyParams = volleyParams[effectID]
for cycleTimeMs, inactiveTimeMs, isInactivityReload in abilityCycleParams.iterCycles():
cycleVolleys = []
for volleyTimeMs, volley in abilityVolleyParams.items():
cycleVolleys.append(volley)
addDmg((fighter, effectID), currentTime + volleyTimeMs / 1000, volley)
addDpsVolley((fighter, effectID), currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
if currentTime > maxTime:
break
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
def _isTimeCacheValid(self, src, maxTime):
try:
cacheMaxTime = self._data[src.item.ID]['maxTime']
except KeyError:
return False
return maxTime <= cacheMaxTime
def _getDataPoint(self, src, time, dataFunc):
data = dataFunc(src)
timesBefore = [t for t in data if floatUnerr(t) <= floatUnerr(time)]
try:
time = max(timesBefore)
except ValueError:
return {}
else:
return data[time]

View File

@@ -0,0 +1,18 @@
# =============================================================================
# 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/>.
# =============================================================================

View File

@@ -0,0 +1,369 @@
# =============================================================================
# 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 math
from functools import lru_cache
from eos.const import FittingHardpoint
from eos.utils.float import floatUnerr
from graphs.calc import calculateRangeFactor
from service.attribute import Attribute
from service.const import GraphDpsDroneMode
from service.settings import GraphSettings
def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
applicationMap = {}
for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
if mod.hardpoint == FittingHardpoint.TURRET:
applicationMap[mod] = getTurretMult(
mod=mod,
src=src,
tgt=tgt,
atkSpeed=atkSpeed,
atkAngle=atkAngle,
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius)
elif mod.hardpoint == FittingHardpoint.MISSILE:
applicationMap[mod] = getLauncherMult(
mod=mod,
src=src,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
elif mod.item.group.name in ('Smart Bomb', 'Structure Area Denial Module'):
applicationMap[mod] = getSmartbombMult(
mod=mod,
distance=distance)
elif mod.item.group.name == 'Missile Launcher Bomb':
applicationMap[mod] = getBombMult(
mod=mod,
src=src,
tgt=tgt,
distance=distance,
tgtSigRadius=tgtSigRadius)
elif mod.item.group.name == 'Structure Guided Bomb Launcher':
applicationMap[mod] = getGuidedBombMult(
mod=mod,
src=src,
distance=distance,
tgtSigRadius=tgtSigRadius)
elif mod.item.group.name in ('Super Weapon', 'Structure Doomsday Weapon'):
applicationMap[mod] = getDoomsdayMult(
mod=mod,
tgt=tgt,
distance=distance,
tgtSigRadius=tgtSigRadius)
for drone in src.item.activeDronesIter():
if not drone.isDealingDamage():
continue
applicationMap[drone] = getDroneMult(
drone=drone,
src=src,
tgt=tgt,
atkSpeed=atkSpeed,
atkAngle=atkAngle,
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius)
for fighter in src.item.activeFightersIter():
if not fighter.isDealingDamage():
continue
for ability in fighter.abilities:
if not ability.dealsDamage or not ability.active:
continue
applicationMap[(fighter, ability.effectID)] = getFighterAbilityMult(
fighter=fighter,
ability=ability,
src=src,
tgt=tgt,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
# Ensure consistent results - round off a little to avoid float errors
for k, v in applicationMap.items():
applicationMap[k] = floatUnerr(v)
return applicationMap
# Item application multiplier calculation
def getTurretMult(mod, src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
cth = _calcTurretChanceToHit(
atkSpeed=atkSpeed,
atkAngle=atkAngle,
atkRadius=src.getRadius(),
atkOptimalRange=mod.maxRange or 0,
atkFalloffRange=mod.falloff or 0,
atkTracking=mod.getModifiedItemAttr('trackingSpeed'),
atkOptimalSigRadius=mod.getModifiedItemAttr('optimalSigRadius'),
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtRadius=tgt.getRadius(),
tgtSigRadius=tgtSigRadius)
mult = _calcTurretMult(cth)
return mult
def getLauncherMult(mod, src, distance, tgtSpeed, tgtSigRadius):
modRange = mod.maxRange
if modRange is None:
return 0
if distance is not None and distance + src.getRadius() > modRange:
return 0
mult = _calcMissileFactor(
atkEr=mod.getModifiedChargeAttr('aoeCloudSize'),
atkEv=mod.getModifiedChargeAttr('aoeVelocity'),
atkDrf=mod.getModifiedChargeAttr('aoeDamageReductionFactor'),
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
return mult
def getSmartbombMult(mod, distance):
modRange = mod.maxRange
if modRange is None:
return 0
if distance is not None and distance > modRange:
return 0
return 1
def getDoomsdayMult(mod, tgt, distance, tgtSigRadius):
modRange = mod.maxRange
# Single-target DDs have no range limit
if distance is not None and modRange and distance > modRange:
return 0
# Single-target titan DDs are vs capitals only
if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar'}.intersection(mod.item.effects):
# Disallow only against subcaps, allow against caps and tgt profiles
if tgt.isFit and not tgt.item.ship.item.requiresSkill('Capital Ships'):
return 0
damageSig = mod.getModifiedItemAttr('doomsdayDamageRadius') or mod.getModifiedItemAttr('signatureRadius')
if not damageSig:
return 1
return min(1, tgtSigRadius / damageSig)
def getBombMult(mod, src, tgt, distance, tgtSigRadius):
modRange = mod.maxRange
if modRange is None:
return 0
blastRadius = mod.getModifiedChargeAttr('explosionRange')
atkRadius = src.getRadius()
tgtRadius = tgt.getRadius()
# Bomb starts in the center of the ship
# Also here we assume that it affects target as long as blast
# touches its surface, not center - I did not check this
if distance is not None and distance < max(0, modRange - atkRadius - tgtRadius - blastRadius):
return 0
if distance is not None and distance > max(0, modRange - atkRadius + tgtRadius + blastRadius):
return 0
return _calcBombFactor(
atkEr=mod.getModifiedChargeAttr('aoeCloudSize'),
tgtSigRadius=tgtSigRadius)
def getGuidedBombMult(mod, src, distance, tgtSigRadius):
modRange = mod.maxRange
if modRange is None:
return 0
if distance is not None and distance > modRange - src.getRadius():
return 0
eR = mod.getModifiedChargeAttr('aoeCloudSize')
if eR == 0:
return 1
else:
return min(1, tgtSigRadius / eR)
def getDroneMult(drone, src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
if distance is not None and distance > src.item.extraAttributes['droneControlRange']:
return 0
droneSpeed = drone.getModifiedItemAttr('maxVelocity')
# Hard to simulate drone behavior, so assume chance to hit is 1 for mobile drones
# which catch up with target
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
if (
droneSpeed > 1 and (
(droneOpt == GraphDpsDroneMode.auto and droneSpeed >= tgtSpeed) or
droneOpt == GraphDpsDroneMode.followTarget)
):
cth = 1
# Otherwise put the drone into center of the ship, move it at its max speed or ship's speed
# (whichever is lower) towards direction of attacking ship and see how well it projects
else:
droneRadius = drone.getModifiedItemAttr('radius')
if distance is None:
cthDistance = None
else:
# As distance is ship surface to ship surface, we adjust it according
# to attacker ship's radiuses to have drone surface to ship surface distance
cthDistance = distance + src.getRadius() - droneRadius
cth = _calcTurretChanceToHit(
atkSpeed=min(atkSpeed, droneSpeed),
atkAngle=atkAngle,
atkRadius=droneRadius,
atkOptimalRange=drone.maxRange or 0,
atkFalloffRange=drone.falloff or 0,
atkTracking=drone.getModifiedItemAttr('trackingSpeed'),
atkOptimalSigRadius=drone.getModifiedItemAttr('optimalSigRadius'),
distance=cthDistance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtRadius=tgt.getRadius(),
tgtSigRadius=tgtSigRadius)
mult = _calcTurretMult(cth)
return mult
def getFighterAbilityMult(fighter, ability, src, tgt, distance, tgtSpeed, tgtSigRadius):
fighterSpeed = fighter.getModifiedItemAttr('maxVelocity')
attrPrefix = ability.attrPrefix
# It's bomb attack
if attrPrefix == 'fighterAbilityLaunchBomb':
# Just assume we can land bomb anywhere
return _calcBombFactor(
atkEr=fighter.getModifiedChargeAttr('aoeCloudSize'),
tgtSigRadius=tgtSigRadius)
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
# It's regular missile-based attack
if (droneOpt == GraphDpsDroneMode.auto and fighterSpeed >= tgtSpeed) or droneOpt == GraphDpsDroneMode.followTarget:
rangeFactor = 1
# Same as with drones, if fighters are slower - put them to center of
# the ship and see how they apply
else:
if distance is None:
rangeFactorDistance = None
else:
rangeFactorDistance = distance + src.getRadius() - fighter.getModifiedItemAttr('radius')
rangeFactor = calculateRangeFactor(
srcOptimalRange=fighter.getModifiedItemAttr('{}RangeOptimal'.format(attrPrefix)) or fighter.getModifiedItemAttr('{}Range'.format(attrPrefix)),
srcFalloffRange=fighter.getModifiedItemAttr('{}RangeFalloff'.format(attrPrefix)),
distance=rangeFactorDistance)
drf = fighter.getModifiedItemAttr('{}ReductionFactor'.format(attrPrefix), None)
if drf is None:
drf = fighter.getModifiedItemAttr('{}DamageReductionFactor'.format(attrPrefix))
drs = fighter.getModifiedItemAttr('{}ReductionSensitivity'.format(attrPrefix), None)
if drs is None:
drs = fighter.getModifiedItemAttr('{}DamageReductionSensitivity'.format(attrPrefix))
missileFactor = _calcMissileFactor(
atkEr=fighter.getModifiedItemAttr('{}ExplosionRadius'.format(attrPrefix)),
atkEv=fighter.getModifiedItemAttr('{}ExplosionVelocity'.format(attrPrefix)),
atkDrf=_calcAggregatedDrf(reductionFactor=drf, reductionSensitivity=drs),
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
resistMult = 1
if tgt.isFit:
resistAttrID = fighter.getModifiedItemAttr('{}ResistanceID'.format(attrPrefix))
if resistAttrID:
resistAttrInfo = Attribute.getInstance().getAttributeInfo(resistAttrID)
if resistAttrInfo is not None:
resistMult = tgt.item.ship.getModifiedItemAttr(resistAttrInfo.name, 1)
mult = rangeFactor * missileFactor * resistMult
return mult
# Turret-specific math
@lru_cache(maxsize=50)
def _calcTurretMult(chanceToHit):
"""Calculate damage multiplier for turret-based weapons."""
# https://wiki.eveuniversity.org/Turret_mechanics#Damage
wreckingChance = min(chanceToHit, 0.01)
wreckingPart = wreckingChance * 3
normalChance = chanceToHit - wreckingChance
if normalChance > 0:
avgDamageMult = (0.01 + chanceToHit) / 2 + 0.49
normalPart = normalChance * avgDamageMult
else:
normalPart = 0
totalMult = normalPart + wreckingPart
return totalMult
@lru_cache(maxsize=1000)
def _calcTurretChanceToHit(
atkSpeed, atkAngle, atkRadius, atkOptimalRange, atkFalloffRange, atkTracking, atkOptimalSigRadius,
distance, tgtSpeed, tgtAngle, tgtRadius, tgtSigRadius
):
"""Calculate chance to hit for turret-based weapons."""
# https://wiki.eveuniversity.org/Turret_mechanics#Hit_Math
angularSpeed = _calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius)
# Turrets can be activated regardless of range to target
rangeFactor = calculateRangeFactor(atkOptimalRange, atkFalloffRange, distance, restrictedRange=False)
trackingFactor = _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius)
cth = rangeFactor * trackingFactor
return cth
def _calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius):
"""Calculate angular speed based on mobility parameters of two ships."""
if distance is None:
return 0
atkAngle = atkAngle * math.pi / 180
tgtAngle = tgtAngle * math.pi / 180
ctcDistance = atkRadius + distance + tgtRadius
# Target is to the right of the attacker, so transversal is projection onto Y axis
transSpeed = abs(atkSpeed * math.sin(atkAngle) - tgtSpeed * math.sin(tgtAngle))
if ctcDistance == 0:
return 0 if transSpeed == 0 else math.inf
else:
return transSpeed / ctcDistance
def _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius):
"""Calculate tracking chance to hit component."""
return 0.5 ** (((angularSpeed * atkOptimalSigRadius) / (atkTracking * tgtSigRadius)) ** 2)
# Missile-specific math
@lru_cache(maxsize=200)
def _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius):
"""Missile application."""
factors = [1]
# "Slow" part
if atkEr > 0:
factors.append(tgtSigRadius / atkEr)
# "Fast" part
if tgtSpeed > 0:
factors.append(((atkEv * tgtSigRadius) / (atkEr * tgtSpeed)) ** atkDrf)
totalMult = min(factors)
return totalMult
def _calcAggregatedDrf(reductionFactor, reductionSensitivity):
"""
Sometimes DRF is specified as 2 separate numbers,
here we combine them into generic form.
"""
return math.log(reductionFactor) / math.log(reductionSensitivity)
# Misc math
def _calcBombFactor(atkEr, tgtSigRadius):
if atkEr == 0:
return 1
else:
return min(1, tgtSigRadius / atkEr)

View File

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

View File

@@ -0,0 +1,467 @@
# =============================================================================
# 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.config
from eos.utils.spoolSupport import SpoolOptions, SpoolType
from eos.utils.stats import DmgTypes
from graphs.data.base import PointGetter, SmoothPointGetter
from service.settings import GraphSettings
from .calc.application import getApplicationPerKey
from .calc.projected import getScramRange, getScrammables, getTackledSpeed, getSigRadiusMult
def applyDamage(dmgMap, applicationMap, tgtResists):
total = DmgTypes(em=0, thermal=0, kinetic=0, explosive=0)
for key, dmg in dmgMap.items():
total += dmg * applicationMap.get(key, 0)
if not GraphSettings.getInstance().get('ignoreResists'):
emRes, thermRes, kinRes, exploRes = tgtResists
total = DmgTypes(
em=total.em * (1 - emRes),
thermal=total.thermal * (1 - thermRes),
kinetic=total.kinetic * (1 - kinRes),
explosive=total.explosive * (1 - exploRes))
return total
# Y mixins
class YDpsMixin:
def _getDamagePerKey(self, src, time):
# Use data from time cache if time was not specified
if time is not None:
return self._getTimeCacheDataPoint(src=src, time=time)
# Compose map ourselves using current fit settings if time is not specified
dpsMap = {}
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
dpsMap[mod] = mod.getDps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))
for drone in src.item.activeDronesIter():
if not drone.isDealingDamage():
continue
dpsMap[drone] = drone.getDps()
for fighter in src.item.activeFightersIter():
if not fighter.isDealingDamage():
continue
for effectID, effectDps in fighter.getDpsPerEffect().items():
dpsMap[(fighter, effectID)] = effectDps
return dpsMap
def _prepareTimeCache(self, src, maxTime):
self.graph._timeCache.prepareDpsData(src=src, maxTime=maxTime)
def _getTimeCacheData(self, src):
return self.graph._timeCache.getDpsData(src=src)
def _getTimeCacheDataPoint(self, src, time):
return self.graph._timeCache.getDpsDataPoint(src=src, time=time)
class YVolleyMixin:
def _getDamagePerKey(self, src, time):
# Use data from time cache if time was not specified
if time is not None:
return self._getTimeCacheDataPoint(src=src, time=time)
# Compose map ourselves using current fit settings if time is not specified
volleyMap = {}
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
volleyMap[mod] = mod.getVolley(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))
for drone in src.item.activeDronesIter():
if not drone.isDealingDamage():
continue
volleyMap[drone] = drone.getVolley()
for fighter in src.item.activeFightersIter():
if not fighter.isDealingDamage():
continue
for effectID, effectVolley in fighter.getVolleyPerEffect().items():
volleyMap[(fighter, effectID)] = effectVolley
return volleyMap
def _prepareTimeCache(self, src, maxTime):
self.graph._timeCache.prepareVolleyData(src=src, maxTime=maxTime)
def _getTimeCacheData(self, src):
return self.graph._timeCache.getVolleyData(src=src)
def _getTimeCacheDataPoint(self, src, time):
return self.graph._timeCache.getVolleyDataPoint(src=src, time=time)
class YInflictedDamageMixin:
def _getDamagePerKey(self, src, time):
# Damage inflicted makes no sense without time specified
if time is None:
raise ValueError
return self._getTimeCacheDataPoint(src=src, time=time)
def _prepareTimeCache(self, src, maxTime):
self.graph._timeCache.prepareDmgData(src=src, maxTime=maxTime)
def _getTimeCacheData(self, src):
return self.graph._timeCache.getDmgData(src=src)
def _getTimeCacheDataPoint(self, src, time):
return self.graph._timeCache.getDmgDataPoint(src=src, time=time)
# X mixins
class XDistanceMixin(SmoothPointGetter):
_baseResolution = 50
_extraDepth = 2
def _getCommonData(self, miscParams, src, tgt):
# Prepare time cache here because we need to do it only once,
# and this function is called once per point info fetch
self._prepareTimeCache(src=src, maxTime=miscParams['time'])
applyProjected = GraphSettings.getInstance().get('applyProjected')
return {
'applyProjected': applyProjected,
'srcScramRange': getScramRange(src=src) if applyProjected else None,
'tgtScrammables': getScrammables(tgt=tgt) if applyProjected else (),
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
'tgtResists': tgt.getResists()}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
tgtSpeed = miscParams['tgtSpeed']
tgtSigRadius = tgt.getSigRadius()
if commonData['applyProjected']:
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getTackledSpeed(
src=src,
tgt=tgt,
currentUntackledSpeed=tgtSpeed,
srcScramRange=commonData['srcScramRange'],
tgtScrammables=commonData['tgtScrammables'],
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=distance)
tgtSigRadius = tgtSigRadius * getSigRadiusMult(
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
srcScramRange=commonData['srcScramRange'],
tgtScrammables=commonData['tgtScrammables'],
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,
distance=distance)
applicationMap = getApplicationPerKey(
src=src,
tgt=tgt,
atkSpeed=miscParams['atkSpeed'],
atkAngle=miscParams['atkAngle'],
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=miscParams['tgtAngle'],
tgtSigRadius=tgtSigRadius)
y = applyDamage(
dmgMap=commonData['dmgMap'],
applicationMap=applicationMap,
tgtResists=commonData['tgtResists']).total
return y
class XTimeMixin(PointGetter):
def _prepareApplicationMap(self, miscParams, src, tgt):
tgtSpeed = miscParams['tgtSpeed']
tgtSigRadius = tgt.getSigRadius()
if GraphSettings.getInstance().get('applyProjected'):
srcScramRange = getScramRange(src=src)
tgtScrammables = getScrammables(tgt=tgt)
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getTackledSpeed(
src=src,
tgt=tgt,
currentUntackledSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=miscParams['distance'])
tgtSigRadius = tgtSigRadius * getSigRadiusMult(
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,
distance=miscParams['distance'])
# Get all data we need for all times into maps/caches
applicationMap = getApplicationPerKey(
src=src,
tgt=tgt,
atkSpeed=miscParams['atkSpeed'],
atkAngle=miscParams['atkAngle'],
distance=miscParams['distance'],
tgtSpeed=tgtSpeed,
tgtAngle=miscParams['tgtAngle'],
tgtSigRadius=tgtSigRadius)
return applicationMap
def getRange(self, xRange, miscParams, src, tgt):
xs = []
ys = []
minTime, maxTime = xRange
# Prepare time cache and various shared data
self._prepareTimeCache(src=src, maxTime=maxTime)
timeCache = self._getTimeCacheData(src=src)
applicationMap = self._prepareApplicationMap(miscParams=miscParams, src=src, tgt=tgt)
tgtResists = tgt.getResists()
# Custom iteration for time graph to show all data points
currentDmg = None
currentTime = None
for currentTime in sorted(timeCache):
prevDmg = currentDmg
currentDmgData = timeCache[currentTime]
currentDmg = applyDamage(dmgMap=currentDmgData, applicationMap=applicationMap, tgtResists=tgtResists).total
if currentTime < minTime:
continue
# First set of data points
if not xs:
# Start at exactly requested time, at last known value
initialDmg = prevDmg or 0
xs.append(minTime)
ys.append(initialDmg)
# If current time is bigger then starting, extend plot to that time with old value
if currentTime > minTime:
xs.append(currentTime)
ys.append(initialDmg)
# If new value is different, extend it with new point to the new value
if currentDmg != prevDmg:
xs.append(currentTime)
ys.append(currentDmg)
continue
# Last data point
if currentTime >= maxTime:
xs.append(maxTime)
ys.append(prevDmg)
break
# Anything in-between
if currentDmg != prevDmg:
if prevDmg is not None:
xs.append(currentTime)
ys.append(prevDmg)
xs.append(currentTime)
ys.append(currentDmg)
# Special case - there are no damage dealers
if currentDmg is None and currentTime is None:
xs.append(minTime)
ys.append(0)
# Make sure that last data point is always at max time
if maxTime > (currentTime or 0):
xs.append(maxTime)
ys.append(currentDmg or 0)
return xs, ys
def getPoint(self, x, miscParams, src, tgt):
time = x
# Prepare time cache and various data
self._prepareTimeCache(src=src, maxTime=time)
dmgData = self._getTimeCacheDataPoint(src=src, time=time)
applicationMap = self._prepareApplicationMap(miscParams=miscParams, src=src, tgt=tgt)
y = applyDamage(dmgMap=dmgData, applicationMap=applicationMap, tgtResists=tgt.getResists()).total
return y
class XTgtSpeedMixin(SmoothPointGetter):
_baseResolution = 50
_extraDepth = 2
def _getCommonData(self, miscParams, src, tgt):
# Prepare time cache here because we need to do it only once,
# and this function is called once per point info fetch
self._prepareTimeCache(src=src, maxTime=miscParams['time'])
return {
'applyProjected': GraphSettings.getInstance().get('applyProjected'),
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
'tgtResists': tgt.getResists()}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
tgtSpeed = x
tgtSigRadius = tgt.getSigRadius()
if commonData['applyProjected']:
srcScramRange = getScramRange(src=src)
tgtScrammables = getScrammables(tgt=tgt)
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getTackledSpeed(
src=src,
tgt=tgt,
currentUntackledSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=miscParams['distance'])
tgtSigRadius = tgtSigRadius * getSigRadiusMult(
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,
distance=miscParams['distance'])
applicationMap = getApplicationPerKey(
src=src,
tgt=tgt,
atkSpeed=miscParams['atkSpeed'],
atkAngle=miscParams['atkAngle'],
distance=miscParams['distance'],
tgtSpeed=tgtSpeed,
tgtAngle=miscParams['tgtAngle'],
tgtSigRadius=tgtSigRadius)
y = applyDamage(
dmgMap=commonData['dmgMap'],
applicationMap=applicationMap,
tgtResists=commonData['tgtResists']).total
return y
class XTgtSigRadiusMixin(SmoothPointGetter):
_baseResolution = 50
_extraDepth = 2
def _getCommonData(self, miscParams, src, tgt):
tgtSpeed = miscParams['tgtSpeed']
tgtSigMult = 1
if GraphSettings.getInstance().get('applyProjected'):
srcScramRange = getScramRange(src=src)
tgtScrammables = getScrammables(tgt=tgt)
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getTackledSpeed(
src=src,
tgt=tgt,
currentUntackledSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=miscParams['distance'])
tgtSigMult = getSigRadiusMult(
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,
distance=miscParams['distance'])
# Prepare time cache here because we need to do it only once,
# and this function is called once per point info fetch
self._prepareTimeCache(src=src, maxTime=miscParams['time'])
return {
'tgtSpeed': tgtSpeed,
'tgtSigMult': tgtSigMult,
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
'tgtResists': tgt.getResists()}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
tgtSigRadius = x
applicationMap = getApplicationPerKey(
src=src,
tgt=tgt,
atkSpeed=miscParams['atkSpeed'],
atkAngle=miscParams['atkAngle'],
distance=miscParams['distance'],
tgtSpeed=commonData['tgtSpeed'],
tgtAngle=miscParams['tgtAngle'],
tgtSigRadius=tgtSigRadius * commonData['tgtSigMult'])
y = applyDamage(
dmgMap=commonData['dmgMap'],
applicationMap=applicationMap,
tgtResists=commonData['tgtResists']).total
return y
# Final getters
class Distance2DpsGetter(XDistanceMixin, YDpsMixin):
pass
class Distance2VolleyGetter(XDistanceMixin, YVolleyMixin):
pass
class Distance2InflictedDamageGetter(XDistanceMixin, YInflictedDamageMixin):
pass
class Time2DpsGetter(XTimeMixin, YDpsMixin):
pass
class Time2VolleyGetter(XTimeMixin, YVolleyMixin):
pass
class Time2InflictedDamageGetter(XTimeMixin, YInflictedDamageMixin):
pass
class TgtSpeed2DpsGetter(XTgtSpeedMixin, YDpsMixin):
pass
class TgtSpeed2VolleyGetter(XTgtSpeedMixin, YVolleyMixin):
pass
class TgtSpeed2InflictedDamageGetter(XTgtSpeedMixin, YInflictedDamageMixin):
pass
class TgtSigRadius2DpsGetter(XTgtSigRadiusMixin, YDpsMixin):
pass
class TgtSigRadius2VolleyGetter(XTgtSigRadiusMixin, YVolleyMixin):
pass
class TgtSigRadius2InflictedDamageGetter(XTgtSigRadiusMixin, YInflictedDamageMixin):
pass

View File

@@ -0,0 +1,113 @@
# =============================================================================
# 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 graphs.data.base import FitGraph, XDef, YDef, Input, VectorDef
from service.const import GraphCacheCleanupReason
from service.settings import GraphSettings
from .cache import ProjectedDataCache, TimeCache
from .getter import (
Distance2DpsGetter, Distance2VolleyGetter, Distance2InflictedDamageGetter,
Time2DpsGetter, Time2VolleyGetter, Time2InflictedDamageGetter,
TgtSpeed2DpsGetter, TgtSpeed2VolleyGetter, TgtSpeed2InflictedDamageGetter,
TgtSigRadius2DpsGetter, TgtSigRadius2VolleyGetter, TgtSigRadius2InflictedDamageGetter)
class FitDamageStatsGraph(FitGraph):
def __init__(self):
super().__init__()
self._timeCache = TimeCache()
self._projectedCache = ProjectedDataCache()
def _clearInternalCache(self, reason, extraData):
# Here, we care only about fit changes and graph changes.
# - Input changes are irrelevant as time cache cares only about
# time input, and it regenerates once time goes beyond cached value
# - Option changes are irrelevant as cache contains "raw" damage
# values which do not rely on any graph options
if reason in (GraphCacheCleanupReason.fitChanged, GraphCacheCleanupReason.fitRemoved):
self._timeCache.clearForFit(extraData)
self._projectedCache.clearForFit(extraData)
elif reason == GraphCacheCleanupReason.graphSwitched:
self._timeCache.clearAll()
self._projectedCache.clearAll()
# UI stuff
internalName = 'dmgStatsGraph'
name = 'Damage Stats'
xDefs = [
XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km')),
XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')),
XDef(handle='tgtSpeed', unit='m/s', label='Target speed', mainInput=('tgtSpeed', '%')),
XDef(handle='tgtSpeed', unit='%', label='Target speed', mainInput=('tgtSpeed', '%')),
XDef(handle='tgtSigRad', unit='m', label='Target signature radius', mainInput=('tgtSigRad', '%')),
XDef(handle='tgtSigRad', unit='%', label='Target signature radius', mainInput=('tgtSigRad', '%'))]
inputs = [
Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=None, defaultRange=(0, 100), mainTooltip='Distance between the attacker and the target, as seen in overview (surface-to-surface)', secondaryTooltip='Distance between the attacker and the target, as seen in overview (surface-to-surface)\nWhen set, places the target that far away from the attacker\nWhen not set, attacker\'s weapons always hit the target'),
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), secondaryTooltip='When set, uses attacker\'s exact damage stats at a given time\nWhen not set, uses attacker\'s damage stats as shown in stats panel of main window'),
Input(handle='tgtSpeed', unit='%', label='Target speed', iconID=1389, defaultValue=100, defaultRange=(0, 100)),
Input(handle='tgtSigRad', unit='%', label='Target signature', iconID=1390, defaultValue=100, defaultRange=(100, 200), conditions=[
(('tgtSigRad', 'm'), None),
(('tgtSigRad', '%'), None)])]
srcVectorDef = VectorDef(lengthHandle='atkSpeed', lengthUnit='%', angleHandle='atkAngle', angleUnit='degrees', label='Attacker')
tgtVectorDef = VectorDef(lengthHandle='tgtSpeed', lengthUnit='%', angleHandle='tgtAngle', angleUnit='degrees', label='Target')
hasTargets = True
srcExtraCols = ('Dps', 'Volley', 'Speed', 'Radius')
@property
def yDefs(self):
ignoreResists = GraphSettings.getInstance().get('ignoreResists')
return [
YDef(handle='dps', unit=None, label='DPS' if ignoreResists else 'Effective DPS'),
YDef(handle='volley', unit=None, label='Volley' if ignoreResists else 'Effective volley'),
YDef(handle='damage', unit=None, label='Damage inflicted' if ignoreResists else 'Effective damage inflicted')]
@property
def tgtExtraCols(self):
cols = []
if not GraphSettings.getInstance().get('ignoreResists'):
cols.append('Target Resists')
cols.extend(('Speed', 'SigRadius', 'Radius'))
return cols
# Calculation stuff
_normalizers = {
('distance', 'km'): lambda v, src, tgt: None if v is None else v * 1000,
('atkSpeed', '%'): lambda v, src, tgt: v / 100 * src.getMaxVelocity(),
('tgtSpeed', '%'): lambda v, src, tgt: v / 100 * tgt.getMaxVelocity(),
('tgtSigRad', '%'): lambda v, src, tgt: v / 100 * tgt.getSigRadius()}
_limiters = {'time': lambda src, tgt: (0, 2500)}
_getters = {
('distance', 'dps'): Distance2DpsGetter,
('distance', 'volley'): Distance2VolleyGetter,
('distance', 'damage'): Distance2InflictedDamageGetter,
('time', 'dps'): Time2DpsGetter,
('time', 'volley'): Time2VolleyGetter,
('time', 'damage'): Time2InflictedDamageGetter,
('tgtSpeed', 'dps'): TgtSpeed2DpsGetter,
('tgtSpeed', 'volley'): TgtSpeed2VolleyGetter,
('tgtSpeed', 'damage'): TgtSpeed2InflictedDamageGetter,
('tgtSigRad', 'dps'): TgtSigRadius2DpsGetter,
('tgtSigRad', 'volley'): TgtSigRadius2VolleyGetter,
('tgtSigRad', 'damage'): TgtSigRadius2InflictedDamageGetter}
_denormalizers = {
('distance', 'km'): lambda v, src, tgt: None if v is None else v / 1000,
('tgtSpeed', '%'): lambda v, src, tgt: v * 100 / tgt.getMaxVelocity(),
('tgtSigRad', '%'): lambda v, src, tgt: v * 100 / tgt.getSigRadius()}

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 FitEwarStatsGraph
FitEwarStatsGraph.register()

View File

@@ -0,0 +1,305 @@
# =============================================================================
# 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 math
from graphs.calc import calculateMultiplier, calculateRangeFactor
from graphs.data.base import SmoothPointGetter
class Distance2NeutingStrGetter(SmoothPointGetter):
_baseResolution = 50
_extraDepth = 2
def _getCommonData(self, miscParams, src, tgt):
resonance = 1 - (miscParams['resist'] or 0)
neuts = []
for mod in src.item.activeModulesIter():
for effectName in ('energyNeutralizerFalloff', 'structureEnergyNeutralizerFalloff'):
if effectName in mod.item.effects:
neuts.append((
mod.getModifiedItemAttr('energyNeutralizerAmount') / self.__getDuration(mod) * resonance,
mod.maxRange or 0, mod.falloff or 0))
if 'energyNosferatuFalloff' in mod.item.effects and mod.getModifiedItemAttr('nosOverride'):
neuts.append((
mod.getModifiedItemAttr('powerTransferAmount') / self.__getDuration(mod) * resonance,
mod.maxRange or 0, mod.falloff or 0))
if 'doomsdayAOENeut' in mod.item.effects:
neuts.append((
mod.getModifiedItemAttr('energyNeutralizerAmount') / self.__getDuration(mod) * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0))
for drone in src.item.activeDronesIter():
if 'entityEnergyNeutralizerFalloff' in drone.item.effects:
neuts.extend(drone.amountActive * ((
drone.getModifiedItemAttr('energyNeutralizerAmount') / (drone.getModifiedItemAttr('energyNeutralizerDuration') / 1000) * resonance,
src.item.extraAttributes['droneControlRange'], 0),))
for fighter, ability in src.item.activeFighterAbilityIter():
if ability.effect.name == 'fighterAbilityEnergyNeutralizer':
nps = fighter.getModifiedItemAttr('fighterAbilityEnergyNeutralizerAmount') / (ability.cycleTime / 1000)
neuts.append((
nps * fighter.amount * resonance,
math.inf, 0))
return {'neuts': neuts}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
combinedStr = 0
for strength, optimal, falloff in commonData['neuts']:
combinedStr += strength * calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
return combinedStr
def __getDuration(self, mod):
return getattr(mod.getCycleParameters(), 'averageTime', math.inf) / 1000
class Distance2WebbingStrGetter(SmoothPointGetter):
_baseResolution = 50
_extraDepth = 2
def _getCommonData(self, miscParams, src, tgt):
resonance = 1 - (miscParams['resist'] or 0)
webs = []
for mod in src.item.activeModulesIter():
for effectName in ('remoteWebifierFalloff', 'structureModuleEffectStasisWebifier'):
if effectName in mod.item.effects:
webs.append((
mod.getModifiedItemAttr('speedFactor') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
if 'doomsdayAOEWeb' in mod.item.effects:
webs.append((
mod.getModifiedItemAttr('speedFactor') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
for drone in src.item.activeDronesIter():
if 'remoteWebifierEntity' in drone.item.effects:
webs.extend(drone.amountActive * ((
drone.getModifiedItemAttr('speedFactor') * resonance,
src.item.extraAttributes['droneControlRange'], 0, 'default'),))
for fighter, ability in src.item.activeFighterAbilityIter():
if ability.effect.name == 'fighterAbilityStasisWebifier':
webs.append((
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierSpeedPenalty') * fighter.amount * resonance,
math.inf, 0, 'default'))
return {'webs': webs}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
strMults = {}
for strength, optimal, falloff, stackingGroup in commonData['webs']:
strength *= calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
strMults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
strMult = calculateMultiplier(strMults)
strength = (1 - strMult) * 100
return strength
class Distance2EcmStrMaxGetter(SmoothPointGetter):
_baseResolution = 50
_extraDepth = 2
ECM_ATTRS_GENERAL = ('scanGravimetricStrengthBonus', 'scanLadarStrengthBonus', 'scanMagnetometricStrengthBonus', 'scanRadarStrengthBonus')
ECM_ATTRS_FIGHTERS = ('fighterAbilityECMStrengthGravimetric', 'fighterAbilityECMStrengthLadar', 'fighterAbilityECMStrengthMagnetometric', 'fighterAbilityECMStrengthRadar')
def _getCommonData(self, miscParams, src, tgt):
resonance = 1 - (miscParams['resist'] or 0)
ecms = []
for mod in src.item.activeModulesIter():
for effectName in ('remoteECMFalloff', 'structureModuleEffectECM'):
if effectName in mod.item.effects:
ecms.append((
max(mod.getModifiedItemAttr(a) for a in self.ECM_ATTRS_GENERAL) * resonance,
mod.maxRange or 0, mod.falloff or 0))
if 'doomsdayAOEECM' in mod.item.effects:
ecms.append((
max(mod.getModifiedItemAttr(a) for a in self.ECM_ATTRS_GENERAL) * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0))
for drone in src.item.activeDronesIter():
if 'entityECMFalloff' in drone.item.effects:
ecms.extend(drone.amountActive * ((
max(drone.getModifiedItemAttr(a) for a in self.ECM_ATTRS_GENERAL) * resonance,
src.item.extraAttributes['droneControlRange'], 0),))
for fighter, ability in src.item.activeFighterAbilityIter():
if ability.effect.name == 'fighterAbilityECM':
ecms.append((
max(fighter.getModifiedItemAttr(a) for a in self.ECM_ATTRS_FIGHTERS) * fighter.amount * resonance,
math.inf, 0))
return {'ecms': ecms}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
combinedStr = 0
for strength, optimal, falloff in commonData['ecms']:
combinedStr += strength * calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
return combinedStr
class Distance2DampStrLockRangeGetter(SmoothPointGetter):
_baseResolution = 50
_extraDepth = 2
def _getCommonData(self, miscParams, src, tgt):
resonance = 1 - (miscParams['resist'] or 0)
damps = []
for mod in src.item.activeModulesIter():
for effectName in ('remoteSensorDampFalloff', 'structureModuleEffectRemoteSensorDampener'):
if effectName in mod.item.effects:
damps.append((
mod.getModifiedItemAttr('maxTargetRangeBonus') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
if 'doomsdayAOEDamp' in mod.item.effects:
damps.append((
mod.getModifiedItemAttr('maxTargetRangeBonus') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
for drone in src.item.activeDronesIter():
if 'remoteSensorDampEntity' in drone.item.effects:
damps.extend(drone.amountActive * ((
drone.getModifiedItemAttr('maxTargetRangeBonus') * resonance,
src.item.extraAttributes['droneControlRange'], 0, 'default'),))
return {'damps': damps}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
strMults = {}
for strength, optimal, falloff, stackingGroup in commonData['damps']:
strength *= calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
strMults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
strMult = calculateMultiplier(strMults)
strength = (1 - strMult) * 100
return strength
class Distance2TdStrOptimalGetter(SmoothPointGetter):
_baseResolution = 50
_extraDepth = 2
def _getCommonData(self, miscParams, src, tgt):
resonance = 1 - (miscParams['resist'] or 0)
tds = []
for mod in src.item.activeModulesIter():
for effectName in ('shipModuleTrackingDisruptor', 'structureModuleEffectWeaponDisruption'):
if effectName in mod.item.effects:
tds.append((
mod.getModifiedItemAttr('maxRangeBonus') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
if 'doomsdayAOETrack' in mod.item.effects:
tds.append((
mod.getModifiedItemAttr('maxRangeBonus') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
for drone in src.item.activeDronesIter():
if 'npcEntityWeaponDisruptor' in drone.item.effects:
tds.extend(drone.amountActive * ((
drone.getModifiedItemAttr('maxRangeBonus') * resonance,
src.item.extraAttributes['droneControlRange'], 0, 'default'),))
return {'tds': tds}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
strMults = {}
for strength, optimal, falloff, stackingGroup in commonData['tds']:
strength *= calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
strMults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
strMult = calculateMultiplier(strMults)
strength = (1 - strMult) * 100
return strength
class Distance2GdStrRangeGetter(SmoothPointGetter):
_baseResolution = 50
_extraDepth = 2
def _getCommonData(self, miscParams, src, tgt):
resonance = 1 - (miscParams['resist'] or 0)
gds = []
for mod in src.item.activeModulesIter():
for effectName in ('shipModuleGuidanceDisruptor', 'structureModuleEffectWeaponDisruption'):
if effectName in mod.item.effects:
gds.append((
mod.getModifiedItemAttr('missileVelocityBonus') * resonance,
mod.getModifiedItemAttr('explosionDelayBonus') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
if 'doomsdayAOETrack' in mod.item.effects:
gds.append((
mod.getModifiedItemAttr('missileVelocityBonus') * resonance,
mod.getModifiedItemAttr('explosionDelayBonus') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
return {'gds': gds}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
velocityStrMults = {}
timeStrMults = {}
for velocityStr, timeStr, optimal, falloff, stackingGroup in commonData['gds']:
rangeFactor = calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
velocityStr *= rangeFactor
timeStr *= rangeFactor
velocityStrMults.setdefault(stackingGroup, []).append((1 + velocityStr / 100, None))
timeStrMults.setdefault(stackingGroup, []).append((1 + timeStr / 100, None))
velocityStrMult = calculateMultiplier(velocityStrMults)
timeStrMult = calculateMultiplier(timeStrMults)
strength = (1 - velocityStrMult * timeStrMult) * 100
return strength
class Distance2TpStrGetter(SmoothPointGetter):
_baseResolution = 50
_extraDepth = 2
def _getCommonData(self, miscParams, src, tgt):
resonance = 1 - (miscParams['resist'] or 0)
tps = []
for mod in src.item.activeModulesIter():
for effectName in ('remoteTargetPaintFalloff', 'structureModuleEffectTargetPainter'):
if effectName in mod.item.effects:
tps.append((
mod.getModifiedItemAttr('signatureRadiusBonus') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
if 'doomsdayAOEPaint' in mod.item.effects:
tps.append((
mod.getModifiedItemAttr('signatureRadiusBonus') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
for drone in src.item.activeDronesIter():
if 'remoteTargetPaintEntity' in drone.item.effects:
tps.extend(drone.amountActive * ((
drone.getModifiedItemAttr('signatureRadiusBonus') * resonance,
src.item.extraAttributes['droneControlRange'], 0, 'default'),))
return {'tps': tps}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
strMults = {}
for strength, optimal, falloff, stackingGroup in commonData['tps']:
strength *= calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
strMults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
strMult = calculateMultiplier(strMults)
strength = (strMult - 1) * 100
return strength

View File

@@ -0,0 +1,59 @@
# =============================================================================
# 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 graphs.data.base import FitGraph, Input, XDef, YDef
from .getter import (
Distance2NeutingStrGetter, Distance2WebbingStrGetter, Distance2EcmStrMaxGetter,
Distance2DampStrLockRangeGetter, Distance2TdStrOptimalGetter, Distance2GdStrRangeGetter,
Distance2TpStrGetter)
class FitEwarStatsGraph(FitGraph):
# UI stuff
internalName = 'ewarStatsGraph'
name = 'Electronic Warfare Stats'
xDefs = [XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km'))]
yDefs = [
YDef(handle='neutStr', unit=None, label='Cap neutralized per second', selectorLabel='Neuts: cap per second'),
YDef(handle='webStr', unit='%', label='Speed reduction', selectorLabel='Webs: speed reduction'),
YDef(handle='ecmStrMax', unit=None, label='Combined ECM strength', selectorLabel='ECM: combined strength'),
YDef(handle='dampStrLockRange', unit='%', label='Lock range reduction', selectorLabel='Damps: lock range reduction'),
YDef(handle='tdStrOptimal', unit='%', label='Turret optimal range reduction', selectorLabel='TDs: turret optimal range reduction'),
YDef(handle='gdStrRange', unit='%', label='Missile flight range reduction', selectorLabel='GDs: missile flight range reduction'),
YDef(handle='tpStr', unit='%', label='Signature radius increase', selectorLabel='TPs: signature radius increase')]
inputs = [
Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=None, defaultRange=(0, 100)),
Input(handle='resist', unit='%', label='Target resistance', iconID=1393, defaultValue=0, defaultRange=(0, 100))]
# Calculation stuff
_normalizers = {
('distance', 'km'): lambda v, src, tgt: None if v is None else v * 1000,
('resist', '%'): lambda v, src, tgt: None if v is None else v / 100}
_limiters = {'resist': lambda src, tgt: (0, 1)}
_getters = {
('distance', 'neutStr'): Distance2NeutingStrGetter,
('distance', 'webStr'): Distance2WebbingStrGetter,
('distance', 'ecmStrMax'): Distance2EcmStrMaxGetter,
('distance', 'dampStrLockRange'): Distance2DampStrLockRangeGetter,
('distance', 'tdStrOptimal'): Distance2TdStrOptimalGetter,
('distance', 'gdStrRange'): Distance2GdStrRangeGetter,
('distance', 'tpStr'): Distance2TpStrGetter}
_denormalizers = {('distance', 'km'): lambda v, src, tgt: None if v is None else v / 1000}

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 FitLockTimeGraph
FitLockTimeGraph.register()

View File

@@ -0,0 +1,29 @@
# =============================================================================
# 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 graphs.data.base import SmoothPointGetter
class TgtSigRadius2LockTimeGetter(SmoothPointGetter):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
tgtSigRadius = x
time = src.item.calculateLockTime(radius=tgtSigRadius)
return time

View File

@@ -0,0 +1,39 @@
# =============================================================================
# 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 math
from graphs.data.base import FitGraph, Input, XDef, YDef
from .getter import TgtSigRadius2LockTimeGetter
class FitLockTimeGraph(FitGraph):
# UI stuff
internalName = 'lockTimeGraph'
name = 'Lock Time'
xDefs = [XDef(handle='tgtSigRad', unit='m', label='Target signature radius', mainInput=('tgtSigRad', 'm'))]
yDefs = [YDef(handle='time', unit='s', label='Lock time')]
inputs = [Input(handle='tgtSigRad', unit='m', label='Target signature', iconID=1390, defaultValue=None, defaultRange=(25, 500))]
srcExtraCols = ('ScanResolution',)
# Calculation stuff
_limiters = {'tgtSigRad': lambda src, tgt: (1, math.inf)}
_getters = {('tgtSigRad', 'time'): TgtSigRadius2LockTimeGetter}

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 FitMobilityGraph
FitMobilityGraph.register()

View File

@@ -0,0 +1,62 @@
# =============================================================================
# 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 math
from graphs.data.base import SmoothPointGetter
class Time2SpeedGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxSpeed': src.getMaxVelocity(),
'mass': src.item.ship.getModifiedItemAttr('mass'),
'agility': src.item.ship.getModifiedItemAttr('agility')}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
maxSpeed = commonData['maxSpeed']
mass = commonData['mass']
agility = commonData['agility']
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
speed = maxSpeed * (1 - math.exp((-time * 1000000) / (agility * mass)))
return speed
class Time2DistanceGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxSpeed': src.getMaxVelocity(),
'mass': src.item.ship.getModifiedItemAttr('mass'),
'agility': src.item.ship.getModifiedItemAttr('agility')}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
maxSpeed = commonData['maxSpeed']
mass = commonData['mass']
agility = commonData['agility']
# Definite integral of:
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
distance_t = maxSpeed * time + (maxSpeed * agility * mass * math.exp((-time * 1000000) / (agility * mass)) / 1000000)
distance_0 = maxSpeed * 0 + (maxSpeed * agility * mass * math.exp((-0 * 1000000) / (agility * mass)) / 1000000)
distance = distance_t - distance_0
return distance

View File

@@ -0,0 +1,41 @@
# =============================================================================
# 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 graphs.data.base import FitGraph, XDef, YDef, Input
from .getter import Time2SpeedGetter, Time2DistanceGetter
class FitMobilityGraph(FitGraph):
# UI stuff
internalName = 'mobilityGraph'
name = 'Mobility'
xDefs = [XDef(handle='time', unit='s', label='Time', mainInput=('time', 's'))]
yDefs = [
YDef(handle='speed', unit='m/s', label='Speed'),
YDef(handle='distance', unit='km', label='Distance')]
inputs = [Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=10, defaultRange=(0, 30))]
srcExtraCols = ('Speed', 'Agility')
# Calculation stuff
_getters = {
('time', 'speed'): Time2SpeedGetter,
('time', 'distance'): Time2DistanceGetter}
_denormalizers = {('distance', 'km'): lambda v, src, tgt: v / 1000}

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 FitRemoteRepsGraph
FitRemoteRepsGraph.register()

View File

@@ -0,0 +1,218 @@
# =============================================================================
# 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 copy import copy
from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import SpoolOptions, SpoolType
from eos.utils.stats import RRTypes
from graphs.data.base import FitDataCache
class TimeCache(FitDataCache):
# Whole data getters
def getRpsData(self, src, ancReload):
"""Return RPS data in {time: {key: rps}} format."""
return self._data[src.item.ID][ancReload]['finalRps']
def getRepAmountData(self, src, ancReload):
"""Return rep amount data in {time: {key: amount}} format."""
return self._data[src.item.ID][ancReload]['finalRepAmount']
# Specific data point getters
def getRpsDataPoint(self, src, ancReload, time):
"""Get RPS data by specified time in {key: rps} format."""
return self._getDataPoint(src=src, ancReload=ancReload, time=time, dataFunc=self.getRpsData)
def getRepAmountDataPoint(self, src, ancReload, time):
"""Get rep amount data by specified time in {key: amount} format."""
return self._getDataPoint(src=src, ancReload=ancReload, time=time, dataFunc=self.getRepAmountData)
# Preparation functions
def prepareRpsData(self, src, ancReload, maxTime):
# Time is none means that time parameter has to be ignored,
# we do not need cache for that
if maxTime is None:
return True
self._generateInternalForm(src=src, ancReload=ancReload, maxTime=maxTime)
fitCache = self._data[src.item.ID][ancReload]
# Final cache has been generated already, don't do anything
if 'finalRps' in fitCache:
return
# Convert cache from segments with assigned values into points
# which are located at times when rps value changes
pointCache = {}
for key, rpsList in fitCache['internalRps'].items():
pointData = pointCache[key] = {}
prevRps = None
prevTimeEnd = None
for timeStart, timeEnd, rps in rpsList:
# First item
if not pointData:
pointData[timeStart] = rps
# Gap between items
elif floatUnerr(prevTimeEnd) < floatUnerr(timeStart):
pointData[prevTimeEnd] = RRTypes(0, 0, 0, 0)
pointData[timeStart] = rps
# Changed value
elif rps != prevRps:
pointData[timeStart] = rps
prevRps = rps
prevTimeEnd = timeEnd
# We have data in another form, do not need old one any longer
del fitCache['internalRps']
changesByTime = {}
for key, rpsMap in pointCache.items():
for time in rpsMap:
changesByTime.setdefault(time, []).append(key)
# Here we convert cache to following format:
# {time: {key: rps}
finalRpsCache = fitCache['finalRps'] = {}
timeRpsData = {}
for time in sorted(changesByTime):
timeRpsData = copy(timeRpsData)
for key in changesByTime[time]:
timeRpsData[key] = pointCache[key][time]
finalRpsCache[time] = timeRpsData
def prepareRepAmountData(self, src, ancReload, maxTime):
# Time is none means that time parameter has to be ignored,
# we do not need cache for that
if maxTime is None:
return
self._generateInternalForm(src=src, ancReload=ancReload, maxTime=maxTime)
fitCache = self._data[src.item.ID][ancReload]
# Final cache has been generated already, don't do anything
if 'finalRepAmount' in fitCache:
return
intCache = fitCache['internalRepAmount']
changesByTime = {}
for key, remAmountMap in intCache.items():
for time in remAmountMap:
changesByTime.setdefault(time, []).append(key)
# Here we convert cache to following format:
# {time: {key: hp repaired by key at this time}}
finalCache = fitCache['finalRepAmount'] = {}
timeRepAmountData = {}
for time in sorted(changesByTime):
timeRepAmountData = copy(timeRepAmountData)
for key in changesByTime[time]:
keyRepAmount = intCache[key][time]
if key in timeRepAmountData:
timeRepAmountData[key] = timeRepAmountData[key] + keyRepAmount
else:
timeRepAmountData[key] = keyRepAmount
finalCache[time] = timeRepAmountData
# We do not need internal cache once we have final
del fitCache['internalRepAmount']
# Private stuff
def _generateInternalForm(self, src, ancReload, maxTime):
if self._isTimeCacheValid(src=src, ancReload=ancReload, maxTime=maxTime):
return
fitCache = self._data.setdefault(src.item.ID, {})[ancReload] = {'maxTime': maxTime}
intCacheRps = fitCache['internalRps'] = {}
intCacheRepAmount = fitCache['internalRepAmount'] = {}
def addRps(rrKey, addedTimeStart, addedTimeFinish, addedRepAmounts):
if not addedRepAmounts:
return
repAmountSum = sum(addedRepAmounts, RRTypes(0, 0, 0, 0))
if repAmountSum.shield > 0 or repAmountSum.armor > 0 or repAmountSum.hull > 0:
addedRps = repAmountSum / (addedTimeFinish - addedTimeStart)
rrCacheRps = intCacheRps.setdefault(rrKey, [])
rrCacheRps.append((addedTimeStart, addedTimeFinish, addedRps))
def addRepAmount(rrKey, addedTime, addedRepAmount):
if addedRepAmount.shield > 0 or addedRepAmount.armor > 0 or addedRepAmount.hull > 0:
intCacheRepAmount.setdefault(rrKey, {})[addedTime] = addedRepAmount
# Modules
for mod in src.item.activeModulesIter():
if not mod.isRemoteRepping():
continue
isAncShield = 'shipModuleAncillaryRemoteShieldBooster' in mod.item.effects
isAncArmor = 'shipModuleAncillaryRemoteArmorRepairer' in mod.item.effects
if isAncShield or isAncArmor:
cycleParams = mod.getCycleParameters(reloadOverride=ancReload)
else:
cycleParams = mod.getCycleParameters(reloadOverride=True)
if cycleParams is None:
continue
currentTime = 0
nonstopCycles = 0
cyclesWithoutReload = 0
cyclesUntilReload = mod.numShots
for cycleTimeMs, inactiveTimeMs, isInactivityReload in cycleParams.iterCycles():
cyclesWithoutReload += 1
cycleRepAmounts = []
repAmountParams = mod.getRepAmountParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
for repTimeMs, repAmount in repAmountParams.items():
# Loaded ancillary armor rep can keep running at less efficiency if we decide to not reload
if isAncArmor and mod.charge and not ancReload and cyclesWithoutReload > cyclesUntilReload:
repAmount = repAmount / mod.getModifiedItemAttr('chargedArmorDamageMultiplier', 1)
cycleRepAmounts.append(repAmount)
addRepAmount(mod, currentTime + repTimeMs / 1000, repAmount)
addRps(mod, currentTime, currentTime + cycleTimeMs / 1000, cycleRepAmounts)
if inactiveTimeMs > 0:
nonstopCycles = 0
else:
nonstopCycles += 1
if isInactivityReload:
cyclesWithoutReload = 0
if currentTime > maxTime:
break
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
# Drones
for drone in src.item.activeDronesIter():
if not drone.isRemoteRepping():
continue
cycleParams = drone.getCycleParameters(reloadOverride=True)
if cycleParams is None:
continue
currentTime = 0
repAmountParams = drone.getRepAmountParameters()
for cycleTimeMs, inactiveTimeMs, isInactivityReload in cycleParams.iterCycles():
cycleRepAmounts = []
for repTimeMs, repAmount in repAmountParams.items():
cycleRepAmounts.append(repAmount)
addRepAmount(drone, currentTime + repTimeMs / 1000, repAmount)
addRps(drone, currentTime, currentTime + cycleTimeMs / 1000, cycleRepAmounts)
if currentTime > maxTime:
break
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
def _isTimeCacheValid(self, src, ancReload, maxTime):
try:
cacheMaxTime = self._data[src.item.ID][ancReload]['maxTime']
except KeyError:
return False
return maxTime <= cacheMaxTime
def _getDataPoint(self, src, ancReload, time, dataFunc):
data = dataFunc(src=src, ancReload=ancReload)
timesBefore = [t for t in data if floatUnerr(t) <= floatUnerr(time)]
try:
time = max(timesBefore)
except ValueError:
return {}
else:
return data[time]

View File

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

View File

@@ -0,0 +1,190 @@
# =============================================================================
# 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.config
from eos.utils.spoolSupport import SpoolOptions, SpoolType
from eos.utils.stats import RRTypes
from graphs.data.base import PointGetter, SmoothPointGetter
from .calc import getApplicationPerKey
def applyReps(rrMap, applicationMap):
totalAmount = RRTypes(shield=0, armor=0, hull=0, capacitor=0)
for key, repAmount in rrMap.items():
totalAmount += repAmount * applicationMap.get(key, 0)
# We do not want to include energy transfers into final value
totalReps = totalAmount.shield + totalAmount.armor + totalAmount.hull
return totalReps
# Y mixins
class YRpsMixin:
def _getRepsPerKey(self, src, ancReload, time):
# Use data from time cache if time was not specified
if time is not None:
return self._getTimeCacheDataPoint(src=src, ancReload=ancReload, time=time)
# Compose map ourselves using current fit settings if time is not specified
rpsMap = {}
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
for mod in src.item.activeModulesIter():
if not mod.isRemoteRepping():
continue
isAncShield = 'shipModuleAncillaryRemoteShieldBooster' in mod.item.effects
isAncArmor = 'shipModuleAncillaryRemoteArmorRepairer' in mod.item.effects
rpsMap[mod] = mod.getRemoteReps(
spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False),
reloadOverride=ancReload if (isAncShield or isAncArmor) else None)
for drone in src.item.activeDronesIter():
if not drone.isRemoteRepping():
continue
rpsMap[drone] = drone.getRemoteReps()
return rpsMap
def _prepareTimeCache(self, src, ancReload, maxTime):
self.graph._timeCache.prepareRpsData(src=src, ancReload=ancReload, maxTime=maxTime)
def _getTimeCacheData(self, src, ancReload):
return self.graph._timeCache.getRpsData(src=src, ancReload=ancReload)
def _getTimeCacheDataPoint(self, src, ancReload, time):
return self.graph._timeCache.getRpsDataPoint(src=src, ancReload=ancReload, time=time)
class YRepAmountMixin:
def _getRepsPerKey(self, src, ancReload, time):
# Total reps given makes no sense without time specified
if time is None:
raise ValueError
return self._getTimeCacheDataPoint(src=src, ancReload=ancReload, time=time)
def _prepareTimeCache(self, src, ancReload, maxTime):
self.graph._timeCache.prepareRepAmountData(src=src, ancReload=ancReload, maxTime=maxTime)
def _getTimeCacheData(self, src, ancReload):
return self.graph._timeCache.getRepAmountData(src=src, ancReload=ancReload)
def _getTimeCacheDataPoint(self, src, ancReload, time):
return self.graph._timeCache.getRepAmountDataPoint(src=src, ancReload=ancReload, time=time)
# X mixins
class XDistanceMixin(SmoothPointGetter):
_baseResolution = 50
_extraDepth = 2
def _getCommonData(self, miscParams, src, tgt):
# Prepare time cache here because we need to do it only once,
# and this function is called once per point info fetch
self._prepareTimeCache(src=src, ancReload=miscParams['ancReload'], maxTime=miscParams['time'])
return {'rrMap': self._getRepsPerKey(src=src, ancReload=miscParams['ancReload'], time=miscParams['time'])}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
applicationMap = getApplicationPerKey(src=src, distance=distance)
y = applyReps(
rrMap=commonData['rrMap'],
applicationMap=applicationMap)
return y
class XTimeMixin(PointGetter):
def getRange(self, xRange, miscParams, src, tgt):
xs = []
ys = []
minTime, maxTime = xRange
# Prepare time cache and various shared data
self._prepareTimeCache(src=src, ancReload=miscParams['ancReload'], maxTime=maxTime)
timeCache = self._getTimeCacheData(src=src, ancReload=miscParams['ancReload'])
applicationMap = getApplicationPerKey(src=src, distance=miscParams['distance'])
# Custom iteration for time graph to show all data points
currentRepAmount = None
currentTime = None
for currentTime in sorted(timeCache):
prevRepAmount = currentRepAmount
currentRepAmountData = timeCache[currentTime]
currentRepAmount = applyReps(rrMap=currentRepAmountData, applicationMap=applicationMap)
if currentTime < minTime:
continue
# First set of data points
if not xs:
# Start at exactly requested time, at last known value
initialRepAmount = prevRepAmount or 0
xs.append(minTime)
ys.append(initialRepAmount)
# If current time is bigger then starting, extend plot to that time with old value
if currentTime > minTime:
xs.append(currentTime)
ys.append(initialRepAmount)
# If new value is different, extend it with new point to the new value
if currentRepAmount != prevRepAmount:
xs.append(currentTime)
ys.append(currentRepAmount)
continue
# Last data point
if currentTime >= maxTime:
xs.append(maxTime)
ys.append(prevRepAmount)
break
# Anything in-between
if currentRepAmount != prevRepAmount:
if prevRepAmount is not None:
xs.append(currentTime)
ys.append(prevRepAmount)
xs.append(currentTime)
ys.append(currentRepAmount)
# Special case - there are no remote reppers
if currentRepAmount is None and currentTime is None:
xs.append(minTime)
ys.append(0)
# Make sure that last data point is always at max time
if maxTime > (currentTime or 0):
xs.append(maxTime)
ys.append(currentRepAmount or 0)
return xs, ys
def getPoint(self, x, miscParams, src, tgt):
time = x
# Prepare time cache and various data
self._prepareTimeCache(src=src, ancReload=miscParams['ancReload'], maxTime=time)
repAmountData = self._getTimeCacheDataPoint(src=src, ancReload=miscParams['ancReload'], time=time)
applicationMap = getApplicationPerKey(src=src, distance=miscParams['distance'])
y = applyReps(rrMap=repAmountData, applicationMap=applicationMap)
return y
# Final getters
class Distance2RpsGetter(XDistanceMixin, YRpsMixin):
pass
class Distance2RepAmountGetter(XDistanceMixin, YRepAmountMixin):
pass
class Time2RpsGetter(XTimeMixin, YRpsMixin):
pass
class Time2RepAmountGetter(XTimeMixin, YRepAmountMixin):
pass

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/>.
# =============================================================================
from graphs.data.base import FitGraph, XDef, YDef, Input, InputCheckbox
from service.const import GraphCacheCleanupReason
from .cache import TimeCache
from .getter import Distance2RpsGetter, Distance2RepAmountGetter, Time2RpsGetter, Time2RepAmountGetter
class FitRemoteRepsGraph(FitGraph):
def __init__(self):
super().__init__()
self._timeCache = TimeCache()
def _clearInternalCache(self, reason, extraData):
# Here, we care only about fit changes, graph changes and option switches
# - Input changes are irrelevant as time cache cares only about
# time input, and it regenerates once time goes beyond cached value
if reason in (GraphCacheCleanupReason.fitChanged, GraphCacheCleanupReason.fitRemoved):
self._timeCache.clearForFit(extraData)
elif reason == GraphCacheCleanupReason.graphSwitched:
self._timeCache.clearAll()
# UI stuff
internalName = 'remoteRepsGraph'
name = 'Remote Repairs'
xDefs = [
XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km')),
XDef(handle='time', unit='s', label='Time', mainInput=('time', 's'))]
yDefs = [
YDef(handle='rps', unit='HP/s', label='Repair speed'),
YDef(handle='total', unit='HP', label='Total repaired')]
inputs = [
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), secondaryTooltip='When set, uses repairing ship\'s exact RR stats at a given time\nWhen not set, uses repairing ship\'s RR stats as shown in stats panel of main window'),
Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=None, defaultRange=(0, 100), mainTooltip='Distance between the repairing ship and the target, as seen in overview (surface-to-surface)', secondaryTooltip='Distance between the repairing ship and the target, as seen in overview (surface-to-surface)')]
srcExtraCols = ('ShieldRR', 'ArmorRR', 'HullRR')
checkboxes = [InputCheckbox(handle='ancReload', label='Reload ancillary RRs', defaultValue=True)]
# Calculation stuff
_normalizers = {('distance', 'km'): lambda v, src, tgt: None if v is None else v * 1000}
_limiters = {'time': lambda src, tgt: (0, 2500)}
_getters = {
('distance', 'rps'): Distance2RpsGetter,
('distance', 'total'): Distance2RepAmountGetter,
('time', 'rps'): Time2RpsGetter,
('time', 'total'): Time2RepAmountGetter}
_denormalizers = {('distance', 'km'): lambda v, src, tgt: None if v is None else v / 1000}

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 FitShieldRegenGraph
FitShieldRegenGraph.register()

View File

@@ -0,0 +1,97 @@
# =============================================================================
# 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 math
from graphs.data.base import SmoothPointGetter
class Time2ShieldAmountGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxShieldAmount': src.item.ship.getModifiedItemAttr('shieldCapacity'),
'shieldRegenTime': src.item.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
shieldAmount = calculateShieldAmount(
maxShieldAmount=commonData['maxShieldAmount'],
shieldRegenTime=commonData['shieldRegenTime'],
shieldAmountT0=miscParams['shieldAmountT0'] or 0,
time=time)
return shieldAmount
class Time2ShieldRegenGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxShieldAmount': src.item.ship.getModifiedItemAttr('shieldCapacity'),
'shieldRegenTime': src.item.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
shieldAmount = calculateShieldAmount(
maxShieldAmount=commonData['maxShieldAmount'],
shieldRegenTime=commonData['shieldRegenTime'],
shieldAmountT0=miscParams['shieldAmountT0'] or 0,
time=time)
shieldRegen = calculateShieldRegen(
maxShieldAmount=commonData['maxShieldAmount'],
shieldRegenTime=commonData['shieldRegenTime'],
currentShieldAmount=shieldAmount)
return shieldRegen
# Useless, but valid combination of x and y
class ShieldAmount2ShieldAmountGetter(SmoothPointGetter):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
shieldAmount = x
return shieldAmount
class ShieldAmount2ShieldRegenGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxShieldAmount': src.item.ship.getModifiedItemAttr('shieldCapacity'),
'shieldRegenTime': src.item.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
shieldAmount = x
shieldRegen = calculateShieldRegen(
maxShieldAmount=commonData['maxShieldAmount'],
shieldRegenTime=commonData['shieldRegenTime'],
currentShieldAmount=shieldAmount)
return shieldRegen
def calculateShieldAmount(maxShieldAmount, shieldRegenTime, shieldAmountT0, time):
# The same formula as for cap
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
return maxShieldAmount * (1 + math.exp(5 * -time / shieldRegenTime) * (math.sqrt(shieldAmountT0 / maxShieldAmount) - 1)) ** 2
def calculateShieldRegen(maxShieldAmount, shieldRegenTime, currentShieldAmount):
# The same formula as for cap
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
return 10 * maxShieldAmount / shieldRegenTime * (math.sqrt(currentShieldAmount / maxShieldAmount) - currentShieldAmount / maxShieldAmount)

View File

@@ -0,0 +1,79 @@
# =============================================================================
# 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 gui.mainFrame
from graphs.data.base import FitGraph, XDef, YDef, Input
from .getter import (
Time2ShieldAmountGetter, Time2ShieldRegenGetter,
ShieldAmount2ShieldAmountGetter, ShieldAmount2ShieldRegenGetter)
class FitShieldRegenGraph(FitGraph):
# UI stuff
internalName = 'shieldRegenGraph'
name = 'Shield Regeneration'
inputs = [
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), conditions=[
(('time', 's'), None)]),
Input(handle='shieldAmount', unit='%', label='Shield amount', iconID=1384, defaultValue=25, defaultRange=(0, 100), conditions=[
(('shieldAmount', 'EHP'), None),
(('shieldAmount', 'HP'), None),
(('shieldAmount', '%'), None)]),
Input(handle='shieldAmountT0', unit='%', label='Starting shield amount', iconID=1384, defaultValue=0, defaultRange=(0, 100), conditions=[
(('time', 's'), None)])]
srcExtraCols = ('ShieldAmount', 'ShieldTime')
usesHpEffectivity = True
@property
def xDefs(self):
return [
XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')),
XDef(handle='shieldAmount', unit='EHP' if self.isEffective else 'HP', label='Shield amount', mainInput=('shieldAmount', '%')),
XDef(handle='shieldAmount', unit='%', label='Shield amount', mainInput=('shieldAmount', '%'))]
@property
def yDefs(self):
return [
YDef(handle='shieldAmount', unit='EHP' if self.isEffective else 'HP', label='Shield amount'),
YDef(handle='shieldRegen', unit='EHP/s' if self.isEffective else 'HP/s', label='Shield regen')]
# Calculation stuff
_normalizers = {
('shieldAmount', '%'): lambda v, src, tgt: v / 100 * src.item.ship.getModifiedItemAttr('shieldCapacity'),
('shieldAmountT0', '%'): lambda v, src, tgt: None if v is None else v / 100 * src.item.ship.getModifiedItemAttr('shieldCapacity'),
# Needed only for "x mark" support, to convert EHP x into normalized value
('shieldAmount', 'EHP'): lambda v, src, tgt: v / src.item.damagePattern.effectivify(src.item, 1, 'shield')}
_limiters = {
'shieldAmount': lambda src, tgt: (0, src.item.ship.getModifiedItemAttr('shieldCapacity')),
'shieldAmountT0': lambda src, tgt: (0, src.item.ship.getModifiedItemAttr('shieldCapacity'))}
_getters = {
('time', 'shieldAmount'): Time2ShieldAmountGetter,
('time', 'shieldRegen'): Time2ShieldRegenGetter,
('shieldAmount', 'shieldAmount'): ShieldAmount2ShieldAmountGetter,
('shieldAmount', 'shieldRegen'): ShieldAmount2ShieldRegenGetter}
_denormalizers = {
('shieldAmount', '%'): lambda v, src, tgt: v * 100 / src.item.ship.getModifiedItemAttr('shieldCapacity'),
('shieldAmount', 'EHP'): lambda v, src, tgt: src.item.damagePattern.effectivify(src.item, v, 'shield'),
('shieldRegen', 'EHP/s'): lambda v, src, tgt: src.item.damagePattern.effectivify(src.item, v, 'shield')}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.isEffective = gui.mainFrame.MainFrame.getInstance().statsPane.nameViewMap['resistancesViewFull'].showEffective

View File

@@ -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 FitWarpTimeGraph
FitWarpTimeGraph.register()

View File

@@ -0,0 +1,82 @@
# =============================================================================
# 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.const import FittingModuleState
from graphs.data.base import FitDataCache
class SubwarpSpeedCache(FitDataCache):
def getSubwarpSpeed(self, src):
try:
subwarpSpeed = self._data[src.item.ID]
except KeyError:
modStates = {}
disallowedGroups = (
# Active modules which affect ship speed and cannot be used in warp
'Propulsion Module',
'Mass Entanglers',
'Cloaking Device',
# Those reduce ship speed to 0
'Siege Module',
'Super Weapon',
'Cynosural Field Generator',
'Clone Vat Bay',
'Jump Portal Generator')
for mod in src.item.activeModulesIter():
if mod.item is not None and mod.item.group.name in disallowedGroups:
modStates[mod] = mod.state
mod.state = FittingModuleState.ONLINE
projFitStates = {}
for projFit in src.item.projectedFits:
projectionInfo = projFit.getProjectionInfo(src.item.ID)
if projectionInfo is not None and projectionInfo.active:
projFitStates[projectionInfo] = projectionInfo.active
projectionInfo.active = False
projModStates = {}
for mod in src.item.projectedModules:
if not mod.isExclusiveSystemEffect and mod.state >= FittingModuleState.ACTIVE:
projModStates[mod] = mod.state
mod.state = FittingModuleState.ONLINE
projDroneStates = {}
for drone in src.item.projectedDrones:
if drone.amountActive > 0:
projDroneStates[drone] = drone.amountActive
drone.amountActive = 0
projFighterStates = {}
for fighter in src.item.projectedFighters:
if fighter.active:
projFighterStates[fighter] = fighter.active
fighter.active = False
src.item.calculateModifiedAttributes()
subwarpSpeed = src.getMaxVelocity()
self._data[src.item.ID] = subwarpSpeed
for projInfo, state in projFitStates.items():
projInfo.active = state
for mod, state in modStates.items():
mod.state = state
for mod, state in projModStates.items():
mod.state = state
for drone, amountActive in projDroneStates.items():
drone.amountActive = amountActive
for fighter, state in projFighterStates.items():
fighter.active = state
src.item.calculateModifiedAttributes()
return subwarpSpeed

View File

@@ -0,0 +1,77 @@
# =============================================================================
# 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 math
from graphs.data.base import SmoothPointGetter
AU_METERS = 149597870700
class Distance2TimeGetter(SmoothPointGetter):
_baseResolution = 500
def _getCommonData(self, miscParams, src, tgt):
return {
'subwarpSpeed': self.graph._subspeedCache.getSubwarpSpeed(src),
'warpSpeed': src.item.warpSpeed}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
time = calculate_time_in_warp(
max_subwarp_speed=commonData['subwarpSpeed'],
max_warp_speed=commonData['warpSpeed'],
warp_dist=distance)
return time
# Taken from https://wiki.eveuniversity.org/Warp_time_calculation#Implementation
# with minor modifications
# Warp speed in AU/s, subwarp speed in m/s, distance in m
def calculate_time_in_warp(max_warp_speed, max_subwarp_speed, warp_dist):
if warp_dist == 0:
return 0
k_accel = max_warp_speed
k_decel = min(max_warp_speed / 3, 2)
warp_dropout_speed = max_subwarp_speed / 2
max_ms_warp_speed = max_warp_speed * AU_METERS
accel_dist = AU_METERS
decel_dist = max_ms_warp_speed / k_decel
minimum_dist = accel_dist + decel_dist
cruise_time = 0
if minimum_dist > warp_dist:
max_ms_warp_speed = warp_dist * k_accel * k_decel / (k_accel + k_decel)
else:
cruise_time = (warp_dist - minimum_dist) / max_ms_warp_speed
accel_time = math.log(max_ms_warp_speed / k_accel) / k_accel
decel_time = math.log(max_ms_warp_speed / warp_dropout_speed) / k_decel
total_time = cruise_time + accel_time + decel_time
return total_time

View File

@@ -0,0 +1,59 @@
# =============================================================================
# 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 graphs.data.base import FitGraph, Input, XDef, YDef
from service.const import GraphCacheCleanupReason
from .cache import SubwarpSpeedCache
from .getter import AU_METERS, Distance2TimeGetter
class FitWarpTimeGraph(FitGraph):
def __init__(self):
super().__init__()
self._subspeedCache = SubwarpSpeedCache()
def _clearInternalCache(self, reason, extraData):
if reason in (GraphCacheCleanupReason.fitChanged, GraphCacheCleanupReason.fitRemoved):
self._subspeedCache.clearForFit(extraData)
elif reason == GraphCacheCleanupReason.graphSwitched:
self._subspeedCache.clearAll()
# UI stuff
internalName = 'warpTimeGraph'
name = 'Warp Time'
xDefs = [
XDef(handle='distance', unit='AU', label='Distance', mainInput=('distance', 'AU')),
XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km'))]
yDefs = [YDef(handle='time', unit='s', label='Warp time')]
inputs = [
Input(handle='distance', unit='AU', label='Distance', iconID=1391, defaultValue=20, defaultRange=(0, 50)),
Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=1000, defaultRange=(150, 5000))]
srcExtraCols = ('WarpSpeed', 'WarpDistance')
# Calculation stuff
_normalizers = {
('distance', 'AU'): lambda v, src, tgt: v * AU_METERS,
('distance', 'km'): lambda v, src, tgt: v * 1000}
_limiters = {'distance': lambda src, tgt: (0, src.item.maxWarpDistance * AU_METERS)}
_getters = {('distance', 'time'): Distance2TimeGetter}
_denormalizers = {
('distance', 'AU'): lambda v, src, tgt: v / AU_METERS,
('distance', 'km'): lambda v, src, tgt: v / 1000}

4
graphs/events.py Normal file
View File

@@ -0,0 +1,4 @@
# noinspection PyPackageRequirements
import wx.lib.newevent
ResistModeChanged, RESIST_MODE_CHANGED = wx.lib.newevent.NewEvent()

18
graphs/gui/__init__.py Normal file
View File

@@ -0,0 +1,18 @@
# =============================================================================
# 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/>.
# =============================================================================

346
graphs/gui/canvasPanel.py Normal file
View File

@@ -0,0 +1,346 @@
# =============================================================================
# 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 itertools
import math
import os
import traceback
from bisect import bisect
# noinspection PyPackageRequirements
import wx
from logbook import Logger
from graphs.style import BASE_COLORS, LIGHTNESSES, STYLES, hsl_to_hsv
from gui.utils.numberFormatter import roundToPrec
pyfalog = Logger(__name__)
try:
import matplotlib as mpl
mpl_version = int(mpl.__version__[0]) or -1
if mpl_version >= 2:
mpl.use('wxagg')
graphFrame_enabled = True
else:
graphFrame_enabled = False
from matplotlib.lines import Line2D
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as Canvas
from matplotlib.figure import Figure
from matplotlib.colors import hsv_to_rgb
except ImportError as e:
pyfalog.warning('Matplotlib failed to import. Likely missing or incompatible version.')
graphFrame_enabled = False
except Exception:
# We can get exceptions deep within matplotlib. Catch those. See GH #1046
tb = traceback.format_exc()
pyfalog.critical('Exception when importing Matplotlib. Continuing without importing.')
pyfalog.critical(tb)
graphFrame_enabled = False
class GraphCanvasPanel(wx.Panel):
def __init__(self, graphFrame, parent):
super().__init__(parent)
self.graphFrame = graphFrame
# Remove matplotlib font cache, see #234
try:
cache_dir = mpl._get_cachedir()
except:
cache_dir = os.path.expanduser(os.path.join('~', '.matplotlib'))
cache_file = os.path.join(cache_dir, 'fontList.cache')
if os.access(cache_dir, os.W_OK | os.X_OK) and os.path.isfile(cache_file):
os.remove(cache_file)
mainSizer = wx.BoxSizer(wx.VERTICAL)
self.figure = Figure(figsize=(5, 3), tight_layout={'pad': 1.08})
rgbtuple = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE).Get()
clr = [c / 255. for c in rgbtuple]
self.figure.set_facecolor(clr)
self.figure.set_edgecolor(clr)
self.canvas = Canvas(self, -1, self.figure)
self.canvas.SetBackgroundColour(wx.Colour(*rgbtuple))
self.canvas.mpl_connect('button_press_event', self.OnMplCanvasClick)
self.subplot = self.figure.add_subplot(111)
self.subplot.grid(True)
mainSizer.Add(self.canvas, 1, wx.EXPAND | wx.ALL, 0)
self.SetSizer(mainSizer)
self.xMark = None
self.mplOnDragHandler = None
self.mplOnReleaseHandler = None
def draw(self, accurateMarks=True):
self.subplot.clear()
self.subplot.grid(True)
allXs = set()
allYs = set()
plotData = {}
legendData = []
chosenX = self.graphFrame.ctrlPanel.xType
chosenY = self.graphFrame.ctrlPanel.yType
self.subplot.set(
xlabel=self.graphFrame.ctrlPanel.formatLabel(chosenX),
ylabel=self.graphFrame.ctrlPanel.formatLabel(chosenY))
mainInput, miscInputs = self.graphFrame.ctrlPanel.getValues()
view = self.graphFrame.getView()
sources = self.graphFrame.ctrlPanel.sources
if view.hasTargets:
iterList = tuple(itertools.product(sources, self.graphFrame.ctrlPanel.targets))
else:
iterList = tuple((f, None) for f in sources)
# Draw plot lines and get data for legend
for source, target in iterList:
# Get line style data
try:
colorData = BASE_COLORS[source.colorID]
except KeyError:
pyfalog.warning('Invalid color "{}" for "{}"'.format(source.colorID, source.name))
continue
color = colorData.hsl
lineStyle = 'solid'
if target is not None:
try:
lightnessData = LIGHTNESSES[target.lightnessID]
except KeyError:
pyfalog.warning('Invalid lightness "{}" for "{}"'.format(target.lightnessID, target.name))
continue
color = lightnessData.func(color)
try:
lineStyleData = STYLES[target.lineStyleID]
except KeyError:
pyfalog.warning('Invalid line style "{}" for "{}"'.format(target.lightnessID, target.name))
continue
lineStyle = lineStyleData.mplSpec
color = hsv_to_rgb(hsl_to_hsv(color))
# Get point data
try:
xs, ys = view.getPlotPoints(
mainInput=mainInput,
miscInputs=miscInputs,
xSpec=chosenX,
ySpec=chosenY,
src=source,
tgt=target)
if not self.__checkNumbers(xs, ys):
pyfalog.warning('Failed to plot "{}" vs "{}" due to inf or NaN in values'.format(source.name, '' if target is None else target.name))
continue
plotData[(source, target)] = (xs, ys)
allXs.update(xs)
allYs.update(ys)
# If we have single data point, show marker - otherwise line won't be shown
if len(xs) == 1 and len(ys) == 1:
self.subplot.plot(xs, ys, color=color, linestyle=lineStyle, marker='.')
else:
self.subplot.plot(xs, ys, color=color, linestyle=lineStyle)
# Fill data for legend
if target is None:
legendData.append((color, lineStyle, source.shortName))
else:
legendData.append((color, lineStyle, '{} vs {}'.format(source.shortName, target.shortName)))
except Exception:
pyfalog.warning('Failed to plot "{}" vs "{}"'.format(source.name, '' if target is None else target.name))
self.canvas.draw()
self.Refresh()
return
# Setting Y limits for canvas
if self.graphFrame.ctrlPanel.showY0:
allYs.add(0)
canvasMinY, canvasMaxY = self._getLimits(allYs, minExtra=0.05, maxExtra=0.1)
canvasMinX, canvasMaxX = self._getLimits(allXs, minExtra=0.02, maxExtra=0.02)
self.subplot.set_ylim(bottom=canvasMinY, top=canvasMaxY)
self.subplot.set_xlim(left=canvasMinX, right=canvasMaxX)
# Process X marks line
if self.xMark is not None:
minX = min(allXs, default=None)
maxX = max(allXs, default=None)
if minX is not None and maxX is not None:
minY = min(allYs, default=None)
maxY = max(allYs, default=None)
yDiff = (maxY or 0) - (minY or 0)
xMark = max(min(self.xMark, maxX), minX)
# If in top 10% of X coordinates, align labels differently
if xMark > canvasMinX + 0.9 * (canvasMaxX - canvasMinX):
labelAlignment = 'right'
labelPrefix = ''
labelSuffix = ' '
else:
labelAlignment = 'left'
labelPrefix = ' '
labelSuffix = ''
# Draw line
self.subplot.axvline(x=xMark, linestyle='dotted', linewidth=1, color=(0, 0, 0))
# Draw its X position
if chosenX.unit is None:
xLabel = '{}{}{}'.format(labelPrefix, roundToPrec(xMark, 4), labelSuffix)
else:
xLabel = '{}{} {}{}'.format(labelPrefix, roundToPrec(xMark, 4), chosenX.unit, labelSuffix)
self.subplot.annotate(
xLabel, xy=(xMark, canvasMaxY - 0.01 * (canvasMaxY - canvasMinY)), xytext=(0, 0), annotation_clip=False,
textcoords='offset pixels', ha=labelAlignment, va='top', fontsize='small')
# Get Y values
yMarks = set()
def addYMark(val):
if val is None:
return
# Round according to shown Y range - the bigger the range,
# the rougher the rounding
if yDiff != 0:
rounded = roundToPrec(val, 4, nsValue=yDiff)
else:
rounded = val
# If due to some bug or insufficient plot density we're
# out of bounds, do not add anything
if minY <= val <= maxY or minY <= rounded <= maxY:
yMarks.add(rounded)
for source, target in iterList:
xs, ys = plotData[(source, target)]
if not xs or xMark < min(xs) or xMark > max(xs):
continue
# Fetch values from graphs when we're asked to provide accurate data
if accurateMarks:
try:
y = view.getPoint(
x=xMark,
miscInputs=miscInputs,
xSpec=chosenX,
ySpec=chosenY,
src=source,
tgt=target)
addYMark(y)
except Exception:
pyfalog.warning('Failed to get X mark for "{}" vs "{}"'.format(source.name, '' if target is None else target.name))
# Silently skip this mark, otherwise other marks and legend display will fail
continue
# Otherwise just do linear interpolation between two points
else:
if xMark in xs:
# We might have multiples of the same value in our sequence, pick value for the last one
idx = len(xs) - xs[::-1].index(xMark) - 1
addYMark(ys[idx])
continue
idx = bisect(xs, xMark)
yMark = self._interpolateX(x=xMark, x1=xs[idx - 1], y1=ys[idx - 1], x2=xs[idx], y2=ys[idx])
addYMark(yMark)
# Draw Y values
for yMark in yMarks:
self.subplot.annotate(
'{}{}{}'.format(labelPrefix, yMark, labelSuffix), xy=(xMark, yMark), xytext=(0, 0),
textcoords='offset pixels', ha=labelAlignment, va='center', fontsize='small')
legendLines = []
for i, iData in enumerate(legendData):
color, lineStyle, label = iData
legendLines.append(Line2D([0], [0], color=color, linestyle=lineStyle, label=label.replace('$', '\$')))
if len(legendLines) > 0 and self.graphFrame.ctrlPanel.showLegend:
legend = self.subplot.legend(handles=legendLines)
for t in legend.get_texts():
t.set_fontsize('small')
for l in legend.get_lines():
l.set_linewidth(1)
self.canvas.draw()
self.Refresh()
def markXApproximate(self, x):
if x is not None:
self.xMark = x
self.draw(accurateMarks=False)
def unmarkX(self):
self.xMark = None
self.draw()
@staticmethod
def _getLimits(vals, minExtra=0, maxExtra=0):
minVal = min(vals, default=0)
maxVal = max(vals, default=0)
# Extend range a little for some visual space
valRange = maxVal - minVal
minVal -= valRange * minExtra
maxVal += valRange * maxExtra
# Extend by % of value if we show function of a constant
if minVal == maxVal:
minVal -= minVal * 0.05
maxVal += minVal * 0.05
# If still equal, function is 0, spread out visual space as special case
if minVal == maxVal:
minVal -= 5
maxVal += 5
return minVal, maxVal
@staticmethod
def _interpolateX(x, x1, y1, x2, y2):
pos = (x - x1) / (x2 - x1)
y = y1 + pos * (y2 - y1)
return y
@staticmethod
def __checkNumbers(xs, ys):
for number in itertools.chain(xs, ys):
if math.isnan(number) or math.isinf(number):
return False
return True
# Matplotlib event handlers
def OnMplCanvasClick(self, event):
if event.button == 1:
if not self.mplOnDragHandler:
self.mplOnDragHandler = self.canvas.mpl_connect('motion_notify_event', self.OnMplCanvasDrag)
if not self.mplOnReleaseHandler:
self.mplOnReleaseHandler = self.canvas.mpl_connect('button_release_event', self.OnMplCanvasRelease)
self.markXApproximate(event.xdata)
elif event.button == 3:
self.unmarkX()
def OnMplCanvasDrag(self, event):
self.markXApproximate(event.xdata)
def OnMplCanvasRelease(self, event):
if event.button == 1:
if self.mplOnDragHandler:
self.canvas.mpl_disconnect(self.mplOnDragHandler)
self.mplOnDragHandler = None
if self.mplOnReleaseHandler:
self.canvas.mpl_disconnect(self.mplOnReleaseHandler)
self.mplOnReleaseHandler = None
# Do not write markX here because of strange mouse behavior: when dragging,
# sometimes when you release button, x coordinate changes. To avoid that,
# we just re-use coordinates set on click/drag and just request to redraw
# using accurate data
self.draw(accurateMarks=True)

471
graphs/gui/ctrlPanel.py Normal file
View File

@@ -0,0 +1,471 @@
# =============================================================================
# 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 collections import namedtuple
# noinspection PyPackageRequirements
import wx
from gui.bitmap_loader import BitmapLoader
from gui.contextMenu import ContextMenu
from gui.utils.inputs import FloatBox, FloatRangeBox
from service.const import GraphCacheCleanupReason
from service.fit import Fit
from .lists import SourceWrapperList, TargetWrapperList
from .vector import VectorPicker
InputData = namedtuple('InputData', ('handle', 'unit', 'value'))
InputBox = namedtuple('InputBox', ('handle', 'unit', 'textBox', 'icon', 'label'))
CheckBox = namedtuple('CheckBox', ('handle', 'checkBox'))
class GraphControlPanel(wx.Panel):
def __init__(self, graphFrame, parent):
super().__init__(parent)
self.graphFrame = graphFrame
self._mainInputBox = None
self._miscInputBoxes = []
self._inputCheckboxes = []
self._storedRanges = {}
self._storedConsts = {}
mainSizer = wx.BoxSizer(wx.VERTICAL)
optsSizer = wx.BoxSizer(wx.HORIZONTAL)
commonOptsSizer = wx.BoxSizer(wx.VERTICAL)
ySubSelectionSizer = wx.BoxSizer(wx.HORIZONTAL)
yText = wx.StaticText(self, wx.ID_ANY, 'Axis Y:')
ySubSelectionSizer.Add(yText, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.ySubSelection = wx.Choice(self, wx.ID_ANY)
self.ySubSelection.Bind(wx.EVT_CHOICE, self.OnYTypeUpdate)
ySubSelectionSizer.Add(self.ySubSelection, 1, wx.EXPAND | wx.ALL, 0)
commonOptsSizer.Add(ySubSelectionSizer, 0, wx.EXPAND | wx.ALL, 0)
xSubSelectionSizer = wx.BoxSizer(wx.HORIZONTAL)
xText = wx.StaticText(self, wx.ID_ANY, 'Axis X:')
xSubSelectionSizer.Add(xText, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.xSubSelection = wx.Choice(self, wx.ID_ANY)
self.xSubSelection.Bind(wx.EVT_CHOICE, self.OnXTypeUpdate)
xSubSelectionSizer.Add(self.xSubSelection, 1, wx.EXPAND | wx.ALL, 0)
commonOptsSizer.Add(xSubSelectionSizer, 0, wx.EXPAND | wx.TOP, 5)
self.showLegendCb = wx.CheckBox(self, wx.ID_ANY, 'Show legend', wx.DefaultPosition, wx.DefaultSize, 0)
self.showLegendCb.SetValue(True)
self.showLegendCb.Bind(wx.EVT_CHECKBOX, self.OnShowLegendChange)
commonOptsSizer.Add(self.showLegendCb, 0, wx.EXPAND | wx.TOP, 5)
self.showY0Cb = wx.CheckBox(self, wx.ID_ANY, 'Always show Y = 0', wx.DefaultPosition, wx.DefaultSize, 0)
self.showY0Cb.SetValue(True)
self.showY0Cb.Bind(wx.EVT_CHECKBOX, self.OnShowY0Change)
commonOptsSizer.Add(self.showY0Cb, 0, wx.EXPAND | wx.TOP, 5)
optsSizer.Add(commonOptsSizer, 0, wx.EXPAND | wx.RIGHT, 10)
graphOptsSizer = wx.BoxSizer(wx.HORIZONTAL)
self.inputsSizer = wx.BoxSizer(wx.VERTICAL)
graphOptsSizer.Add(self.inputsSizer, 1, wx.EXPAND | wx.ALL, 0)
vectorSize = 90 if 'wxGTK' in wx.PlatformInfo else 75
self.srcVectorSizer = wx.BoxSizer(wx.VERTICAL)
self.srcVectorLabel = wx.StaticText(self, wx.ID_ANY, '')
self.srcVectorSizer.Add(self.srcVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL| wx.BOTTOM, 5)
self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=vectorSize, offset=0)
self.srcVector.Bind(VectorPicker.EVT_VECTOR_CHANGED, self.OnNonMainInputChanged)
self.srcVectorSizer.Add(self.srcVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0)
graphOptsSizer.Add(self.srcVectorSizer, 0, wx.EXPAND | wx.LEFT, 15)
self.tgtVectorSizer = wx.BoxSizer(wx.VERTICAL)
self.tgtVectorLabel = wx.StaticText(self, wx.ID_ANY, '')
self.tgtVectorSizer.Add(self.tgtVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM, 5)
self.tgtVector = VectorPicker(self, style=wx.NO_BORDER, size=vectorSize, offset=0)
self.tgtVector.Bind(VectorPicker.EVT_VECTOR_CHANGED, self.OnNonMainInputChanged)
self.tgtVectorSizer.Add(self.tgtVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0)
graphOptsSizer.Add(self.tgtVectorSizer, 0, wx.EXPAND | wx.LEFT, 10)
optsSizer.Add(graphOptsSizer, 1, wx.EXPAND | wx.ALL, 0)
contextSizer = wx.BoxSizer(wx.VERTICAL)
savedFont = self.GetFont()
contextIconFont = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
contextIconFont.SetPointSize(8)
self.SetFont(contextIconFont)
self.contextIcon = wx.StaticText(self, wx.ID_ANY, '\u2630', size=wx.Size((10, -1)))
self.contextIcon.Bind(wx.EVT_CONTEXT_MENU, self.contextMenuHandler)
self.contextIcon.Bind(wx.EVT_LEFT_UP, self.contextMenuHandler)
self.SetFont(savedFont)
contextSizer.Add(self.contextIcon, 0, wx.EXPAND | wx.ALL, 0)
optsSizer.Add(contextSizer, 0, wx.EXPAND | wx.ALL, 0)
mainSizer.Add(optsSizer, 0, wx.EXPAND | wx.ALL, 10)
self.srcTgtSizer = wx.BoxSizer(wx.HORIZONTAL)
self.sourceList = SourceWrapperList(graphFrame, self)
self.sourceList.SetMinSize((270, -1))
self.srcTgtSizer.Add(self.sourceList, 1, wx.EXPAND | wx.ALL, 0)
self.targetList = TargetWrapperList(graphFrame, self)
self.targetList.SetMinSize((270, -1))
self.srcTgtSizer.Add(self.targetList, 1, wx.EXPAND | wx.LEFT, 10)
mainSizer.Add(self.srcTgtSizer, 1, wx.EXPAND | wx.LEFT | wx.BOTTOM | wx.RIGHT, 10)
self.SetSizer(mainSizer)
self.inputTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.OnInputTimer, self.inputTimer)
self._setVectorDefaults()
def updateControls(self, layout=True):
if layout:
self.Freeze()
self._clearStoredValues()
view = self.graphFrame.getView()
self.refreshAxeLabels()
# Vectors
self._setVectorDefaults()
if view.srcVectorDef is not None:
self.srcVector.Show(True)
self.srcVectorLabel.Show(True)
self.srcVectorLabel.SetLabel(view.srcVectorDef.label)
else:
self.srcVector.Show(False)
self.srcVectorLabel.Show(False)
if view.tgtVectorDef is not None:
self.tgtVector.Show(True)
self.tgtVectorLabel.Show(True)
self.tgtVectorLabel.SetLabel(view.tgtVectorDef.label)
else:
self.tgtVector.Show(False)
self.tgtVectorLabel.Show(False)
# Source and target list
self.refreshColumns(layout=False)
self.targetList.Show(view.hasTargets)
# Inputs
self._updateInputs(storeInputs=False)
# Context icon
self.contextIcon.Show(ContextMenu.hasMenu(self, None, None, (view.internalName,)))
if layout:
self.graphFrame.Layout()
self.graphFrame.UpdateWindowSize()
self.Thaw()
def _updateInputs(self, storeInputs=True):
if storeInputs:
self._storeCurrentValues()
# Clean up old inputs
for inputBox in (self._mainInputBox, *self._miscInputBoxes):
if inputBox is None:
continue
for child in (inputBox.textBox, inputBox.icon, inputBox.label):
if child is not None:
child.Destroy()
for checkbox in self._inputCheckboxes:
checkbox.checkBox.Destroy()
self.inputsSizer.Clear()
self._mainInputBox = None
self._miscInputBoxes.clear()
self._inputCheckboxes.clear()
# Update vectors
view = self.graphFrame.getView()
handledHandles = set()
if view.srcVectorDef is not None:
self.__handleVector(view.srcVectorDef, self.srcVector, handledHandles, self.xType.mainInput[0])
if view.tgtVectorDef is not None:
self.__handleVector(view.tgtVectorDef, self.tgtVector, handledHandles, self.xType.mainInput[0])
# Update inputs
self.__addInputField(view.inputMap[self.xType.mainInput], handledHandles, mainInput=True)
for inputDef in view.inputs:
if inputDef.handle in handledHandles:
continue
self.__addInputField(inputDef, handledHandles)
# Add checkboxes
for checkboxDef in view.checkboxes:
if checkboxDef.handle in handledHandles:
continue
self.__addInputCheckbox(checkboxDef, handledHandles)
def __handleVector(self, vectorDef, vector, handledHandles, mainInputHandle):
handledHandles.add(vectorDef.lengthHandle)
handledHandles.add(vectorDef.angleHandle)
try:
storedLength = self._storedConsts[(vectorDef.lengthHandle, vectorDef.lengthUnit)]
except KeyError:
pass
else:
vector.SetLength(storedLength / 100)
try:
storedAngle = self._storedConsts[(vectorDef.angleHandle, vectorDef.angleUnit)]
except KeyError:
pass
else:
vector.SetAngle(storedAngle)
vector.SetDirectionOnly(vectorDef.lengthHandle == mainInputHandle)
def __addInputField(self, inputDef, handledHandles, mainInput=False):
if not self.__checkInputConditions(inputDef):
return
handledHandles.add(inputDef.handle)
fieldSizer = wx.BoxSizer(wx.HORIZONTAL)
tooltipText = (inputDef.mainTooltip if mainInput else inputDef.secondaryTooltip) or ''
if mainInput:
fieldTextBox = FloatRangeBox(self, self._storedRanges.get((inputDef.handle, inputDef.unit), inputDef.defaultRange))
fieldTextBox.Bind(wx.EVT_TEXT, self.OnMainInputChanged)
else:
fieldTextBox = FloatBox(self, self._storedConsts.get((inputDef.handle, inputDef.unit), inputDef.defaultValue))
fieldTextBox.Bind(wx.EVT_TEXT, self.OnNonMainInputChanged)
fieldTextBox.SetToolTip(wx.ToolTip(tooltipText))
fieldSizer.Add(fieldTextBox, 0, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
fieldIcon = None
if inputDef.iconID is not None:
icon = BitmapLoader.getBitmap(inputDef.iconID, 'icons')
if icon is not None:
fieldIcon = wx.StaticBitmap(self)
fieldIcon.SetBitmap(icon)
fieldIcon.SetToolTip(wx.ToolTip(tooltipText))
fieldSizer.Add(fieldIcon, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 3)
fieldLabel = wx.StaticText(self, wx.ID_ANY, self.formatLabel(inputDef))
fieldLabel.SetToolTip(wx.ToolTip(tooltipText))
fieldSizer.Add(fieldLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0)
self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5)
# Store info about added input box
inputBox = InputBox(handle=inputDef.handle, unit=inputDef.unit, textBox=fieldTextBox, icon=fieldIcon, label=fieldLabel)
if mainInput:
self._mainInputBox = inputBox
else:
self._miscInputBoxes.append(inputBox)
def __addInputCheckbox(self, checkboxDef, handledHandles):
if not self.__checkInputConditions(checkboxDef):
return
handledHandles.add(checkboxDef.handle)
fieldCheckbox = wx.CheckBox(self, wx.ID_ANY, checkboxDef.label, wx.DefaultPosition, wx.DefaultSize, 0)
fieldCheckbox.SetValue(self._storedConsts.get((checkboxDef.handle, None), checkboxDef.defaultValue))
fieldCheckbox.Bind(wx.EVT_CHECKBOX, self.OnNonMainInputChanged)
self.inputsSizer.Add(fieldCheckbox, 0, wx.BOTTOM, 5)
# Store info about added checkbox
checkbox = CheckBox(handle=checkboxDef.handle, checkBox=fieldCheckbox)
self._inputCheckboxes.append(checkbox)
def __checkInputConditions(self, inputDef):
if not inputDef.conditions:
return True
selectedX = self.xType
selectedY = self.yType
for xCond, yCond in inputDef.conditions:
xMatch = True
yMatch = True
if xCond is not None:
xCondHandle, xCondUnit = xCond
xMatch = selectedX.handle == xCondHandle and selectedX.unit == xCondUnit
if yCond is not None:
yCondHandle, yCondUnit = yCond
yMatch = selectedY.handle == yCondHandle and selectedY.unit == yCondUnit
if xMatch and yMatch:
return True
return False
def refreshAxeLabels(self, restoreSelection=False):
view = self.graphFrame.getView()
if restoreSelection:
selectedY = self.ySubSelection.GetSelection()
selectedX = self.xSubSelection.GetSelection()
else:
selectedY = selectedX = 0
self.ySubSelection.Clear()
for yDef in view.yDefs:
self.ySubSelection.Append(self.formatLabel(yDef, selector=True), yDef)
self.ySubSelection.Enable(len(view.yDefs) > 1)
self.ySubSelection.SetSelection(selectedY)
self.xSubSelection.Clear()
for xDef in view.xDefs:
self.xSubSelection.Append(self.formatLabel(xDef, selector=True), xDef)
self.xSubSelection.Enable(len(view.xDefs) > 1)
self.xSubSelection.SetSelection(selectedX)
def refreshColumns(self, layout=True):
view = self.graphFrame.getView()
self.sourceList.refreshExtraColumns(view.srcExtraCols)
self.targetList.refreshExtraColumns(view.tgtExtraCols)
self.srcTgtSizer.Detach(self.sourceList)
self.srcTgtSizer.Detach(self.targetList)
self.srcTgtSizer.Add(self.sourceList, self.sourceList.getWidthProportion(), wx.EXPAND | wx.ALL, 0)
self.srcTgtSizer.Add(self.targetList, self.targetList.getWidthProportion(), wx.EXPAND | wx.LEFT, 10)
self.Layout()
def OnShowLegendChange(self, event):
event.Skip()
self.graphFrame.draw()
def OnShowY0Change(self, event):
event.Skip()
self.graphFrame.draw()
def OnYTypeUpdate(self, event):
event.Skip()
self._updateInputs()
self.graphFrame.resetXMark()
self.graphFrame.Layout()
self.graphFrame.UpdateWindowSize()
self.graphFrame.draw()
def OnXTypeUpdate(self, event):
event.Skip()
self._updateInputs()
self.graphFrame.resetXMark()
self.graphFrame.Layout()
self.graphFrame.UpdateWindowSize()
self.graphFrame.draw()
def OnMainInputChanged(self, event):
event.Skip()
self.graphFrame.resetXMark()
self.inputTimer.Stop()
self.inputTimer.Start(Fit.getInstance().serviceFittingOptions['marketSearchDelay'], True)
def OnNonMainInputChanged(self, event):
event.Skip()
self.inputTimer.Stop()
self.inputTimer.Start(Fit.getInstance().serviceFittingOptions['marketSearchDelay'], True)
def OnInputTimer(self, event):
event.Skip()
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.inputChanged)
self.graphFrame.draw()
def getValues(self):
view = self.graphFrame.getView()
misc = []
processedHandles = set()
def addMiscData(handle, unit, value):
if handle in processedHandles:
return
inputData = InputData(handle=handle, unit=unit, value=value)
misc.append(inputData)
# Main input box
main = InputData(handle=self._mainInputBox.handle, unit=self._mainInputBox.unit, value=self._mainInputBox.textBox.GetValueRange())
processedHandles.add(self._mainInputBox.handle)
# Vectors
srcVectorDef = view.srcVectorDef
if srcVectorDef is not None:
if not self.srcVector.IsDirectionOnly:
addMiscData(handle=srcVectorDef.lengthHandle, unit=srcVectorDef.lengthUnit, value=self.srcVector.GetLength() * 100)
addMiscData(handle=srcVectorDef.angleHandle, unit=srcVectorDef.angleUnit, value=self.srcVector.GetAngle())
tgtVectorDef = view.tgtVectorDef
if tgtVectorDef is not None:
if not self.tgtVector.IsDirectionOnly:
addMiscData(handle=tgtVectorDef.lengthHandle, unit=tgtVectorDef.lengthUnit, value=self.tgtVector.GetLength() * 100)
addMiscData(handle=tgtVectorDef.angleHandle, unit=tgtVectorDef.angleUnit, value=self.tgtVector.GetAngle())
# Other input boxes
for inputBox in self._miscInputBoxes:
addMiscData(handle=inputBox.handle, unit=inputBox.unit, value=inputBox.textBox.GetValueFloat())
# Checkboxes
for checkbox in self._inputCheckboxes:
addMiscData(handle=checkbox.handle, unit=None, value=checkbox.checkBox.GetValue())
return main, misc
@property
def showLegend(self):
return self.showLegendCb.GetValue()
@property
def showY0(self):
return self.showY0Cb.GetValue()
@property
def yType(self):
return self.ySubSelection.GetClientData(self.ySubSelection.GetSelection())
@property
def xType(self):
return self.xSubSelection.GetClientData(self.xSubSelection.GetSelection())
@property
def sources(self):
return self.sourceList.wrappers
@property
def targets(self):
return self.targetList.wrappers
# Fit events
def OnFitRenamed(self, event):
self.sourceList.OnFitRenamed(event)
self.targetList.OnFitRenamed(event)
def OnFitChanged(self, event):
self.sourceList.OnFitChanged(event)
self.targetList.OnFitChanged(event)
def OnFitRemoved(self, event):
self.sourceList.OnFitRemoved(event)
self.targetList.OnFitRemoved(event)
# Target profile events
def OnProfileRenamed(self, event):
self.sourceList.OnProfileRenamed(event)
self.targetList.OnProfileRenamed(event)
def OnProfileChanged(self, event):
self.sourceList.OnProfileChanged(event)
self.targetList.OnProfileChanged(event)
def OnProfileRemoved(self, event):
self.sourceList.OnProfileRemoved(event)
self.targetList.OnProfileRemoved(event)
def OnResistModeChanged(self, event):
self.targetList.OnResistModeChanged(event)
def formatLabel(self, axisDef, selector=False):
label = axisDef.selectorLabel if selector else axisDef.label
if axisDef.unit is None:
return label
return '{}, {}'.format(label, axisDef.unit)
def _storeCurrentValues(self):
main, misc = self.getValues()
if main is not None:
self._storedRanges[(main.handle, main.unit)] = main.value
for input in misc:
self._storedConsts[(input.handle, input.unit)] = input.value
def _clearStoredValues(self):
self._storedRanges.clear()
self._storedConsts.clear()
def _setVectorDefaults(self):
self.srcVector.SetValue(length=0, angle=90)
self.tgtVector.SetValue(length=1, angle=90)
def contextMenuHandler(self, event):
viewName = self.graphFrame.getView().internalName
menu = ContextMenu.getMenu(self, None, None, (viewName,))
if menu is not None:
self.PopupMenu(menu)
event.Skip()

245
graphs/gui/frame.py Normal file
View File

@@ -0,0 +1,245 @@
# =============================================================================
# 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 logbook import Logger
import gui.display
import gui.globalEvents as GE
import gui.mainFrame
from graphs.data.base import FitGraph
from graphs.events import RESIST_MODE_CHANGED
from gui.auxFrame import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from service.const import GraphCacheCleanupReason
from service.settings import GraphSettings
from . import canvasPanel
from .ctrlPanel import GraphControlPanel
pyfalog = Logger(__name__)
REDRAW_DELAY = 500
class GraphFrame(AuxiliaryFrame):
def __init__(self, parent):
if not canvasPanel.graphFrame_enabled:
pyfalog.warning('Matplotlib is not enabled. Skipping initialization.')
return
super().__init__(parent, title='Graphs', size=(520, 390), resizeable=True)
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.SetIcon(wx.Icon(BitmapLoader.getBitmap('graphs_small', 'gui')))
mainSizer = wx.BoxSizer(wx.VERTICAL)
# Layout - graph selector
self.graphSelection = wx.Choice(self, wx.ID_ANY, style=0)
self.graphSelection.Bind(wx.EVT_CHOICE, self.OnGraphSwitched)
mainSizer.Add(self.graphSelection, 0, wx.EXPAND)
# Layout - plot area
self.canvasPanel = canvasPanel.GraphCanvasPanel(self, self)
mainSizer.Add(self.canvasPanel, 1, wx.EXPAND | wx.ALL, 0)
mainSizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, wx.EXPAND)
# Layout - graph control panel
self.ctrlPanel = GraphControlPanel(self, self)
mainSizer.Add(self.ctrlPanel, 0, wx.EXPAND | wx.ALL, 0)
self.SetSizer(mainSizer)
# Setup - graph selector
for view in FitGraph.views:
self.graphSelection.Append(view.name, view())
self.graphSelection.SetSelection(0)
self.ctrlPanel.updateControls(layout=False)
# Event bindings - local events
self.Bind(wx.EVT_CLOSE, self.OnClose)
self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent)
# Event bindings - external events
self.mainFrame.Bind(GE.FIT_RENAMED, self.OnFitRenamed)
self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged)
self.mainFrame.Bind(GE.FIT_REMOVED, self.OnFitRemoved)
self.mainFrame.Bind(GE.TARGET_PROFILE_RENAMED, self.OnProfileRenamed)
self.mainFrame.Bind(GE.TARGET_PROFILE_CHANGED, self.OnProfileChanged)
self.mainFrame.Bind(GE.TARGET_PROFILE_REMOVED, self.OnProfileRemoved)
self.mainFrame.Bind(RESIST_MODE_CHANGED, self.OnResistModeChanged)
self.mainFrame.Bind(GE.GRAPH_OPTION_CHANGED, self.OnGraphOptionChanged)
self.mainFrame.Bind(GE.EFFECTIVE_HP_TOGGLED, self.OnEffectiveHpToggled)
self.drawTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.OnDrawTimer, self.drawTimer)
self.Layout()
self.UpdateWindowSize()
self.draw()
@classmethod
def openOne(cls, parent):
if canvasPanel.graphFrame_enabled:
super().openOne(parent)
def UpdateWindowSize(self):
curW, curH = self.GetSize()
bestW, bestH = self.GetBestSize()
newW = max(curW, bestW)
newH = max(curH, bestH)
if newW > curW or newH > curH:
newSize = wx.Size(newW, newH)
self.SetSize(newSize)
self.SetMinSize(newSize)
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == wx.WXK_ESCAPE and mstate.GetModifiers() == wx.MOD_NONE:
self.Close()
return
event.Skip()
# Fit events
def OnFitRenamed(self, event):
event.Skip()
self.ctrlPanel.OnFitRenamed(event)
self.draw()
def OnFitChanged(self, event):
event.Skip()
for fitID in event.fitIDs:
self.clearCache(reason=GraphCacheCleanupReason.fitChanged, extraData=fitID)
self.ctrlPanel.OnFitChanged(event)
# Data has to be recalculated - delay redraw
# to give time to finish UI update in main window
self.drawTimer.Stop()
self.drawTimer.Start(REDRAW_DELAY, True)
def OnFitRemoved(self, event):
event.Skip()
self.clearCache(reason=GraphCacheCleanupReason.fitRemoved, extraData=event.fitID)
self.ctrlPanel.OnFitRemoved(event)
self.draw()
# Target profile events
def OnProfileRenamed(self, event):
event.Skip()
self.ctrlPanel.OnProfileRenamed(event)
self.draw()
def OnProfileChanged(self, event):
event.Skip()
self.clearCache(reason=GraphCacheCleanupReason.profileChanged, extraData=event.profileID)
self.ctrlPanel.OnProfileChanged(event)
self.draw()
def OnProfileRemoved(self, event):
event.Skip()
self.clearCache(reason=GraphCacheCleanupReason.profileRemoved, extraData=event.profileID)
self.ctrlPanel.OnProfileRemoved(event)
self.draw()
def OnResistModeChanged(self, event):
event.Skip()
for fitID in event.fitIDs:
self.clearCache(reason=GraphCacheCleanupReason.resistModeChanged, extraData=fitID)
self.ctrlPanel.OnResistModeChanged(event)
self.draw()
def OnGraphOptionChanged(self, event):
event.Skip()
layout = getattr(event, 'refreshColumns', False) or getattr(event, 'refreshColumns', False)
if layout:
self.ctrlPanel.Freeze()
if getattr(event, 'refreshAxeLabels', False):
self.ctrlPanel.refreshAxeLabels(restoreSelection=True)
if getattr(event, 'refreshColumns', False):
self.ctrlPanel.refreshColumns()
self.Layout()
self.ctrlPanel.Thaw()
self.clearCache(reason=GraphCacheCleanupReason.optionChanged)
self.draw()
def OnEffectiveHpToggled(self, event):
event.Skip()
currentView = self.getView()
# Redraw graph if needed
if currentView.usesHpEffectivity:
currentView.isEffective = event.effective
self.ctrlPanel.refreshAxeLabels(restoreSelection=True)
self.Layout()
self.clearCache(reason=GraphCacheCleanupReason.hpEffectivityChanged)
# Data has to be recalculated - delay redraw
# to give time to finish UI update in main window
self.drawTimer.Stop()
self.drawTimer.Start(REDRAW_DELAY, True)
# Even if graph is not selected, keep it updated
for idx in range(self.graphSelection.GetCount()):
view = self.getView(idx=idx)
if view is currentView:
continue
if view.usesHpEffectivity:
view.isEffective = event.effective
def OnGraphSwitched(self, event):
view = self.getView()
GraphSettings.getInstance().set('selectedGraph', view.internalName)
self.clearCache(reason=GraphCacheCleanupReason.graphSwitched)
self.resetXMark()
self.ctrlPanel.updateControls()
self.draw()
event.Skip()
def OnDrawTimer(self, event):
event.Skip()
self.draw()
def OnClose(self, event):
self.mainFrame.Unbind(GE.FIT_RENAMED, handler=self.OnFitRenamed)
self.mainFrame.Unbind(GE.FIT_CHANGED, handler=self.OnFitChanged)
self.mainFrame.Unbind(GE.FIT_REMOVED, handler=self.OnFitRemoved)
self.mainFrame.Unbind(GE.TARGET_PROFILE_RENAMED, handler=self.OnProfileRenamed)
self.mainFrame.Unbind(GE.TARGET_PROFILE_CHANGED, handler=self.OnProfileChanged)
self.mainFrame.Unbind(GE.TARGET_PROFILE_REMOVED, handler=self.OnProfileRemoved)
self.mainFrame.Unbind(RESIST_MODE_CHANGED, handler=self.OnResistModeChanged)
self.mainFrame.Unbind(GE.GRAPH_OPTION_CHANGED, handler=self.OnGraphOptionChanged)
self.mainFrame.Unbind(GE.EFFECTIVE_HP_TOGGLED, handler=self.OnEffectiveHpToggled)
event.Skip()
def getView(self, idx=None):
if idx is None:
idx = self.graphSelection.GetSelection()
return self.graphSelection.GetClientData(idx)
def clearCache(self, reason, extraData=None):
self.getView().clearCache(reason, extraData)
def draw(self):
self.canvasPanel.draw()
def resetXMark(self):
self.canvasPanel.xMark = None

379
graphs/gui/lists.py Normal file
View File

@@ -0,0 +1,379 @@
# =============================================================================
# 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
import gui.display
from eos.saveddata.targetProfile import TargetProfile
from graphs.style import BASE_COLORS, LIGHTNESSES, STYLES
from graphs.wrapper import SourceWrapper, TargetWrapper
from gui.builtinViewColumns.graphColor import GraphColor
from gui.builtinViewColumns.graphLightness import GraphLightness
from gui.builtinViewColumns.graphLineStyle import GraphLineStyle
from gui.contextMenu import ContextMenu
from service.const import GraphCacheCleanupReason
from service.fit import Fit
from .stylePickers import ColorPickerPopup, LightnessPickerPopup, LineStylePickerPopup
class BaseWrapperList(gui.display.Display):
def __init__(self, graphFrame, parent):
super().__init__(parent)
self.graphFrame = graphFrame
self._wrappers = []
self.hoveredRow = None
self.hoveredColumn = None
self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent)
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick)
self.Bind(wx.EVT_MOTION, self.OnMouseMove)
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
@property
def wrappers(self):
# Sort fits first, then target profiles
return sorted(self._wrappers, key=lambda w: not w.isFit)
# UI-related stuff
@property
def defaultTTText(self):
raise NotImplementedError
def refreshExtraColumns(self, extraColSpecs):
baseColNames = set()
for baseColName in self.DEFAULT_COLS:
if ":" in baseColName:
baseColName = baseColName.split(":", 1)[0]
baseColNames.add(baseColName)
columnsToRemove = set()
for col in self.activeColumns:
if col.name not in baseColNames:
columnsToRemove.add(col)
for col in columnsToRemove:
self.removeColumn(col)
for colSpec in extraColSpecs:
self.appendColumnBySpec(colSpec)
self.refreshView()
def refreshView(self):
self.refresh(self.wrappers)
def updateView(self):
self.update(self.wrappers)
# UI event handling
def OnMouseMove(self, event):
row, _, col = self.HitTestSubItem(event.Position)
if row != self.hoveredRow or col != self.hoveredColumn:
if self.ToolTip is not None:
self.SetToolTip(None)
else:
self.hoveredRow = row
self.hoveredColumn = col
if row != -1 and col != -1 and col < self.ColumnCount:
item = self.getWrapper(row)
if item is None:
return
tooltip = self.activeColumns[col].getToolTip(item)
if tooltip:
self.SetToolTip(tooltip)
else:
self.SetToolTip(None)
else:
self.SetToolTip(self.defaultTTText)
event.Skip()
def OnLeaveWindow(self, event):
self.SetToolTip(None)
self.hoveredRow = None
self.hoveredColumn = None
event.Skip()
def handleDrag(self, type, fitID):
if type == 'fit' and not self.containsFitID(fitID):
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
self.appendItem(fit)
self.updateView()
self.graphFrame.draw()
def OnLeftDown(self, event):
row, _ = self.HitTest(event.Position)
if row != -1:
pickers = {
self.getColIndex(GraphColor): ColorPickerPopup,
self.getColIndex(GraphLightness): LightnessPickerPopup,
self.getColIndex(GraphLineStyle): LineStylePickerPopup}
# In case we had no index for some column, remove None
pickers.pop(None, None)
col = self.getColumn(event.Position)
if col in pickers:
picker = pickers[col]
wrapper = self.getWrapper(row)
if wrapper is not None:
win = picker(parent=self, wrapper=wrapper)
pos = wx.GetMousePosition()
win.Position(pos, (0, 0))
win.Popup()
return
event.Skip()
def OnLineStyleChange(self):
self.updateView()
self.graphFrame.draw()
def OnLeftDClick(self, event):
row, _ = self.HitTest(event.Position)
wrapper = self.getWrapper(row)
if wrapper is None:
return
self.removeWrappers([wrapper])
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL:
self.selectAll()
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE:
self.removeWrappers(self.getSelectedWrappers())
event.Skip()
# Wrapper-related methods
def getWrapper(self, row):
if row == -1:
return None
try:
return self.wrappers[row]
except IndexError:
return None
def removeWrappers(self, wrappers):
wrappers = set(wrappers).intersection(self._wrappers)
if not wrappers:
return
for wrapper in wrappers:
self._wrappers.remove(wrapper)
self.updateView()
for wrapper in wrappers:
if wrapper.isFit:
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.fitRemoved, extraData=wrapper.item.ID)
elif wrapper.isProfile:
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.profileRemoved, extraData=wrapper.item.ID)
self.graphFrame.draw()
def getSelectedWrappers(self):
wrappers = []
for row in self.getSelectedRows():
wrapper = self.getWrapper(row)
if wrapper is None:
continue
wrappers.append(wrapper)
return wrappers
def appendItem(self, item):
raise NotImplemented
def containsFitID(self, fitID):
for wrapper in self._wrappers:
if wrapper.isFit and wrapper.item.ID == fitID:
return True
return False
def containsProfileID(self, profileID):
for wrapper in self._wrappers:
if wrapper.isProfile and wrapper.item.ID == profileID:
return True
return False
# Wrapper-related events
def OnFitRenamed(self, event):
if self.containsFitID(event.fitID):
self.updateView()
def OnFitChanged(self, event):
if set(event.fitIDs).intersection(w.item.ID for w in self._wrappers if w.isFit):
self.updateView()
def OnFitRemoved(self, event):
wrapper = next((w for w in self._wrappers if w.isFit and w.item.ID == event.fitID), None)
if wrapper is not None:
self._wrappers.remove(wrapper)
self.updateView()
def OnProfileRenamed(self, event):
if self.containsProfileID(event.profileID):
self.updateView()
def OnProfileChanged(self, event):
if self.containsProfileID(event.profileID):
self.updateView()
def OnProfileRemoved(self, event):
wrapper = next((w for w in self._wrappers if w.isProfile and w.item.ID == event.profileID), None)
if wrapper is not None:
self._wrappers.remove(wrapper)
self.updateView()
# Context menu handlers
def addFit(self, fit):
if fit is None:
return
if self.containsFitID(fit.ID):
return
self.appendItem(fit)
self.updateView()
self.graphFrame.draw()
def getExistingFitIDs(self):
return [w.item.ID for w in self._wrappers if w.isFit]
def addFitsByIDs(self, fitIDs):
sFit = Fit.getInstance()
for fitID in fitIDs:
if self.containsFitID(fitID):
continue
fit = sFit.getFit(fitID)
if fit is not None:
self.appendItem(fit)
self.updateView()
self.graphFrame.draw()
class SourceWrapperList(BaseWrapperList):
DEFAULT_COLS = (
'Graph Color',
'Base Icon',
'Base Name')
def __init__(self, graphFrame, parent):
super().__init__(graphFrame, parent)
self.Bind(wx.EVT_CONTEXT_MENU, self.spawnMenu)
fit = Fit.getInstance().getFit(self.graphFrame.mainFrame.getActiveFit())
if fit is not None:
self.appendItem(fit)
self.updateView()
def appendItem(self, item):
# Find out least used color
colorUseMap = {c: 0 for c in BASE_COLORS}
for wrapper in self._wrappers:
if wrapper.colorID not in colorUseMap:
continue
colorUseMap[wrapper.colorID] += 1
def getDefaultParams():
leastUses = min(colorUseMap.values(), default=0)
for colorID in BASE_COLORS:
if leastUses == colorUseMap.get(colorID, 0):
return colorID
return None
colorID = getDefaultParams()
self._wrappers.append(SourceWrapper(item=item, colorID=colorID))
def spawnMenu(self, event):
clickedPos = self.getRowByAbs(event.Position)
self.ensureSelection(clickedPos)
selection = self.getSelectedWrappers()
mainItem = self.getWrapper(clickedPos)
itemContext = None if mainItem is None else 'Fit'
menu = ContextMenu.getMenu(self, mainItem, selection, ('graphFitList', itemContext), ('graphFitListMisc', itemContext))
if menu:
self.PopupMenu(menu)
@property
def defaultTTText(self):
return 'Drag a fit into this list to graph it'
class TargetWrapperList(BaseWrapperList):
DEFAULT_COLS = (
'Graph Lightness',
'Graph Line Style',
'Base Icon',
'Base Name')
def __init__(self, graphFrame, parent):
super().__init__(graphFrame, parent)
self.Bind(wx.EVT_CONTEXT_MENU, self.spawnMenu)
self.appendItem(TargetProfile.getIdeal())
self.updateView()
def appendItem(self, item):
# Find out least used lightness
lightnessUseMap = {(l, s): 0 for l in LIGHTNESSES for s in STYLES}
for wrapper in self._wrappers:
key = (wrapper.lightnessID, wrapper.lineStyleID)
if key not in lightnessUseMap:
continue
lightnessUseMap[key] += 1
def getDefaultParams():
leastUses = min(lightnessUseMap.values(), default=0)
for lineStyleID in STYLES:
for lightnessID in LIGHTNESSES:
if leastUses == lightnessUseMap.get((lightnessID, lineStyleID), 0):
return lightnessID, lineStyleID
return None, None
lightnessID, lineStyleID = getDefaultParams()
self._wrappers.append(TargetWrapper(item=item, lightnessID=lightnessID, lineStyleID=lineStyleID))
def spawnMenu(self, event):
clickedPos = self.getRowByAbs(event.Position)
self.ensureSelection(clickedPos)
selection = self.getSelectedWrappers()
mainItem = self.getWrapper(clickedPos)
itemContext = None if mainItem is None else 'Target'
menu = ContextMenu.getMenu(self, mainItem, selection, ('graphTgtList', itemContext), ('graphTgtListMisc', itemContext))
if menu:
self.PopupMenu(menu)
def OnResistModeChanged(self, event):
if set(event.fitIDs).intersection(w.item.ID for w in self._wrappers if w.isFit):
self.updateView()
@property
def defaultTTText(self):
return 'Drag a fit into this list to have your fits graphed against it'
# Context menu handlers
def addProfile(self, profile):
if profile is None:
return
if self.containsProfileID(profile.ID):
return
self.appendItem(profile)
self.updateView()
self.graphFrame.draw()

106
graphs/gui/stylePickers.py Normal file
View File

@@ -0,0 +1,106 @@
# =============================================================================
# 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 graphs.style import BASE_COLORS, LIGHTNESSES, STYLES
from gui.bitmap_loader import BitmapLoader
from service.const import GraphLightness
class StylePickerPopup(wx.PopupTransientWindow):
def __init__(self, parent, wrapper):
super().__init__(parent, flags=wx.BORDER_SIMPLE)
self.wrapper = wrapper
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
sizer = wx.BoxSizer(wx.VERTICAL)
grid = wx.GridSizer(self.nrows, self.ncols, 0, 0)
self.patches = list()
for styleID in self.sortingOrder:
styleData = self.styleContainer[styleID]
icon = wx.StaticBitmap(self, wx.ID_ANY, BitmapLoader.getBitmap(styleData.iconName, 'gui'))
icon.styleID = styleID
icon.SetToolTip(styleData.name)
icon.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
grid.Add(icon, flag=wx.ALL, border=3)
sizer.Add(grid)
self.SetSizer(sizer)
self.Fit()
self.Layout()
@property
def styleContainer(self):
raise NotImplementedError
@property
def sortingOrder(self):
return self.styleContainer
@property
def ncols(self):
raise NotImplementedError
@property
def nrows(self):
raise NotImplementedError
@property
def wrapperAttr(self):
raise NotImplementedError
def OnLeftDown(self, event):
styleID = getattr(event.GetEventObject(), 'styleID', None)
if styleID is not None:
setattr(self.wrapper, self.wrapperAttr, styleID)
self.Parent.OnLineStyleChange()
self.Hide()
self.Destroy()
return
event.Skip()
class ColorPickerPopup(StylePickerPopup):
styleContainer = BASE_COLORS
wrapperAttr = 'colorID'
ncols = 4
nrows = 2
class LightnessPickerPopup(StylePickerPopup):
styleContainer = LIGHTNESSES
sortingOrder = (GraphLightness.dark, GraphLightness.normal, GraphLightness.bright)
wrapperAttr = 'lightnessID'
ncols = 3
nrows = 1
class LineStylePickerPopup(StylePickerPopup):
styleContainer = STYLES
wrapperAttr = 'lineStyleID'
ncols = 4
nrows = 1

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