Compare commits
626 Commits
v2.9.3dev1
...
v2.10.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5df2db5879 | ||
|
|
5a34db0d2f | ||
|
|
6f50be1e7e | ||
|
|
d15fefcf1b | ||
|
|
c2a240bab0 | ||
|
|
40c3bf723f | ||
|
|
7a92ace2db | ||
|
|
500f5b8310 | ||
|
|
44830a4de6 | ||
|
|
f3f13e7ba8 | ||
|
|
0269a64ae1 | ||
|
|
5d6cdcbd23 | ||
|
|
81906a7bd2 | ||
|
|
b25b038934 | ||
|
|
b469fa520e | ||
|
|
4f865896c7 | ||
|
|
3b50dddef2 | ||
|
|
380e9c2e87 | ||
|
|
1c1443c862 | ||
|
|
7dab220009 | ||
|
|
eebd59413b | ||
|
|
f4a635eb43 | ||
|
|
0e57258cc5 | ||
|
|
67462c3278 | ||
|
|
fce8129fa2 | ||
|
|
707dbeecf8 | ||
|
|
13a0bf9d42 | ||
|
|
3d3bf4ce2c | ||
|
|
46ae2a006e | ||
|
|
0062206f87 | ||
|
|
b6642aa76c | ||
|
|
522f1c8314 | ||
|
|
6fceda5f27 | ||
|
|
668a947543 | ||
|
|
f3eadc9ef1 | ||
|
|
7693483720 | ||
|
|
219e1c11dc | ||
|
|
99c6614d86 | ||
|
|
93af60c3d0 | ||
|
|
b4789bbebf | ||
|
|
160c2f0942 | ||
|
|
9605efe643 | ||
|
|
56e09b8528 | ||
|
|
7dc17543df | ||
|
|
3793721dc3 | ||
|
|
54aa284fd9 | ||
|
|
207818537b | ||
|
|
d32ff668e1 | ||
|
|
1b2cd62629 | ||
|
|
2d96af9fc8 | ||
|
|
20422d3046 | ||
|
|
0e2ae0e0f0 | ||
|
|
9572a51f28 | ||
|
|
cf6f884b3b | ||
|
|
4ca737281d | ||
|
|
847f4e343e | ||
|
|
e0db6eb2ad | ||
|
|
c7f625456e | ||
|
|
332eb048a1 | ||
|
|
e2c7f169fd | ||
|
|
4eb68c7c13 | ||
|
|
c1228b95fe | ||
|
|
2ab61f2b9e | ||
|
|
d266aa796e | ||
|
|
e488497a42 | ||
|
|
e8766817f8 | ||
|
|
d577b1d1c6 | ||
|
|
7984b57494 | ||
|
|
567e8df174 | ||
|
|
65e7607221 | ||
|
|
6e3b536d83 | ||
|
|
348c4d71df | ||
|
|
9494885f45 | ||
|
|
01bda70fef | ||
|
|
8b2f9ce59d | ||
|
|
60a8e905b8 | ||
|
|
d9a4b0a359 | ||
|
|
2d43a6ade5 | ||
|
|
d0a56e3ee8 | ||
|
|
9b15f1942d | ||
|
|
edd261c677 | ||
|
|
3c967ba9eb | ||
|
|
8332ccaa7a | ||
|
|
26b1610ca5 | ||
|
|
ae1a5f4e44 | ||
|
|
4594f57961 | ||
|
|
c1d0849f87 | ||
|
|
313264a49f | ||
|
|
a724347236 | ||
|
|
df7ad187f5 | ||
|
|
c142a011a0 | ||
|
|
477c43884a | ||
|
|
f3551ce570 | ||
|
|
6baa9dd322 | ||
|
|
302975c243 | ||
|
|
73f75fb44e | ||
|
|
7f651b144f | ||
|
|
ff98658491 | ||
|
|
53db5943b1 | ||
|
|
33b7ab0d98 | ||
|
|
f0af93f8b9 | ||
|
|
379fd2353a | ||
|
|
98f0766425 | ||
|
|
be07a4735c | ||
|
|
d736a10dc9 | ||
|
|
c5c3b9cba1 | ||
|
|
bfa9ad4d96 | ||
|
|
2f8ece9080 | ||
|
|
c7e769e42e | ||
|
|
3ab06d0832 | ||
|
|
251bc71f86 | ||
|
|
3f3870bb30 | ||
|
|
eb9612b9a3 | ||
|
|
1d7efce197 | ||
|
|
ff60cf313e | ||
|
|
a275878ba0 | ||
|
|
09fb4c1d35 | ||
|
|
483c1c35fd | ||
|
|
810b8be92a | ||
|
|
679ed7b806 | ||
|
|
e1896c0216 | ||
|
|
01310c166e | ||
|
|
3bc93899fe | ||
|
|
69bd988174 | ||
|
|
20bee1196a | ||
|
|
9803da1825 | ||
|
|
bc61e32ee7 | ||
|
|
4f784e2eea | ||
|
|
931d8d355f | ||
|
|
6912b6eb39 | ||
|
|
163d2c9b10 | ||
|
|
cc8def1cf5 | ||
|
|
bcdefdc4ac | ||
|
|
4f2e1be9ac | ||
|
|
663623dec6 | ||
|
|
cecf5d7e31 | ||
|
|
8a4caeaa2d | ||
|
|
f8062ba39f | ||
|
|
3e6e3b0743 | ||
|
|
1e35eaf62a | ||
|
|
8a3dc2f3dc | ||
|
|
34a6fdc07e | ||
|
|
fc65cb6000 | ||
|
|
247add778d | ||
|
|
ab1071f1d7 | ||
|
|
f5cb5c3993 | ||
|
|
ea7f122030 | ||
|
|
2160cc4aaa | ||
|
|
817e99a05d | ||
|
|
53f5656478 | ||
|
|
42d11bd3f1 | ||
|
|
a028ebe198 | ||
|
|
c315adf987 | ||
|
|
01371f227c | ||
|
|
3174deed99 | ||
|
|
6db178e4d2 | ||
|
|
eb07e03e93 | ||
|
|
a881cd2bcc | ||
|
|
676b09720a | ||
|
|
ac7b6d9ecd | ||
|
|
49cb81b516 | ||
|
|
27e361dc5b | ||
|
|
4c330bfb16 | ||
|
|
fcb85c85a3 | ||
|
|
9be1b96226 | ||
|
|
11598f9a09 | ||
|
|
39e23237a5 | ||
|
|
22507673aa | ||
|
|
7fdcd4aa15 | ||
|
|
c559508175 | ||
|
|
95621b6aab | ||
|
|
fb93aa1ad5 | ||
|
|
035c69c60a | ||
|
|
788bbb5d25 | ||
|
|
04178ca824 | ||
|
|
647bdb78df | ||
|
|
ce9099a25b | ||
|
|
4e715750a5 | ||
|
|
b330f72326 | ||
|
|
dd5c95d2f2 | ||
|
|
ae5e0cc71a | ||
|
|
cfe0e36e48 | ||
|
|
63a362286e | ||
|
|
2155aa0d21 | ||
|
|
f52fda3f03 | ||
|
|
f315f8b85a | ||
|
|
72e56246f4 | ||
|
|
5102cb35c8 | ||
|
|
170853f0f4 | ||
|
|
7bc0d88898 | ||
|
|
4834cfe8ca | ||
|
|
2c2065119b | ||
|
|
636672fdce | ||
|
|
bc8c70fa9c | ||
|
|
e3ac9a7722 | ||
|
|
e14d3d7214 | ||
|
|
5b898a678b | ||
|
|
e2d6baaeb1 | ||
|
|
eb87ba1d89 | ||
|
|
0257e70c29 | ||
|
|
70d1a3534b | ||
|
|
16f4903eba | ||
|
|
6b77d72f06 | ||
|
|
ac5768e666 | ||
|
|
f3bd47f347 | ||
|
|
1fbb47d64b | ||
|
|
3797887abc | ||
|
|
8870eef79b | ||
|
|
eefcd9e738 | ||
|
|
5f296bbe30 | ||
|
|
d88fa1131d | ||
|
|
3c6071ad88 | ||
|
|
deb772f0a7 | ||
|
|
858719aad8 | ||
|
|
14debfd25c | ||
|
|
510c9cafec | ||
|
|
6434902f86 | ||
|
|
16be84420b | ||
|
|
8f2283f9aa | ||
|
|
920b84886c | ||
|
|
cb2f0e40ba | ||
|
|
14a9c9910c | ||
|
|
c8d0ae8659 | ||
|
|
fd541ead6c | ||
|
|
71e7ea0230 | ||
|
|
cab2d41269 | ||
|
|
f5b1c79029 | ||
|
|
885a3f1ac9 | ||
|
|
e821b2d09c | ||
|
|
1b2bff8a77 | ||
|
|
d213e94860 | ||
|
|
d2b71d97d2 | ||
|
|
1ff7bdf1a7 | ||
|
|
f221f2df4f | ||
|
|
46f365c42d | ||
|
|
a53c00aeda | ||
|
|
044818aa65 | ||
|
|
a55084dbae | ||
|
|
c2c9528e80 | ||
|
|
9c7ad95f6e | ||
|
|
fe9dc0a3e5 | ||
|
|
25712ef778 | ||
|
|
a63b543e0c | ||
|
|
8ebec1f957 | ||
|
|
0733fee878 | ||
|
|
d52dd535a3 | ||
|
|
cbc6475875 | ||
|
|
c6de92592c | ||
|
|
5f97734881 | ||
|
|
62fbb7c9c8 | ||
|
|
4ddbdebae4 | ||
|
|
542b79fa00 | ||
|
|
9f6f5c8a76 | ||
|
|
8591f649d1 | ||
|
|
3bbd51614d | ||
|
|
cb20c8588f | ||
|
|
10b1c6ebfb | ||
|
|
3c6739b83a | ||
|
|
57426f783e | ||
|
|
0788ff050d | ||
|
|
18d59c119c | ||
|
|
ae34cd5422 | ||
|
|
50807e9381 | ||
|
|
fdb4d4d443 | ||
|
|
ff22f12a56 | ||
|
|
15dc2a325a | ||
|
|
ee9c1db000 | ||
|
|
b3b134ea45 | ||
|
|
2e9b024390 | ||
|
|
ee2193e1bb | ||
|
|
a582cf93bd | ||
|
|
fc2d7cf7b8 | ||
|
|
61836dbb83 | ||
|
|
154122388e | ||
|
|
0114417018 | ||
|
|
e662edc2cc | ||
|
|
9e6cdb2f4f | ||
|
|
98b1fdb476 | ||
|
|
0f0e544f54 | ||
|
|
6d50f03396 | ||
|
|
7ec9d3f122 | ||
|
|
592adb36f1 | ||
|
|
d571191ec2 | ||
|
|
1f5fe47580 | ||
|
|
1e3783c21d | ||
|
|
7190d91d31 | ||
|
|
5f697c166a | ||
|
|
68b5fd9893 | ||
|
|
3e1a91d073 | ||
|
|
c68451228a | ||
|
|
77ae235385 | ||
|
|
c4009bdbd7 | ||
|
|
1eb48b00e1 | ||
|
|
67cef93dd8 | ||
|
|
2aa274f56f | ||
|
|
cd20164d7a | ||
|
|
5a0ca503c1 | ||
|
|
4c1c15e69e | ||
|
|
0320a16ba4 | ||
|
|
fcc8f3c5a7 | ||
|
|
fae3e8568a | ||
|
|
4d2bb5ba87 | ||
|
|
d71cf64564 | ||
|
|
1bb30499c2 | ||
|
|
d8deb98d7b | ||
|
|
e1078ef6da | ||
|
|
a1d807bd45 | ||
|
|
4f3228388c | ||
|
|
81e3edc041 | ||
|
|
077db2ecd6 | ||
|
|
6d2746ad75 | ||
|
|
0ffdae97fd | ||
|
|
70fd1ac6de | ||
|
|
78d056c6ff | ||
|
|
43ba63233d | ||
|
|
adf750fe44 | ||
|
|
250996e8ac | ||
|
|
56639a0812 | ||
|
|
c12e450648 | ||
|
|
4fce6f7b99 | ||
|
|
27b8c12639 | ||
|
|
5d5d9ff153 | ||
|
|
d803c8374f | ||
|
|
a5b22aa112 | ||
|
|
ae8fb25d3f | ||
|
|
530dd1c03b | ||
|
|
473b65850d | ||
|
|
2c49bde5bf | ||
|
|
97b32b33d3 | ||
|
|
1382e87133 | ||
|
|
19d03591b1 | ||
|
|
6f1321aa13 | ||
|
|
365a3798c2 | ||
|
|
8d3981e1a4 | ||
|
|
e34fcb2f9c | ||
|
|
71f1c69f23 | ||
|
|
072ad028a3 | ||
|
|
a652e12fa4 | ||
|
|
58f3618350 | ||
|
|
1b26cee9c1 | ||
|
|
4752e5a20f | ||
|
|
d8e277593d | ||
|
|
edbc341909 | ||
|
|
5f20f249f7 | ||
|
|
a1de3b9225 | ||
|
|
5110e63809 | ||
|
|
26d4cfa2de | ||
|
|
ae1a9950bc | ||
|
|
1c120f2fd6 | ||
|
|
d7e45b0f76 | ||
|
|
e796b748b6 | ||
|
|
a74984d37b | ||
|
|
e342f96fbe | ||
|
|
e262aa7daa | ||
|
|
c64d09ca54 | ||
|
|
8def076175 | ||
|
|
a64fbd8976 | ||
|
|
eda869fe0d | ||
|
|
04a74e278b | ||
|
|
6786cc7eff | ||
|
|
6984bd435f | ||
|
|
ec8b771a24 | ||
|
|
6ce72e4fb3 | ||
|
|
cda9ba5978 | ||
|
|
e2ae89f6b9 | ||
|
|
3bc3705c42 | ||
|
|
522de5ca5a | ||
|
|
efd8a6964e | ||
|
|
1cd10d2109 | ||
|
|
3a09f4b45c | ||
|
|
64bc2c34c2 | ||
|
|
845630437e | ||
|
|
c4484d735a | ||
|
|
5b74c6c5e1 | ||
|
|
3e410540c9 | ||
|
|
5bba1dc88b | ||
|
|
8c0cae8bc3 | ||
|
|
71e55a000b | ||
|
|
7bcdf95f5c | ||
|
|
4402addcb0 | ||
|
|
3d57861481 | ||
|
|
5d1d2b87df | ||
|
|
389b5d57aa | ||
|
|
53de46bab7 | ||
|
|
e6dce726b7 | ||
|
|
63ca8dc559 | ||
|
|
6e083a5af8 | ||
|
|
ac93c5487c | ||
|
|
1f94b28b87 | ||
|
|
78b6eb4283 | ||
|
|
417e478d27 | ||
|
|
78d2dff0d8 | ||
|
|
63c45c5060 | ||
|
|
c4f225003a | ||
|
|
185cf4f625 | ||
|
|
d2b838e9d5 | ||
|
|
15b6a848e8 | ||
|
|
193fcc60d8 | ||
|
|
ae110371fe | ||
|
|
5857413285 | ||
|
|
4448d7e62f | ||
|
|
d3ca0a961e | ||
|
|
405492d9d7 | ||
|
|
d27d7656d5 | ||
|
|
120bd9aa0c | ||
|
|
6ab79ab5c0 | ||
|
|
b8d189c0ad | ||
|
|
86e04321c8 | ||
|
|
6bcc906c4a | ||
|
|
c3becec822 | ||
|
|
aae2e7c531 | ||
|
|
52490144d3 | ||
|
|
c04c672f11 | ||
|
|
f51979b69a | ||
|
|
ab6b9759b0 | ||
|
|
b3027532ff | ||
|
|
494c9b08cb | ||
|
|
c595195519 | ||
|
|
c3efa819f4 | ||
|
|
4e7580b277 | ||
|
|
af642a4259 | ||
|
|
c365efb67e | ||
|
|
fc7613451e | ||
|
|
62b7b44120 | ||
|
|
744fce2e82 | ||
|
|
dd55493b4e | ||
|
|
7e7b49d2e4 | ||
|
|
24494e9b29 | ||
|
|
eff0510092 | ||
|
|
988688939b | ||
|
|
d448116e91 | ||
|
|
75ce6ffbcf | ||
|
|
60933a309f | ||
|
|
428cb5c888 | ||
|
|
d195ec7e68 | ||
|
|
c2017f3cb9 | ||
|
|
66ff4d827c | ||
|
|
745914bf9e | ||
|
|
421146eb54 | ||
|
|
ef81f9c830 | ||
|
|
1e760b2111 | ||
|
|
fe50372b12 | ||
|
|
7ef79eaa79 | ||
|
|
9b282587b2 | ||
|
|
4af36514bc | ||
|
|
5320e99276 | ||
|
|
b733205541 | ||
|
|
b125c62930 | ||
|
|
7895e4076d | ||
|
|
9ec192de7d | ||
|
|
8e41a31d1d | ||
|
|
fa4a2436aa | ||
|
|
22ca78cb68 | ||
|
|
ee4a1f936b | ||
|
|
15a8c5750a | ||
|
|
9f261f5b80 | ||
|
|
b6a58b4ba6 | ||
|
|
09ca85ca81 | ||
|
|
3aa69a6eaf | ||
|
|
2a645b1b04 | ||
|
|
0420f399ad | ||
|
|
509a45dcee | ||
|
|
52724d790b | ||
|
|
4b960af9ab | ||
|
|
022f0c06ee | ||
|
|
5ffd644ad9 | ||
|
|
03183827a6 | ||
|
|
5e7fcc32b6 | ||
|
|
3c0d87940b | ||
|
|
4cf07c4b76 | ||
|
|
28db388fa0 | ||
|
|
2c1905f041 | ||
|
|
30d03f0ab5 | ||
|
|
4ca3f10bc9 | ||
|
|
9cc228cfff | ||
|
|
3359d8cb88 | ||
|
|
738d7f687d | ||
|
|
b224196b05 | ||
|
|
bbcc32c8cf | ||
|
|
3c0b8643f6 | ||
|
|
c85b6e4a36 | ||
|
|
6003302e10 | ||
|
|
e7dd045979 | ||
|
|
6ca7a22c3e | ||
|
|
e8f09514ab | ||
|
|
1a3a656879 | ||
|
|
e77ada4e8c | ||
|
|
41b72c2789 | ||
|
|
a4be7c5e9a | ||
|
|
fb3c183b3e | ||
|
|
3e7dbef659 | ||
|
|
89260d1d36 | ||
|
|
d451bda7ed | ||
|
|
933c84466f | ||
|
|
d4c9100f77 | ||
|
|
c6aa72a3e3 | ||
|
|
7cf6ff04b6 | ||
|
|
5b575fdfe3 | ||
|
|
a8a5fabce7 | ||
|
|
f41d6dd2c1 | ||
|
|
09727c102a | ||
|
|
6580734dc7 | ||
|
|
ff34865067 | ||
|
|
bdd400fd51 | ||
|
|
1e8184a80b | ||
|
|
6a20f04c7f | ||
|
|
d81acc1f9c | ||
|
|
bc84c20cb2 | ||
|
|
d5c5e2698e | ||
|
|
16a78e689e | ||
|
|
066f29660d | ||
|
|
bba0df5f50 | ||
|
|
45452ca680 | ||
|
|
4fbbc18f9f | ||
|
|
22ec280ec2 | ||
|
|
89c06b5201 | ||
|
|
86d5f72988 | ||
|
|
c3e055a4c9 | ||
|
|
e48631956d | ||
|
|
2964f3b009 | ||
|
|
527c66dca4 | ||
|
|
24909f0523 | ||
|
|
ed7494b3a4 | ||
|
|
8fae275e5a | ||
|
|
a09a2a5f4b | ||
|
|
2adc150811 | ||
|
|
f5cad33b6c | ||
|
|
338cf45f65 | ||
|
|
359c60bafb | ||
|
|
f808b73a5d | ||
|
|
8dd87cde58 | ||
|
|
1ec78d9beb | ||
|
|
9c710285f2 | ||
|
|
90f745a18f | ||
|
|
af446579ab | ||
|
|
bcc11bd172 | ||
|
|
0c31f756a8 | ||
|
|
16fdd5a5e6 | ||
|
|
ec1a2c66ee | ||
|
|
c3f41d68e6 | ||
|
|
2f8701b4b2 | ||
|
|
9b2d5410d6 | ||
|
|
1d8c9d2c40 | ||
|
|
8c0817245f | ||
|
|
2a04e60ae0 | ||
|
|
fb5eb220fd | ||
|
|
512f48ebdd | ||
|
|
09db7d26a7 | ||
|
|
690cf5eca1 | ||
|
|
1aee4c59c4 | ||
|
|
f1384074b5 | ||
|
|
203bed06d6 | ||
|
|
0b00e28863 | ||
|
|
d74d331642 | ||
|
|
f075fbdc63 | ||
|
|
d59b6696ca | ||
|
|
eedf6f9a39 | ||
|
|
1cca5729fc | ||
|
|
a7b01ece22 | ||
|
|
d17e6d08d8 | ||
|
|
b29aaa9e20 | ||
|
|
71ae59b2b5 | ||
|
|
51294f6cbc | ||
|
|
0439ace886 | ||
|
|
c85c735f9a | ||
|
|
c65b582497 | ||
|
|
7f2ac83e17 | ||
|
|
5ef2a40d1e | ||
|
|
5b52da737a | ||
|
|
51e8713cd6 | ||
|
|
c9b60f2c65 | ||
|
|
d777999af4 | ||
|
|
74444d56c4 | ||
|
|
a433c9638a | ||
|
|
672141cffc | ||
|
|
ac132cbb92 | ||
|
|
d9535b08b1 | ||
|
|
d93544b3bc | ||
|
|
2320c3cb57 | ||
|
|
bd5710c676 | ||
|
|
49f1412d91 | ||
|
|
54eea7d702 | ||
|
|
e26bcb2e5e | ||
|
|
7305c0a017 | ||
|
|
7d37b9e0e0 | ||
|
|
87f28db730 | ||
|
|
56d9a8b626 | ||
|
|
cb8f76c582 | ||
|
|
af0b7b92c7 | ||
|
|
9418b7a709 | ||
|
|
47c34f2186 | ||
|
|
2ca418c287 | ||
|
|
775e69305c | ||
|
|
0f1cbb4234 | ||
|
|
306710a314 | ||
|
|
776a4ee977 | ||
|
|
9dccfd756a | ||
|
|
15281ee6ce | ||
|
|
9a0dd6c521 | ||
|
|
a570f291ae | ||
|
|
cde7fdcaba | ||
|
|
e4780bc8ba | ||
|
|
4d35e5aee1 | ||
|
|
f7b705b9e2 | ||
|
|
48f44cdb0c | ||
|
|
013a2264c0 | ||
|
|
8222686dda | ||
|
|
7f2121e98d | ||
|
|
4b6c881dca | ||
|
|
5f9bf4a861 | ||
|
|
154db5df0b | ||
|
|
321b939d3a | ||
|
|
95a1d669f5 | ||
|
|
9e3c9bd056 | ||
|
|
bb9b3780ae | ||
|
|
4c976d9f35 | ||
|
|
52a1314803 | ||
|
|
a5475eb244 | ||
|
|
ba0a5db72f | ||
|
|
2bac4a954f | ||
|
|
e9f3453b04 | ||
|
|
c950592b5b | ||
|
|
1cd42669a0 | ||
|
|
2b24f14122 | ||
|
|
4932b685e1 | ||
|
|
cfffa1d99d |
@@ -1,5 +1,7 @@
|
||||
os: linux
|
||||
language: python
|
||||
git:
|
||||
depth: 400
|
||||
python:
|
||||
- 3.6
|
||||
matrix:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:":
|
||||
|
||||
@@ -62,4 +62,4 @@ def HeronFit(DB, Gamedata, Saveddata):
|
||||
for _ in range(4):
|
||||
fit.modules.append(mod)
|
||||
|
||||
return fit
|
||||
return fit
|
||||
|
||||
@@ -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
|
||||
|
||||
198
eos/capSim.py
198
eos/capSim.py
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 *
|
||||
|
||||
|
||||
16
eos/db/migrations/upgrade32.py
Normal file
16
eos/db/migrations/upgrade32.py
Normal 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))
|
||||
30
eos/db/migrations/upgrade33.py
Normal file
30
eos/db/migrations/upgrade33.py
Normal 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')
|
||||
@@ -11,7 +11,7 @@ __all__ = [
|
||||
"implant",
|
||||
"damagePattern",
|
||||
"miscData",
|
||||
"targetResists",
|
||||
"targetProfile",
|
||||
"override",
|
||||
"implantSet",
|
||||
"loadDefaultDatabaseValues"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -23,7 +23,7 @@ from logbook import Logger
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class DatabaseCleanup(object):
|
||||
class DatabaseCleanup:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
46
eos/db/saveddata/targetProfile.py
Normal file
46
eos/db/saveddata/targetProfile.py
Normal 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})
|
||||
@@ -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)
|
||||
@@ -17,10 +17,8 @@
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
from logbook import Logger
|
||||
|
||||
from eos.exception import HandledListActionError
|
||||
from utils.deprecated import deprecated
|
||||
from logbook import Logger
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
@@ -137,27 +135,23 @@ class HandledModuleList(HandledList):
|
||||
self.__toModule(emptyPosition, mod)
|
||||
if mod.isInvalid:
|
||||
self.__toDummy(mod.position)
|
||||
raise HandledListActionError(mod)
|
||||
return
|
||||
|
||||
self.appendIgnoreEmpty(mod)
|
||||
else:
|
||||
self.appendIgnoreEmpty(mod)
|
||||
|
||||
def appendIgnoreEmpty(self, mod):
|
||||
mod.position = len(self)
|
||||
HandledList.append(self, mod)
|
||||
if mod.isInvalid:
|
||||
self.remove(mod)
|
||||
raise HandledListActionError(mod)
|
||||
|
||||
def replace(self, idx, mod):
|
||||
try:
|
||||
oldMod = self[idx]
|
||||
except IndexError:
|
||||
raise HandledListActionError(mod)
|
||||
return
|
||||
self.__toModule(idx, mod)
|
||||
if mod.isInvalid:
|
||||
self.__toModule(idx, oldMod)
|
||||
raise HandledListActionError(mod)
|
||||
|
||||
def replaceRackPosition(self, rackPosition, mod):
|
||||
listPositions = []
|
||||
@@ -182,7 +176,6 @@ class HandledModuleList(HandledList):
|
||||
self.__toDummy(modListPosition)
|
||||
else:
|
||||
self.__toModule(modListPosition, oldMod)
|
||||
raise HandledListActionError(mod)
|
||||
|
||||
def insert(self, idx, mod):
|
||||
mod.position = idx
|
||||
@@ -193,8 +186,6 @@ class HandledModuleList(HandledList):
|
||||
HandledList.insert(self, idx, mod)
|
||||
if mod.isInvalid:
|
||||
self.remove(mod)
|
||||
raise HandledListActionError(mod)
|
||||
|
||||
|
||||
def remove(self, mod):
|
||||
HandledList.remove(self, mod)
|
||||
@@ -236,13 +227,11 @@ class HandledDroneCargoList(HandledList):
|
||||
HandledList.append(self, thing)
|
||||
if thing.isInvalid:
|
||||
self.remove(thing)
|
||||
raise HandledListActionError(thing)
|
||||
|
||||
def insert(self, idx, thing):
|
||||
HandledList.insert(self, idx, thing)
|
||||
if thing.isInvalid:
|
||||
self.remove(thing)
|
||||
raise HandledListActionError(thing)
|
||||
|
||||
|
||||
class HandledImplantList(HandledList):
|
||||
@@ -251,22 +240,22 @@ class HandledImplantList(HandledList):
|
||||
if implant.isInvalid:
|
||||
HandledList.append(self, implant)
|
||||
self.remove(implant)
|
||||
raise HandledListActionError(implant)
|
||||
return
|
||||
if self.__slotCheck(implant):
|
||||
HandledList.append(self, implant)
|
||||
self.remove(implant)
|
||||
raise HandledListActionError(implant)
|
||||
return
|
||||
HandledList.append(self, implant)
|
||||
|
||||
def insert(self, idx, implant):
|
||||
if implant.isInvalid:
|
||||
HandledList.insert(self, idx, implant)
|
||||
self.remove(implant)
|
||||
raise HandledListActionError(implant)
|
||||
return
|
||||
if self.__slotCheck(implant):
|
||||
HandledList.insert(self, idx, implant)
|
||||
self.remove(implant)
|
||||
raise HandledListActionError(implant)
|
||||
return
|
||||
HandledList.insert(self, idx, implant)
|
||||
|
||||
def makeRoom(self, implant):
|
||||
@@ -292,22 +281,22 @@ class HandledBoosterList(HandledList):
|
||||
if booster.isInvalid:
|
||||
HandledList.append(self, booster)
|
||||
self.remove(booster)
|
||||
raise HandledListActionError(booster)
|
||||
return
|
||||
if self.__slotCheck(booster):
|
||||
HandledList.append(self, booster)
|
||||
self.remove(booster)
|
||||
raise HandledListActionError(booster)
|
||||
return
|
||||
HandledList.append(self, booster)
|
||||
|
||||
def insert(self, idx, booster):
|
||||
if booster.isInvalid:
|
||||
HandledList.insert(self, idx, booster)
|
||||
self.remove(booster)
|
||||
raise HandledListActionError(booster)
|
||||
return
|
||||
if self.__slotCheck(booster):
|
||||
HandledList.insert(self, idx, booster)
|
||||
self.remove(booster)
|
||||
raise HandledListActionError(booster)
|
||||
return
|
||||
HandledList.insert(self, idx, booster)
|
||||
|
||||
def makeRoom(self, booster):
|
||||
@@ -346,16 +335,12 @@ class HandledProjectedModList(HandledList):
|
||||
# rows and relationships in database are removed as well
|
||||
HandledList.append(self, proj)
|
||||
self.remove(proj)
|
||||
raise HandledListActionError(proj)
|
||||
|
||||
return
|
||||
proj.projected = True
|
||||
|
||||
HandledList.append(self, proj)
|
||||
|
||||
# Remove non-projectable modules
|
||||
if not proj.item.isType("projected") and not proj.isExclusiveSystemEffect:
|
||||
self.remove(proj)
|
||||
raise HandledListActionError(proj)
|
||||
|
||||
def insert(self, idx, proj):
|
||||
if proj.isInvalid:
|
||||
@@ -363,16 +348,12 @@ class HandledProjectedModList(HandledList):
|
||||
# rows and relationships in database are removed as well
|
||||
HandledList.insert(self, idx, proj)
|
||||
self.remove(proj)
|
||||
raise HandledListActionError(proj)
|
||||
|
||||
return
|
||||
proj.projected = True
|
||||
|
||||
HandledList.insert(self, idx, proj)
|
||||
|
||||
# Remove non-projectable modules
|
||||
if not proj.item.isType("projected") and not proj.isExclusiveSystemEffect:
|
||||
self.remove(proj)
|
||||
raise HandledListActionError(proj)
|
||||
|
||||
@property
|
||||
def currentSystemEffect(self):
|
||||
@@ -399,27 +380,21 @@ class HandledProjectedDroneList(HandledDroneCargoList):
|
||||
def append(self, proj):
|
||||
proj.projected = True
|
||||
HandledList.append(self, proj)
|
||||
|
||||
# Remove invalid or non-projectable drones
|
||||
if proj.isInvalid or not proj.item.isType("projected"):
|
||||
self.remove(proj)
|
||||
proj.projected = False
|
||||
raise HandledListActionError(proj)
|
||||
return True
|
||||
|
||||
def insert(self, idx, proj):
|
||||
proj.projected = True
|
||||
HandledList.insert(self, idx, proj)
|
||||
|
||||
# Remove invalid or non-projectable drones
|
||||
if proj.isInvalid or not proj.item.isType("projected"):
|
||||
self.remove(proj)
|
||||
proj.projected = False
|
||||
raise HandledListActionError(proj)
|
||||
return True
|
||||
|
||||
|
||||
class HandledItem(object):
|
||||
class HandledItem:
|
||||
def preAssignItemAttr(self, *args, **kwargs):
|
||||
self.itemModifiedAttributes.preAssign(*args, **kwargs)
|
||||
|
||||
@@ -436,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)
|
||||
|
||||
|
||||
9683
eos/effects.py
9683
eos/effects.py
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
class EqBase(object):
|
||||
class EqBase:
|
||||
ID = None
|
||||
|
||||
def __eq__(self, other):
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
class HandledListActionError(Exception):
|
||||
...
|
||||
@@ -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):
|
||||
|
||||
@@ -1,120 +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(object):
|
||||
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(object):
|
||||
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(object):
|
||||
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(object):
|
||||
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) / 50.0
|
||||
i = 1
|
||||
while current < end:
|
||||
current = start + i * step
|
||||
i += 1
|
||||
yield current
|
||||
|
||||
@staticmethod
|
||||
def isConstant():
|
||||
return False
|
||||
@@ -1,197 +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 log, sin, radians, exp
|
||||
|
||||
from eos.graph import Graph
|
||||
from eos.const import FittingModuleState, FittingHardpoint
|
||||
from logbook import Logger
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class FitDpsGraph(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)
|
||||
|
||||
@staticmethod
|
||||
def calculateTurretChanceToHit(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"]
|
||||
trackingEq = (((transversal / (distance * 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
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -32,7 +32,7 @@ from eos.effectHandlerHelpers import HandledItem, HandledImplantList
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class Character(object):
|
||||
class Character:
|
||||
__itemList = None
|
||||
__itemIDMap = None
|
||||
__itemNameMap = None
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
import math
|
||||
from logbook import Logger
|
||||
|
||||
from sqlalchemy.orm import validates, reconstructor
|
||||
from sqlalchemy.orm import reconstructor, validates
|
||||
|
||||
import eos.db
|
||||
from eos.effectHandlerHelpers import HandledItem, HandledCharge
|
||||
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut
|
||||
from eos.utils.stats import DmgTypes
|
||||
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, RRTypes
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
@@ -67,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
|
||||
@@ -104,7 +105,16 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
|
||||
@property
|
||||
def cycleTime(self):
|
||||
return max(self.getModifiedItemAttr("duration", 0), 0)
|
||||
if self.hasAmmo:
|
||||
cycleTime = self.getModifiedItemAttr("missileLaunchDuration", 0)
|
||||
else:
|
||||
for attr in ("speed", "duration"):
|
||||
cycleTime = self.getModifiedItemAttr(attr, None)
|
||||
if cycleTime is not None:
|
||||
break
|
||||
if cycleTime is None:
|
||||
return 0
|
||||
return max(cycleTime, 0)
|
||||
|
||||
@property
|
||||
def dealsDamage(self):
|
||||
@@ -121,9 +131,16 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
def hasAmmo(self):
|
||||
return self.charge is not None
|
||||
|
||||
def getVolley(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 DmgTypes(0, 0, 0, 0)
|
||||
return {0: DmgTypes(0, 0, 0, 0)}
|
||||
if self.__baseVolley is None:
|
||||
dmgGetter = self.getModifiedChargeAttr if self.hasAmmo else self.getModifiedItemAttr
|
||||
dmgMult = self.amountActive * (self.getModifiedItemAttr("damageMultiplier", 1))
|
||||
@@ -133,19 +150,23 @@ 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)))
|
||||
return volley
|
||||
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 getDps(self, targetResists=None):
|
||||
volley = self.getVolley(targetResists=targetResists)
|
||||
def getVolley(self, targetProfile=None):
|
||||
return self.getVolleyParameters(targetProfile=targetProfile)[0]
|
||||
|
||||
def getDps(self, targetProfile=None):
|
||||
volley = self.getVolley(targetProfile=targetProfile)
|
||||
if not volley:
|
||||
return DmgTypes(0, 0, 0, 0)
|
||||
cycleAttr = "missileLaunchDuration" if self.hasAmmo else "speed"
|
||||
cycleTime = self.getModifiedItemAttr(cycleAttr)
|
||||
dpsFactor = 1 / (cycleTime / 1000)
|
||||
cycleParams = self.getCycleParameters()
|
||||
if cycleParams is None:
|
||||
return DmgTypes(0, 0, 0, 0)
|
||||
dpsFactor = 1 / (cycleParams.averageTime / 1000)
|
||||
dps = DmgTypes(
|
||||
em=volley.em * dpsFactor,
|
||||
thermal=volley.thermal * dpsFactor,
|
||||
@@ -153,41 +174,65 @@ 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):
|
||||
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
|
||||
rrAmount *= droneAmount / (self.cycleTime / 1000)
|
||||
self.__baseRemoteReps = (rrType, rrAmount)
|
||||
return self.__baseRemoteReps
|
||||
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, False)
|
||||
|
||||
@property
|
||||
def miningStats(self):
|
||||
if self.__miningyield is None:
|
||||
if self.mines is True and self.amountActive > 0:
|
||||
attr = "duration"
|
||||
getter = self.getModifiedItemAttr
|
||||
|
||||
cycleTime = self.getModifiedItemAttr(attr)
|
||||
volley = sum([getter(d) for d in self.MINING_ATTRIBUTES]) * self.amountActive
|
||||
self.__miningyield = volley / (cycleTime / 1000.0)
|
||||
cycleParams = self.getCycleParameters()
|
||||
if cycleParams is None:
|
||||
self.__miningyield = 0
|
||||
else:
|
||||
cycleTime = cycleParams.averageTime
|
||||
volley = sum([getter(d) for d in self.MINING_ATTRIBUTES]) * self.amountActive
|
||||
self.__miningyield = volley / (cycleTime / 1000.0)
|
||||
else:
|
||||
self.__miningyield = 0
|
||||
|
||||
@@ -236,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()
|
||||
@@ -274,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:
|
||||
|
||||
@@ -17,16 +17,19 @@
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
import math
|
||||
from logbook import Logger
|
||||
|
||||
from sqlalchemy.orm import validates, reconstructor
|
||||
from sqlalchemy.orm import reconstructor, validates
|
||||
|
||||
import eos.db
|
||||
from eos.effectHandlerHelpers import HandledItem, HandledCharge
|
||||
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut
|
||||
from eos.saveddata.fighterAbility import FighterAbility
|
||||
from eos.utils.stats import DmgTypes
|
||||
from eos.const import FittingSlot
|
||||
from eos.effectHandlerHelpers import HandledCharge, HandledItem
|
||||
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
|
||||
from eos.saveddata.fighterAbility import FighterAbility
|
||||
from eos.utils.cycles import CycleInfo, CycleSequence
|
||||
from eos.utils.stats import DmgTypes
|
||||
from eos.utils.float import floatUnerr
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
@@ -48,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()
|
||||
|
||||
@@ -133,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):
|
||||
@@ -172,79 +178,143 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
def hasAmmo(self):
|
||||
return self.charge is not None
|
||||
|
||||
def getVolley(self, targetResists=None):
|
||||
if not self.active or self.amountActive <= 0:
|
||||
return DmgTypes(0, 0, 0, 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:
|
||||
em = 0
|
||||
therm = 0
|
||||
kin = 0
|
||||
exp = 0
|
||||
self.__baseVolley = {}
|
||||
for ability in self.abilities:
|
||||
# Not passing resists here as we want to calculate and store base volley
|
||||
abilityVolley = ability.getVolley()
|
||||
em += abilityVolley.em
|
||||
therm += abilityVolley.thermal
|
||||
kin += abilityVolley.kinetic
|
||||
exp += abilityVolley.explosive
|
||||
self.__baseVolley = DmgTypes(em, therm, kin, exp)
|
||||
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)))
|
||||
return volley
|
||||
self.__baseVolley[ability.effectID] = {0: ability.getVolley()}
|
||||
adjustedVolley = {}
|
||||
for effectID, effectData in self.__baseVolley.items():
|
||||
adjustedVolley[effectID] = {}
|
||||
for volleyTime, volleyValue in effectData.items():
|
||||
adjustedVolley[effectID][volleyTime] = DmgTypes(
|
||||
em=volleyValue.em * (1 - getattr(targetProfile, "emAmount", 0)),
|
||||
thermal=volleyValue.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)),
|
||||
kinetic=volleyValue.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)),
|
||||
explosive=volleyValue.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)))
|
||||
return adjustedVolley
|
||||
|
||||
def getDps(self, targetResists=None):
|
||||
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
|
||||
exp = 0
|
||||
for volleyData in volleyParams.values():
|
||||
em += volleyData[0].em
|
||||
therm += volleyData[0].thermal
|
||||
kin += volleyData[0].kinetic
|
||||
exp += volleyData[0].explosive
|
||||
return DmgTypes(em, therm, kin, exp)
|
||||
|
||||
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 getUptime(self):
|
||||
if not self.owner.factorReload:
|
||||
return 1
|
||||
activeTimes = []
|
||||
reloadTimes = []
|
||||
for ability in self.abilities:
|
||||
if ability.numShots > 0:
|
||||
activeTimes.append(ability.numShots * ability.cycleTime)
|
||||
reloadTimes.append(ability.reloadTime)
|
||||
if not activeTimes:
|
||||
return 1
|
||||
shortestActive = sorted(activeTimes)[0]
|
||||
longestReload = sorted(reloadTimes, reverse=True)[0]
|
||||
uptime = shortestActive / (shortestActive + longestReload)
|
||||
return uptime
|
||||
|
||||
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 {}
|
||||
uptime = self.getUptime()
|
||||
if uptime == 1:
|
||||
return {a.effectID: a.getDps(targetResists=targetResists) for a in self.abilities}
|
||||
# Decide if it's better to keep steady dps up and never reload or reload from time to time
|
||||
dpsMapSteady = {}
|
||||
dpsMapPeakAdjusted = {}
|
||||
cycleParams = self.getCycleParametersPerEffectOptimizedDps(targetProfile=targetProfile)
|
||||
dpsMap = {}
|
||||
for ability in self.abilities:
|
||||
abilityDps = ability.getDps(targetResists=targetResists)
|
||||
dpsMapPeakAdjusted[ability.effectID] = DmgTypes(
|
||||
em=abilityDps.em * uptime,
|
||||
thermal=abilityDps.thermal * uptime,
|
||||
kinetic=abilityDps.kinetic * uptime,
|
||||
explosive=abilityDps.explosive * uptime)
|
||||
# Infinite use - add to steady dps
|
||||
if ability.numShots == 0:
|
||||
dpsMapSteady[ability.effectID] = abilityDps
|
||||
totalSteady = sum(i.total for i in dpsMapSteady.values())
|
||||
totalPeakAdjusted = sum(i.total for i in dpsMapPeakAdjusted.values())
|
||||
return dpsMapSteady if totalSteady >= totalPeakAdjusted else dpsMapPeakAdjusted
|
||||
if ability.effectID in cycleParams:
|
||||
cycleTime = cycleParams[ability.effectID].averageTime
|
||||
dpsMap[ability.effectID] = ability.getDps(targetProfile=targetProfile, cycleTimeOverride=cycleTime)
|
||||
return dpsMap
|
||||
|
||||
def getCycleParametersPerEffectOptimizedDps(self, targetProfile=None, reloadOverride=None):
|
||||
cycleParamsInfinite = self.getCycleParametersPerEffectInfinite()
|
||||
cycleParamsReload = self.getCycleParametersPerEffect(reloadOverride=reloadOverride)
|
||||
dpsMapOnlyInfinite = {}
|
||||
dpsMapAllWithReloads = {}
|
||||
# Decide if it's better to keep steady dps up and never reload or reload from time to time
|
||||
for ability in self.abilities:
|
||||
if ability.effectID in cycleParamsInfinite:
|
||||
cycleTime = cycleParamsInfinite[ability.effectID].averageTime
|
||||
dpsMapOnlyInfinite[ability.effectID] = ability.getDps(targetProfile=targetProfile, cycleTimeOverride=cycleTime)
|
||||
if ability.effectID in cycleParamsReload:
|
||||
cycleTime = cycleParamsReload[ability.effectID].averageTime
|
||||
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, 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, 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, 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 {}
|
||||
mostLimitedAbility = min(limitedAbilities, key=lambda a: a.cycleTime * a.numShots)
|
||||
durationToRefuel = mostLimitedAbility.cycleTime * mostLimitedAbility.numShots
|
||||
# find out how many shots various abilities will do until reload, and how much time
|
||||
# "extra" cycle will last (None for no extra cycle)
|
||||
cyclesUntilRefuel = {mostLimitedAbility.effectID: (mostLimitedAbility.numShots, None)}
|
||||
for ability in (a for a in validAbilities if a is not mostLimitedAbility):
|
||||
fullCycles = int(floatUnerr(durationToRefuel / ability.cycleTime))
|
||||
extraShotTime = floatUnerr(durationToRefuel - (fullCycles * ability.cycleTime))
|
||||
if extraShotTime == 0:
|
||||
extraShotTime = None
|
||||
cyclesUntilRefuel[ability.effectID] = (fullCycles, extraShotTime)
|
||||
refuelTimes = {}
|
||||
for ability in validAbilities:
|
||||
spentShots, extraShotTime = cyclesUntilRefuel[ability.effectID]
|
||||
if extraShotTime is not None:
|
||||
spentShots += 1
|
||||
refuelTimes[ability.effectID] = ability.getReloadTime(spentShots)
|
||||
refuelTime = max(refuelTimes.values())
|
||||
cycleParams = {}
|
||||
for ability in validAbilities:
|
||||
regularShots, extraShotTime = cyclesUntilRefuel[ability.effectID]
|
||||
sequence = []
|
||||
if extraShotTime is not None:
|
||||
if regularShots > 0:
|
||||
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, False))
|
||||
sequence.append(CycleInfo(ability.cycleTime, refuelTime, 1, True))
|
||||
cycleParams[ability.effectID] = CycleSequence(sequence, math.inf)
|
||||
return cycleParams
|
||||
|
||||
@property
|
||||
def maxRange(self):
|
||||
@@ -272,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),
|
||||
@@ -280,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:
|
||||
@@ -330,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))
|
||||
@@ -347,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:
|
||||
|
||||
@@ -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
|
||||
@@ -95,8 +95,15 @@ class FighterAbility(object):
|
||||
|
||||
@property
|
||||
def reloadTime(self):
|
||||
return self.getReloadTime()
|
||||
|
||||
def getReloadTime(self, spentShots=None):
|
||||
if spentShots is not None:
|
||||
spentShots = max(self.numShots, spentShots)
|
||||
else:
|
||||
spentShots = self.numShots
|
||||
rearm_time = (self.REARM_TIME_MAPPING[self.fighter.getModifiedItemAttr("fighterSquadronRole")] or 0 if self.hasCharges else 0)
|
||||
return self.fighter.getModifiedItemAttr("fighterRefuelingTime") + rearm_time * self.numShots
|
||||
return self.fighter.getModifiedItemAttr("fighterRefuelingTime") + rearm_time * spentShots
|
||||
|
||||
@property
|
||||
def numShots(self):
|
||||
@@ -107,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":
|
||||
@@ -120,19 +127,20 @@ 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):
|
||||
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)
|
||||
dpsFactor = 1 / (self.cycleTime / 1000)
|
||||
cycleTime = cycleTimeOverride if cycleTimeOverride is not None else self.cycleTime
|
||||
dpsFactor = 1 / (cycleTime / 1000)
|
||||
dps = DmgTypes(
|
||||
em=volley.em * dpsFactor,
|
||||
thermal=volley.thermal * dpsFactor,
|
||||
@@ -141,5 +149,4 @@ class FighterAbility(object):
|
||||
return dps
|
||||
|
||||
def clear(self):
|
||||
self.__dps = None
|
||||
self.__volley = None
|
||||
pass
|
||||
|
||||
@@ -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,13 +1630,14 @@ 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
|
||||
|
||||
for i in self.modules:
|
||||
fitCopy.modules.appendIgnoreEmpty(deepcopy(i))
|
||||
toCopy = (
|
||||
"modules",
|
||||
"drones",
|
||||
"fighters",
|
||||
"cargo",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -17,20 +17,21 @@
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
from math import floor
|
||||
|
||||
from logbook import Logger
|
||||
import math
|
||||
from sqlalchemy.orm import reconstructor, validates
|
||||
|
||||
import eos.db
|
||||
from eos.const import FittingModuleState, FittingHardpoint, FittingSlot
|
||||
from eos.const import FittingHardpoint, FittingModuleState, FittingSlot
|
||||
from eos.effectHandlerHelpers import HandledCharge, HandledItem
|
||||
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
|
||||
from eos.saveddata.citadel import Citadel
|
||||
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__)
|
||||
|
||||
@@ -133,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
|
||||
@@ -287,7 +288,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
# numcycles = math.floor(module_capacity / (module_volume * module_chargerate))
|
||||
chargeRate = self.getModifiedItemAttr("chargeRate")
|
||||
numCharges = self.numCharges
|
||||
numShots = floor(numCharges / chargeRate)
|
||||
numShots = math.floor(numCharges / chargeRate)
|
||||
else:
|
||||
numShots = None
|
||||
return numShots
|
||||
@@ -300,7 +301,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
chance = self.getModifiedChargeAttr("crystalVolatilityChance")
|
||||
damage = self.getModifiedChargeAttr("crystalVolatilityDamage")
|
||||
crystals = self.numCharges
|
||||
numShots = floor((crystals * hp) / (damage * chance))
|
||||
numShots = math.floor((crystals * hp) / (damage * chance))
|
||||
else:
|
||||
# Set 0 (infinite) for permanent crystals like t1 laser crystals
|
||||
numShots = 0
|
||||
@@ -400,8 +401,12 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
volley = self.getModifiedItemAttr("specialtyMiningAmount") or self.getModifiedItemAttr(
|
||||
"miningAmount") or 0
|
||||
if volley:
|
||||
cycleTime = self.cycleTime
|
||||
self.__miningyield = volley / (cycleTime / 1000.0)
|
||||
cycleParams = self.getCycleParameters()
|
||||
if cycleParams is None:
|
||||
self.__miningyield = 0
|
||||
else:
|
||||
cycleTime = cycleParams.averageTime
|
||||
self.__miningyield = volley / (cycleTime / 1000.0)
|
||||
else:
|
||||
self.__miningyield = 0
|
||||
else:
|
||||
@@ -409,91 +414,149 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
|
||||
return self.__miningyield
|
||||
|
||||
def getVolley(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 DmgTypes(0, 0, 0, 0)
|
||||
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)
|
||||
self.__baseVolley = DmgTypes(
|
||||
em=(dmgGetter("emDamage", 0)) * dmgMult,
|
||||
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
|
||||
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
|
||||
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
|
||||
# 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)
|
||||
# Reaper DD can damage each target only once
|
||||
if dmgDuration != 0 and dmgSubcycle != 0 and 'doomsdaySlash' not in self.item.effects:
|
||||
subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle))
|
||||
else:
|
||||
subcycles = 1
|
||||
for i in range(subcycles):
|
||||
self.__baseVolley[dmgDelay + dmgSubcycle * i] = DmgTypes(
|
||||
em=(dmgGetter("emDamage", 0)) * dmgMult,
|
||||
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
|
||||
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
|
||||
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
|
||||
spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self)
|
||||
spoolBoost = calculateSpoolup(
|
||||
self.getModifiedItemAttr("damageMultiplierBonusMax", 0),
|
||||
self.getModifiedItemAttr("damageMultiplierBonusPerCycle", 0),
|
||||
self.rawCycleTime / 1000, spoolType, spoolAmount)[0]
|
||||
spoolMultiplier = 1 + spoolBoost
|
||||
volley = DmgTypes(
|
||||
em=self.__baseVolley.em * spoolMultiplier * (1 - getattr(targetResists, "emAmount", 0)),
|
||||
thermal=self.__baseVolley.thermal * spoolMultiplier * (1 - getattr(targetResists, "thermalAmount", 0)),
|
||||
kinetic=self.__baseVolley.kinetic * spoolMultiplier * (1 - getattr(targetResists, "kineticAmount", 0)),
|
||||
explosive=self.__baseVolley.explosive * spoolMultiplier * (1 - getattr(targetResists, "explosiveAmount", 0)))
|
||||
return volley
|
||||
adjustedVolley = {}
|
||||
for volleyTime, volleyValue in self.__baseVolley.items():
|
||||
adjustedVolley[volleyTime] = DmgTypes(
|
||||
em=volleyValue.em * spoolMultiplier * (1 - getattr(targetProfile, "emAmount", 0)),
|
||||
thermal=volleyValue.thermal * spoolMultiplier * (1 - getattr(targetProfile, "thermalAmount", 0)),
|
||||
kinetic=volleyValue.kinetic * spoolMultiplier * (1 - getattr(targetProfile, "kineticAmount", 0)),
|
||||
explosive=volleyValue.explosive * spoolMultiplier * (1 - getattr(targetProfile, "explosiveAmount", 0)))
|
||||
return adjustedVolley
|
||||
|
||||
def getDps(self, spoolOptions=None, targetResists=None, ignoreState=False):
|
||||
volley = self.getVolley(spoolOptions=spoolOptions, targetResists=targetResists, ignoreState=ignoreState)
|
||||
if not volley:
|
||||
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)
|
||||
# Some weapons repeat multiple times in one cycle (bosonic doomsdays). Get the number of times it fires off
|
||||
volleysPerCycle = max(self.getModifiedItemAttr("doomsdayDamageDuration", 1) / self.getModifiedItemAttr("doomsdayDamageCycleTime", 1), 1)
|
||||
dpsFactor = volleysPerCycle / (self.cycleTime / 1000)
|
||||
return volleyParams[min(volleyParams)]
|
||||
|
||||
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, targetProfile=targetProfile, ignoreState=ignoreState)
|
||||
avgCycleTime = cycleParams.averageTime
|
||||
if len(volleyParams) == 0 or avgCycleTime == 0:
|
||||
return dmgDuringCycle
|
||||
for volleyValue in volleyParams.values():
|
||||
dmgDuringCycle += volleyValue
|
||||
dpsFactor = 1 / (avgCycleTime / 1000)
|
||||
dps = DmgTypes(
|
||||
em=volley.em * dpsFactor,
|
||||
thermal=volley.thermal * dpsFactor,
|
||||
kinetic=volley.kinetic * dpsFactor,
|
||||
explosive=volley.explosive * dpsFactor)
|
||||
em=dmgDuringCycle.em * dpsFactor,
|
||||
thermal=dmgDuringCycle.thermal * dpsFactor,
|
||||
kinetic=dmgDuringCycle.kinetic * dpsFactor,
|
||||
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:
|
||||
rrAmount *= 1 / (self.cycleTime / 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)
|
||||
@@ -620,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
|
||||
@@ -654,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):
|
||||
@@ -750,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
|
||||
@@ -819,49 +893,62 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
except:
|
||||
effect.handler(fit, self, context)
|
||||
|
||||
@property
|
||||
def cycleTime(self):
|
||||
def getCycleParameters(self, reloadOverride=None):
|
||||
"""Copied from new eos as well"""
|
||||
# Determine if we'll take into account reload time or not
|
||||
factorReload = self.owner.factorReload if self.forceReload is None else self.forceReload
|
||||
|
||||
numShots = self.numShots
|
||||
speed = self.rawCycleTime
|
||||
|
||||
if factorReload and self.charge:
|
||||
raw_reload_time = self.reloadTime
|
||||
if reloadOverride is not None:
|
||||
factorReload = reloadOverride
|
||||
else:
|
||||
raw_reload_time = 0.0
|
||||
factorReload = self.owner.factorReload if self.forceReload is None else self.forceReload
|
||||
|
||||
# Module can only fire one shot at a time, think bomb launchers or defender launchers
|
||||
if self.disallowRepeatingAction:
|
||||
if numShots > 0:
|
||||
"""
|
||||
The actual mechanics behind this is complex. Behavior will be (for 3 ammo):
|
||||
fire, reactivation delay, fire, reactivation delay, fire, max(reactivation delay, reload)
|
||||
so your effective reload time depends on where you are at in the cycle.
|
||||
cycles_until_reload = self.numShots
|
||||
if cycles_until_reload == 0:
|
||||
cycles_until_reload = math.inf
|
||||
|
||||
We can't do that, so instead we'll average it out.
|
||||
|
||||
Currently would apply to bomb launchers and defender missiles
|
||||
"""
|
||||
effective_reload_time = ((self.reactivationDelay * (numShots - 1)) + max(raw_reload_time, self.reactivationDelay, 0))
|
||||
else:
|
||||
"""
|
||||
Applies to MJD/MJFG
|
||||
"""
|
||||
effective_reload_time = max(raw_reload_time, self.reactivationDelay, 0)
|
||||
speed = speed + effective_reload_time
|
||||
active_time = self.rawCycleTime
|
||||
if active_time == 0:
|
||||
return None
|
||||
forced_inactive_time = self.reactivationDelay
|
||||
reload_time = self.reloadTime
|
||||
# Effects which cannot be reloaded have the same processing whether
|
||||
# caller wants to take reload time into account or not
|
||||
if reload_time is None and cycles_until_reload < math.inf:
|
||||
final_cycles = 1
|
||||
early_cycles = cycles_until_reload - final_cycles
|
||||
# Single cycle until effect cannot run anymore
|
||||
if early_cycles == 0:
|
||||
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, False)
|
||||
# Multiple cycles with different parameters
|
||||
return CycleSequence((
|
||||
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
|
||||
# 2) effect does not have to reload anything to keep running
|
||||
# 3) effect has enough time to reload during inactivity periods
|
||||
if (
|
||||
not factorReload or
|
||||
cycles_until_reload == math.inf or
|
||||
forced_inactive_time >= reload_time
|
||||
):
|
||||
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:
|
||||
"""
|
||||
Currently no other modules would have a reactivation delay, so for sanities sake don't try and account for it.
|
||||
Okay, technically cloaks do, but they also have 0 cycle time and cap usage so why do you care?
|
||||
"""
|
||||
effective_reload_time = raw_reload_time
|
||||
|
||||
if numShots > 0 and self.charge:
|
||||
speed = (speed * numShots + effective_reload_time) / numShots
|
||||
|
||||
return speed
|
||||
final_cycles = 1
|
||||
early_cycles = cycles_until_reload - final_cycles
|
||||
# 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, True)
|
||||
return CycleSequence((
|
||||
CycleInfo(active_time, forced_inactive_time, early_cycles, False),
|
||||
CycleInfo(active_time, reload_time, final_cycles, True)
|
||||
), math.inf)
|
||||
|
||||
@property
|
||||
def rawCycleTime(self):
|
||||
@@ -887,7 +974,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
def capUse(self):
|
||||
capNeed = self.getModifiedItemAttr("capacitorNeed")
|
||||
if capNeed and self.state >= FittingModuleState.ACTIVE:
|
||||
cycleTime = self.cycleTime
|
||||
cycleParams = self.getCycleParameters()
|
||||
if cycleParams is None:
|
||||
return 0
|
||||
cycleTime = cycleParams.averageTime
|
||||
if cycleTime > 0:
|
||||
capUsed = capNeed / (cycleTime / 1000.0)
|
||||
return capUsed
|
||||
|
||||
@@ -42,7 +42,7 @@ class PriceStatus(IntEnum):
|
||||
fetchTimeout = 4
|
||||
|
||||
|
||||
class Price(object):
|
||||
class Price:
|
||||
def __init__(self, typeID):
|
||||
self.typeID = typeID
|
||||
self.time = 0
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
69
eos/utils/cycles.py
Normal file
69
eos/utils/cycles.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# Borrowed from new eos
|
||||
|
||||
|
||||
from utils.repr import makeReprStr
|
||||
|
||||
|
||||
class CycleInfo:
|
||||
|
||||
def __init__(self, activeTime, inactiveTime, quantity, isInactivityReload):
|
||||
self.activeTime = activeTime
|
||||
self.inactiveTime = inactiveTime
|
||||
self.quantity = quantity
|
||||
self.isInactivityReload = isInactivityReload
|
||||
|
||||
@property
|
||||
def averageTime(self):
|
||||
return self.activeTime + self.inactiveTime
|
||||
|
||||
def iterCycles(self):
|
||||
i = 0
|
||||
while i < self.quantity:
|
||||
yield self.activeTime, self.inactiveTime, self.isInactivityReload
|
||||
i += 1
|
||||
|
||||
def _getCycleQuantity(self):
|
||||
return self.quantity
|
||||
|
||||
def _getTime(self):
|
||||
return (self.activeTime + self.inactiveTime) * self.quantity
|
||||
|
||||
def __repr__(self):
|
||||
spec = ['activeTime', 'inactiveTime', 'quantity', 'isInactivityReload']
|
||||
return makeReprStr(self, spec)
|
||||
|
||||
|
||||
class CycleSequence:
|
||||
|
||||
def __init__(self, sequence, quantity):
|
||||
self.sequence = sequence
|
||||
self.quantity = quantity
|
||||
|
||||
@property
|
||||
def averageTime(self):
|
||||
"""Get average time between cycles."""
|
||||
return self._getTime() / self._getCycleQuantity()
|
||||
|
||||
def iterCycles(self):
|
||||
i = 0
|
||||
while i < self.quantity:
|
||||
for cycleInfo in self.sequence:
|
||||
for cycleTime, inactiveTime, isInactivityReload in cycleInfo.iterCycles():
|
||||
yield cycleTime, inactiveTime, isInactivityReload
|
||||
i += 1
|
||||
|
||||
def _getCycleQuantity(self):
|
||||
quantity = 0
|
||||
for item in self.sequence:
|
||||
quantity += item._getCycleQuantity()
|
||||
return quantity
|
||||
|
||||
def _getTime(self):
|
||||
time = 0
|
||||
for item in self.sequence:
|
||||
time += item._getTime()
|
||||
return time
|
||||
|
||||
def __repr__(self):
|
||||
spec = ['sequence', 'quantity']
|
||||
return makeReprStr(self, spec)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
22
graphs/__init__.py
Normal file
22
graphs/__init__.py
Normal 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
65
graphs/calc.py
Normal 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
28
graphs/data/__init__.py
Normal 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
|
||||
23
graphs/data/base/__init__.py
Normal file
23
graphs/data/base/__init__.py
Normal 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
|
||||
@@ -18,22 +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
|
||||
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from gui.builtinGraphs import fitDps # noqa: E402, F401
|
||||
def clearAll(self):
|
||||
self._data.clear()
|
||||
134
graphs/data/base/defs.py
Normal file
134
graphs/data/base/defs.py
Normal 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))
|
||||
95
graphs/data/base/getter.py
Normal file
95
graphs/data/base/getter.py
Normal 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
298
graphs/data/base/graph.py
Normal 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
|
||||
24
graphs/data/fitCapacitor/__init__.py
Normal file
24
graphs/data/fitCapacitor/__init__.py
Normal 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()
|
||||
197
graphs/data/fitCapacitor/getter.py
Normal file
197
graphs/data/fitCapacitor/getter.py
Normal 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)
|
||||
62
graphs/data/fitCapacitor/graph.py
Normal file
62
graphs/data/fitCapacitor/graph.py
Normal 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')}
|
||||
24
graphs/data/fitDamageStats/__init__.py
Normal file
24
graphs/data/fitDamageStats/__init__.py
Normal 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()
|
||||
22
graphs/data/fitDamageStats/cache/__init__.py
vendored
Normal file
22
graphs/data/fitDamageStats/cache/__init__.py
vendored
Normal 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
|
||||
121
graphs/data/fitDamageStats/cache/projected.py
vendored
Normal file
121
graphs/data/fitDamageStats/cache/projected.py
vendored
Normal 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
254
graphs/data/fitDamageStats/cache/time.py
vendored
Normal 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]
|
||||
18
graphs/data/fitDamageStats/calc/__init__.py
Normal file
18
graphs/data/fitDamageStats/calc/__init__.py
Normal 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/>.
|
||||
# =============================================================================
|
||||
369
graphs/data/fitDamageStats/calc/application.py
Normal file
369
graphs/data/fitDamageStats/calc/application.py
Normal 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)
|
||||
182
graphs/data/fitDamageStats/calc/projected.py
Normal file
182
graphs/data/fitDamageStats/calc/projected.py
Normal 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)
|
||||
467
graphs/data/fitDamageStats/getter.py
Normal file
467
graphs/data/fitDamageStats/getter.py
Normal 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
|
||||
113
graphs/data/fitDamageStats/graph.py
Normal file
113
graphs/data/fitDamageStats/graph.py
Normal 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()}
|
||||
24
graphs/data/fitEwarStats/__init__.py
Normal file
24
graphs/data/fitEwarStats/__init__.py
Normal 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()
|
||||
305
graphs/data/fitEwarStats/getter.py
Normal file
305
graphs/data/fitEwarStats/getter.py
Normal 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
|
||||
59
graphs/data/fitEwarStats/graph.py
Normal file
59
graphs/data/fitEwarStats/graph.py
Normal 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}
|
||||
24
graphs/data/fitLockTime/__init__.py
Normal file
24
graphs/data/fitLockTime/__init__.py
Normal 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()
|
||||
29
graphs/data/fitLockTime/getter.py
Normal file
29
graphs/data/fitLockTime/getter.py
Normal 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
|
||||
39
graphs/data/fitLockTime/graph.py
Normal file
39
graphs/data/fitLockTime/graph.py
Normal 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}
|
||||
24
graphs/data/fitMobility/__init__.py
Normal file
24
graphs/data/fitMobility/__init__.py
Normal 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()
|
||||
62
graphs/data/fitMobility/getter.py
Normal file
62
graphs/data/fitMobility/getter.py
Normal 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
|
||||
41
graphs/data/fitMobility/graph.py
Normal file
41
graphs/data/fitMobility/graph.py
Normal 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}
|
||||
24
graphs/data/fitRemoteReps/__init__.py
Normal file
24
graphs/data/fitRemoteReps/__init__.py
Normal 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()
|
||||
218
graphs/data/fitRemoteReps/cache.py
Normal file
218
graphs/data/fitRemoteReps/cache.py
Normal 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]
|
||||
41
graphs/data/fitRemoteReps/calc.py
Normal file
41
graphs/data/fitRemoteReps/calc.py
Normal 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
|
||||
190
graphs/data/fitRemoteReps/getter.py
Normal file
190
graphs/data/fitRemoteReps/getter.py
Normal 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
|
||||
65
graphs/data/fitRemoteReps/graph.py
Normal file
65
graphs/data/fitRemoteReps/graph.py
Normal 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}
|
||||
24
graphs/data/fitShieldRegen/__init__.py
Normal file
24
graphs/data/fitShieldRegen/__init__.py
Normal 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()
|
||||
97
graphs/data/fitShieldRegen/getter.py
Normal file
97
graphs/data/fitShieldRegen/getter.py
Normal 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)
|
||||
79
graphs/data/fitShieldRegen/graph.py
Normal file
79
graphs/data/fitShieldRegen/graph.py
Normal 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
|
||||
24
graphs/data/fitWarpTime/__init__.py
Normal file
24
graphs/data/fitWarpTime/__init__.py
Normal 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()
|
||||
82
graphs/data/fitWarpTime/cache.py
Normal file
82
graphs/data/fitWarpTime/cache.py
Normal 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
|
||||
77
graphs/data/fitWarpTime/getter.py
Normal file
77
graphs/data/fitWarpTime/getter.py
Normal 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
|
||||
59
graphs/data/fitWarpTime/graph.py
Normal file
59
graphs/data/fitWarpTime/graph.py
Normal 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
4
graphs/events.py
Normal 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
18
graphs/gui/__init__.py
Normal 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
346
graphs/gui/canvasPanel.py
Normal 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
471
graphs/gui/ctrlPanel.py
Normal 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
245
graphs/gui/frame.py
Normal 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
379
graphs/gui/lists.py
Normal 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
106
graphs/gui/stylePickers.py
Normal 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
|
||||
262
graphs/gui/vector.py
Normal file
262
graphs/gui/vector.py
Normal file
@@ -0,0 +1,262 @@
|
||||
# =============================================================================
|
||||
# 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
|
||||
|
||||
# noinspection PyPackageRequirements
|
||||
import wx
|
||||
|
||||
from eos.utils.float import floatUnerr
|
||||
|
||||
|
||||
class VectorPicker(wx.Window):
|
||||
|
||||
myEVT_VECTOR_CHANGED = wx.NewEventType()
|
||||
EVT_VECTOR_CHANGED = wx.PyEventBinder(myEVT_VECTOR_CHANGED, 1)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._label = str(kwargs.pop('label', ''))
|
||||
self._labelpos = int(kwargs.pop('labelpos', 0))
|
||||
self._offset = float(kwargs.pop('offset', 0))
|
||||
self._size = max(0, float(kwargs.pop('size', 50)))
|
||||
self._fontsize = max(1, float(kwargs.pop('fontsize', 8)))
|
||||
self._directionOnly = kwargs.pop('directionOnly', False)
|
||||
super().__init__(*args, **kwargs)
|
||||
self._font = wx.Font(self._fontsize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)
|
||||
self._angle = 0
|
||||
self.__length = 1
|
||||
self._left = False
|
||||
self._right = False
|
||||
self._savedFocusedWindow = None
|
||||
self.SetToolTip(wx.ToolTip(self._tooltip))
|
||||
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
||||
self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
|
||||
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
|
||||
self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
|
||||
self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel)
|
||||
# Allows to focus these widgets on hover, needed to support
|
||||
# vector length changing by scrolling
|
||||
if 'wxMSW' in wx.PlatformInfo:
|
||||
self.Bind(wx.EVT_MOTION, self.OnMouseMove)
|
||||
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave)
|
||||
|
||||
@property
|
||||
def _tooltip(self):
|
||||
if self._directionOnly:
|
||||
return 'Click to set angle\nShift-click or right-click to snap to 15% angle'
|
||||
else:
|
||||
return 'Click to set angle and velocity\nShift-click or right-click to snap to 15% angle/5% speed increments\nMouse wheel to change velocity only'
|
||||
|
||||
@property
|
||||
def _length(self):
|
||||
if self._directionOnly:
|
||||
return 1
|
||||
else:
|
||||
return self.__length
|
||||
|
||||
@_length.setter
|
||||
def _length(self, newLength):
|
||||
self.__length = newLength
|
||||
|
||||
def DoGetBestSize(self):
|
||||
return wx.Size(self._size, self._size)
|
||||
|
||||
def AcceptsFocusFromKeyboard(self):
|
||||
return False
|
||||
|
||||
def GetValue(self):
|
||||
return self._angle, self._length
|
||||
|
||||
def GetAngle(self):
|
||||
return self._angle
|
||||
|
||||
def GetLength(self):
|
||||
return self._length
|
||||
|
||||
def SetValue(self, angle=None, length=None):
|
||||
if angle is not None:
|
||||
self._angle = min(max(angle, -180), 180)
|
||||
if length is not None:
|
||||
self._length = min(max(length, 0), 1)
|
||||
self.Refresh()
|
||||
|
||||
def SetAngle(self, angle):
|
||||
self.SetValue(angle, None)
|
||||
|
||||
def SetLength(self, length):
|
||||
self.SetValue(None, length)
|
||||
|
||||
def OnPaint(self, event):
|
||||
dc = wx.BufferedPaintDC(self)
|
||||
self.Draw(dc)
|
||||
|
||||
def Draw(self, dc):
|
||||
width, height = self.GetClientSize()
|
||||
if not width or not height:
|
||||
return
|
||||
dc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.BRUSHSTYLE_SOLID))
|
||||
dc.Clear()
|
||||
dc.SetTextForeground(wx.Colour(0))
|
||||
dc.SetFont(self._font)
|
||||
|
||||
radius = min(width, height) / 2 - 2
|
||||
dc.SetBrush(wx.WHITE_BRUSH)
|
||||
dc.DrawCircle(radius + 2, radius + 2, radius)
|
||||
a = math.radians(self._angle + self._offset)
|
||||
x = math.cos(a) * radius
|
||||
y = math.sin(a) * radius
|
||||
dc.DrawLine(radius + 2, radius + 2, radius + 2 + x * self._length, radius + 2 - y * self._length)
|
||||
dc.SetBrush(wx.BLACK_BRUSH)
|
||||
dc.DrawCircle(radius + 2 + x * self._length, radius + 2 - y * self._length, 2)
|
||||
|
||||
if self._label:
|
||||
labelText = self._label
|
||||
labelTextW, labelTextH = dc.GetTextExtent(labelText)
|
||||
labelTextX = (radius * 2 + 4 - labelTextW) if (self._labelpos & 1) else 0
|
||||
labelTextY = (radius * 2 + 4 - labelTextH) if (self._labelpos & 2) else 0
|
||||
dc.DrawText(labelText, labelTextX, labelTextY)
|
||||
|
||||
if not self._directionOnly:
|
||||
lengthText = '%d%%' % (100 * self._length,)
|
||||
lengthTextW, lengthTextH = dc.GetTextExtent(lengthText)
|
||||
lengthTextX = radius + 2 + x / 2 - y / 3 - lengthTextW / 2
|
||||
lengthTextY = radius + 2 - y / 2 - x / 3 - lengthTextH / 2
|
||||
dc.DrawText(lengthText, lengthTextX, lengthTextY)
|
||||
|
||||
angleText = '%d\u00B0' % (self._angle,)
|
||||
angleTextW, angleTextH = dc.GetTextExtent(angleText)
|
||||
angleTextX = radius + 2 - x / 2 - angleTextW / 2
|
||||
angleTextY = radius + 2 + y / 2 - angleTextH / 2
|
||||
dc.DrawText(angleText, angleTextX, angleTextY)
|
||||
|
||||
def OnEraseBackground(self, event):
|
||||
pass
|
||||
|
||||
def OnLeftDown(self, event):
|
||||
self._left = True
|
||||
self.SetToolTip(None)
|
||||
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
|
||||
self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnLeftUp)
|
||||
if not self._right:
|
||||
self.Bind(wx.EVT_MOTION, self.OnMotion)
|
||||
if not self.HasCapture():
|
||||
self.CaptureMouse()
|
||||
self.HandleMouseEvent(event)
|
||||
|
||||
def OnRightDown(self, event):
|
||||
self._right = True
|
||||
self.SetToolTip(None)
|
||||
self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
|
||||
self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnRightUp)
|
||||
if not self._left:
|
||||
self.Bind(wx.EVT_MOTION, self.OnMotion)
|
||||
if not self.HasCapture():
|
||||
self.CaptureMouse()
|
||||
self.HandleMouseEvent(event)
|
||||
|
||||
def OnLeftUp(self, event):
|
||||
self.HandleMouseEvent(event)
|
||||
self.Unbind(wx.EVT_LEFT_UP, handler=self.OnLeftUp)
|
||||
self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST, handler=self.OnLeftUp)
|
||||
self._left = False
|
||||
if not self._right:
|
||||
self.Unbind(wx.EVT_MOTION, handler=self.OnMotion)
|
||||
self.SendChangeEvent()
|
||||
self.SetToolTip(wx.ToolTip(self._tooltip))
|
||||
if self.HasCapture():
|
||||
self.ReleaseMouse()
|
||||
|
||||
def OnRightUp(self, event):
|
||||
self.HandleMouseEvent(event)
|
||||
self.Unbind(wx.EVT_RIGHT_UP, handler=self.OnRightUp)
|
||||
self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST, handler=self.OnRightUp)
|
||||
self._right = False
|
||||
if not self._left:
|
||||
self.Unbind(wx.EVT_MOTION, handler=self.OnMotion)
|
||||
self.SendChangeEvent()
|
||||
self.SetToolTip(wx.ToolTip(self._tooltip))
|
||||
if self.HasCapture():
|
||||
self.ReleaseMouse()
|
||||
|
||||
def OnMotion(self, event):
|
||||
self.HandleMouseEvent(event)
|
||||
event.Skip()
|
||||
|
||||
def OnWheel(self, event):
|
||||
amount = 0.1 * event.GetWheelRotation() / event.GetWheelDelta()
|
||||
self._length = floatUnerr(min(max(self._length + amount, 0.0), 1.0))
|
||||
self.Refresh()
|
||||
self.SendChangeEvent()
|
||||
|
||||
def HandleMouseEvent(self, event):
|
||||
width, height = self.GetClientSize()
|
||||
if width and height:
|
||||
center = min(width, height) / 2
|
||||
x, y = event.GetPosition()
|
||||
x = x - center
|
||||
y = center - y
|
||||
angle = self._angle
|
||||
length = min((x ** 2 + y ** 2) ** 0.5 / (center - 2), 1.0)
|
||||
if length < 0.01:
|
||||
length = 0
|
||||
else:
|
||||
angle = ((math.degrees(math.atan2(y, x)) - self._offset + 180) % 360) - 180
|
||||
if (self._right and not self._left) or event.ShiftDown():
|
||||
angle = round(angle / 15.0) * 15.0
|
||||
# floor() for length to make it easier to hit 0%, can still hit 100% outside the circle
|
||||
length = math.floor(length / 0.05) * 0.05
|
||||
if (angle != self._angle) or (length != self._length):
|
||||
self._angle = angle
|
||||
self._length = length
|
||||
self.Refresh()
|
||||
if (self._right and not self._left) or event.ShiftDown():
|
||||
self.SendChangeEvent()
|
||||
|
||||
# Focus manipulation - otherwise scrolling doesn't work under Windows
|
||||
def OnMouseMove(self, event):
|
||||
event.Skip()
|
||||
if not self.HasFocus():
|
||||
self._savedFocusedWindow = self.FindFocus()
|
||||
self.SetFocus()
|
||||
|
||||
def OnMouseLeave(self, event):
|
||||
event.Skip()
|
||||
if self.HasFocus():
|
||||
if self._savedFocusedWindow is not None:
|
||||
self._savedFocusedWindow.SetFocus()
|
||||
self._savedFocusedWindow = None
|
||||
|
||||
def SendChangeEvent(self):
|
||||
changeEvent = wx.CommandEvent(self.myEVT_VECTOR_CHANGED, self.GetId())
|
||||
changeEvent._object = self
|
||||
changeEvent._angle = self._angle
|
||||
changeEvent._length = self._length
|
||||
self.GetEventHandler().ProcessEvent(changeEvent)
|
||||
|
||||
def SetDirectionOnly(self, val):
|
||||
if self._directionOnly is val:
|
||||
return
|
||||
self._directionOnly = val
|
||||
self.GetToolTip().SetTip(self._tooltip)
|
||||
|
||||
@property
|
||||
def IsDirectionOnly(self):
|
||||
return self._directionOnly
|
||||
|
||||
89
graphs/style.py
Normal file
89
graphs/style.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# =============================================================================
|
||||
# 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 OrderedDict, namedtuple
|
||||
|
||||
# noinspection PyPackageRequirements
|
||||
import wx
|
||||
|
||||
from service.const import GraphColor, GraphLightness, GraphLineStyle
|
||||
|
||||
|
||||
ColorData = namedtuple('ColorData', ('hsl', 'name', 'iconName'))
|
||||
LightnessData = namedtuple('LightnessData', ('name', 'iconName', 'func'))
|
||||
|
||||
|
||||
class LineStyleData:
|
||||
|
||||
def __init__(self, name, iconNamePrefix, mplSpec):
|
||||
self.name = name
|
||||
self._iconNamePrefix = iconNamePrefix
|
||||
self.mplSpec = mplSpec
|
||||
|
||||
@property
|
||||
def iconName(self):
|
||||
# Get lightness out of RGB color, see following link for math:
|
||||
# https://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
|
||||
r, g, b, a = (c / 255 for c in wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
|
||||
l = (max(r, g, b) + min (r, g, b)) / 2
|
||||
suffix = '_black' if l > 0.3 else '_white'
|
||||
return '{}{}'.format(self._iconNamePrefix, suffix)
|
||||
|
||||
|
||||
# In HSL format
|
||||
BASE_COLORS = OrderedDict([
|
||||
(GraphColor.red, ColorData((0 / 360.0, 1.0, 0.5), 'Red', 'color_red')),
|
||||
(GraphColor.green, ColorData((120 / 360.0, 1.0, 0.5), 'Green', 'color_green')),
|
||||
(GraphColor.blue, ColorData((240 / 360.0, 1.0, 0.5), 'Blue', 'color_blue')),
|
||||
(GraphColor.orange, ColorData((40 / 360.0, 1.0, 0.5), 'Orange', 'color_orange')),
|
||||
(GraphColor.magenta, ColorData((300 / 360.0, 1.0, 0.5), 'Magenta', 'color_magenta')),
|
||||
(GraphColor.cyan, ColorData((180 / 360.0, 1.0, 0.5), 'Cyan', 'color_cyan')),
|
||||
(GraphColor.purple, ColorData((275 / 360.0, 1.0, 0.5), 'Purple', 'color_purple')),
|
||||
(GraphColor.yellow, ColorData((56 / 360.0, 1.0, 0.5), 'Yellow', 'color_yellow'))])
|
||||
|
||||
|
||||
def hsl_to_hsv(hsl):
|
||||
h, s, l = hsl
|
||||
s *= l if (l < 0.5) else (1 - l)
|
||||
l += s
|
||||
return (h, 2 * s / l, l)
|
||||
|
||||
|
||||
def darken(hsl):
|
||||
h, s, l = hsl
|
||||
return h, s * 0.5, l * 0.7
|
||||
|
||||
|
||||
def brighten(hsl):
|
||||
h, s, l = hsl
|
||||
return h, s * 0.5, l + (1 - l) * 0.5
|
||||
|
||||
|
||||
LIGHTNESSES = OrderedDict([
|
||||
(GraphLightness.normal, LightnessData('Normal', 'lightness_normal', lambda hsl: hsl)),
|
||||
(GraphLightness.dark, LightnessData('Dark', 'lightness_dark', darken)),
|
||||
(GraphLightness.bright, LightnessData('Bright', 'lightness_bright', brighten))])
|
||||
|
||||
|
||||
STYLES = OrderedDict([
|
||||
(GraphLineStyle.solid, LineStyleData('Solid', 'style_solid', 'solid')),
|
||||
(GraphLineStyle.dashed, LineStyleData('Dashed', 'style_dashed', (0, (5, 1)))),
|
||||
(GraphLineStyle.dotted, LineStyleData('Dotted', 'style_dotted', (0, (1, 1)))),
|
||||
(GraphLineStyle.dashdotted, LineStyleData('Dash-dotted', 'style_dashdot', (0, (3, 1, 1, 1))))])
|
||||
257
graphs/wrapper.py
Normal file
257
graphs/wrapper.py
Normal file
@@ -0,0 +1,257 @@
|
||||
# =============================================================================
|
||||
# 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.saveddata.damagePattern import DamagePattern
|
||||
from eos.saveddata.fit import Fit
|
||||
from eos.saveddata.targetProfile import TargetProfile
|
||||
from service.const import TargetResistMode
|
||||
from .calc import calculateMultiplier
|
||||
|
||||
|
||||
class BaseWrapper:
|
||||
|
||||
def __init__(self, item):
|
||||
self.item = item
|
||||
|
||||
@property
|
||||
def isFit(self):
|
||||
return isinstance(self.item, Fit)
|
||||
|
||||
@property
|
||||
def isProfile(self):
|
||||
return isinstance(self.item, TargetProfile)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if self.isFit:
|
||||
return '{} ({})'.format(self.item.name, self.item.ship.item.name)
|
||||
elif self.isProfile:
|
||||
return self.item.name
|
||||
return ''
|
||||
|
||||
@property
|
||||
def shortName(self):
|
||||
if self.isFit:
|
||||
return '{} ({})'.format(self.item.name, self.item.ship.item.getShortName())
|
||||
elif self.isProfile:
|
||||
return self.item.name
|
||||
return ''
|
||||
|
||||
def getMaxVelocity(self, extraMultipliers=None, ignoreAfflictors=()):
|
||||
if self.isFit:
|
||||
if extraMultipliers or ignoreAfflictors:
|
||||
maxVelocity = self.item.ship.getModifiedItemAttrExtended(
|
||||
'maxVelocity',
|
||||
extraMultipliers=extraMultipliers,
|
||||
ignoreAfflictors=ignoreAfflictors)
|
||||
else:
|
||||
maxVelocity = self.item.ship.getModifiedItemAttr('maxVelocity')
|
||||
elif self.isProfile:
|
||||
maxVelocity = self.item.maxVelocity
|
||||
if extraMultipliers:
|
||||
maxVelocity *= calculateMultiplier(extraMultipliers)
|
||||
else:
|
||||
maxVelocity = None
|
||||
return maxVelocity
|
||||
|
||||
def getSigRadius(self, extraMultipliers=None, ignoreAfflictors=()):
|
||||
if self.isFit:
|
||||
if extraMultipliers or ignoreAfflictors:
|
||||
sigRadius = self.item.ship.getModifiedItemAttrExtended(
|
||||
'signatureRadius',
|
||||
extraMultipliers=extraMultipliers,
|
||||
ignoreAfflictors=ignoreAfflictors)
|
||||
else:
|
||||
sigRadius = self.item.ship.getModifiedItemAttr('signatureRadius')
|
||||
elif self.isProfile:
|
||||
sigRadius = self.item.signatureRadius
|
||||
if extraMultipliers:
|
||||
sigRadius *= calculateMultiplier(extraMultipliers)
|
||||
else:
|
||||
sigRadius = None
|
||||
return sigRadius
|
||||
|
||||
def getRadius(self):
|
||||
if self.isFit:
|
||||
radius = self.item.ship.getModifiedItemAttr('radius')
|
||||
elif self.isProfile:
|
||||
radius = self.item.radius
|
||||
else:
|
||||
radius = None
|
||||
return radius
|
||||
|
||||
|
||||
class SourceWrapper(BaseWrapper):
|
||||
|
||||
def __init__(self, item, colorID):
|
||||
super().__init__(item)
|
||||
self._colorID = colorID
|
||||
|
||||
@property
|
||||
def colorID(self):
|
||||
return self._colorID
|
||||
|
||||
@colorID.setter
|
||||
def colorID(self, value):
|
||||
self._colorID = value
|
||||
|
||||
|
||||
class TargetWrapper(BaseWrapper):
|
||||
|
||||
def __init__(self, item, lightnessID, lineStyleID):
|
||||
super().__init__(item=item)
|
||||
self.lightnessID = lightnessID
|
||||
self.lineStyleID = lineStyleID
|
||||
self.resistMode = TargetResistMode.auto
|
||||
|
||||
def getResists(self, includeLayer=False):
|
||||
em = therm = kin = explo = 0
|
||||
layer = None
|
||||
if self.isProfile:
|
||||
em = self.item.emAmount
|
||||
therm = self.item.thermalAmount
|
||||
kin = self.item.kineticAmount
|
||||
explo = self.item.explosiveAmount
|
||||
if self.isFit:
|
||||
if self.resistMode == TargetResistMode.auto:
|
||||
em, therm, kin, explo, layer = _getAutoResists(fit=self.item)
|
||||
elif self.resistMode == TargetResistMode.shield:
|
||||
em, therm, kin, explo = _getShieldResists(ship=self.item.ship)
|
||||
elif self.resistMode == TargetResistMode.armor:
|
||||
em, therm, kin, explo = _getArmorResists(ship=self.item.ship)
|
||||
elif self.resistMode == TargetResistMode.hull:
|
||||
em, therm, kin, explo = _getHullResists(ship=self.item.ship)
|
||||
elif self.resistMode == TargetResistMode.weightedAverage:
|
||||
em, therm, kin, explo = _getWeightedResists(fit=self.item)
|
||||
if includeLayer:
|
||||
return em, therm, kin, explo, layer
|
||||
else:
|
||||
return em, therm, kin, explo
|
||||
|
||||
|
||||
|
||||
def _getShieldResists(ship):
|
||||
em = 1 - ship.getModifiedItemAttr('shieldEmDamageResonance')
|
||||
therm = 1 - ship.getModifiedItemAttr('shieldThermalDamageResonance')
|
||||
kin = 1 - ship.getModifiedItemAttr('shieldKineticDamageResonance')
|
||||
explo = 1 - ship.getModifiedItemAttr('shieldExplosiveDamageResonance')
|
||||
return em, therm, kin, explo
|
||||
|
||||
|
||||
def _getArmorResists(ship):
|
||||
em = 1 - ship.getModifiedItemAttr('armorEmDamageResonance')
|
||||
therm = 1 - ship.getModifiedItemAttr('armorThermalDamageResonance')
|
||||
kin = 1 - ship.getModifiedItemAttr('armorKineticDamageResonance')
|
||||
explo = 1 - ship.getModifiedItemAttr('armorExplosiveDamageResonance')
|
||||
return em, therm, kin, explo
|
||||
|
||||
|
||||
def _getHullResists(ship):
|
||||
em = 1 - ship.getModifiedItemAttr('emDamageResonance')
|
||||
therm = 1 - ship.getModifiedItemAttr('thermalDamageResonance')
|
||||
kin = 1 - ship.getModifiedItemAttr('kineticDamageResonance')
|
||||
explo = 1 - ship.getModifiedItemAttr('explosiveDamageResonance')
|
||||
return em, therm, kin, explo
|
||||
|
||||
|
||||
def _getWeightedResists(fit):
|
||||
shieldEmRes, shieldThermRes, shieldKinRes, shieldExploRes = _getShieldResists(fit.ship)
|
||||
armorEmRes, armorThermRes, armorKinRes, armorExploRes = _getArmorResists(fit.ship)
|
||||
hullEmRes, hullThermRes, hullKinRes, hullExploRes = _getHullResists(fit.ship)
|
||||
hpData = fit.hp
|
||||
shieldHp = hpData['shield']
|
||||
armorHp = hpData['armor']
|
||||
hullHp = hpData['hull']
|
||||
totalHp = shieldHp + armorHp + hullHp
|
||||
totalEhpEm = shieldHp / (1 - shieldEmRes) + armorHp / (1 - armorEmRes) + hullHp / (1 - hullEmRes)
|
||||
totalEhpTherm = shieldHp / (1 - shieldThermRes) + armorHp / (1 - armorThermRes) + hullHp / (1 - hullThermRes)
|
||||
totalEhpKin = shieldHp / (1 - shieldKinRes) + armorHp / (1 - armorKinRes) + hullHp / (1 - hullKinRes)
|
||||
totalEhpExplo = shieldHp / (1 - shieldExploRes) + armorHp / (1 - armorExploRes) + hullHp / (1 - hullExploRes)
|
||||
weightedEmRes = 1 - totalHp / totalEhpEm
|
||||
weightedThermRes = 1 - totalHp / totalEhpTherm
|
||||
weightedKinRes = 1 - totalHp / totalEhpKin
|
||||
weightedExploRes = 1 - totalHp / totalEhpExplo
|
||||
return weightedEmRes, weightedThermRes, weightedKinRes, weightedExploRes
|
||||
|
||||
|
||||
def _getAutoResists(fit):
|
||||
# Get all the data
|
||||
# HP / EHP
|
||||
hpData = fit.hp
|
||||
shieldHp = hpData['shield']
|
||||
armorHp = hpData['armor']
|
||||
hullHp = hpData['hull']
|
||||
uniformDamagePattern = DamagePattern(emAmount=25, thermalAmount=25, kineticAmount=25, explosiveAmount=25)
|
||||
ehpData = uniformDamagePattern.calculateEhp(fit)
|
||||
shieldEhp = ehpData['shield']
|
||||
armorEhp = ehpData['armor']
|
||||
hullEhp = ehpData['hull']
|
||||
totalEhp = shieldEhp + armorEhp + hullEhp
|
||||
# Resist factors
|
||||
try:
|
||||
shieldResFactor = shieldEhp / shieldHp
|
||||
except ZeroDivisionError:
|
||||
shieldResFactor = 1
|
||||
try:
|
||||
armorResFactor = armorEhp / armorHp
|
||||
except ZeroDivisionError:
|
||||
armorResFactor = 1
|
||||
try:
|
||||
hullResFactor = hullEhp / hullHp
|
||||
except ZeroDivisionError:
|
||||
hullResFactor = 1
|
||||
# Tank
|
||||
tankData = fit.tank
|
||||
shieldTank = tankData['shieldRepair']
|
||||
armorTank = tankData['armorRepair']
|
||||
hullTank = tankData['hullRepair']
|
||||
shieldRegen = tankData['passiveShield']
|
||||
|
||||
shieldScore = 0
|
||||
armorScore = 0
|
||||
hullScore = 0
|
||||
# EHP scoring
|
||||
ehpWeight = 100
|
||||
shieldScore += ehpWeight * (shieldEhp / totalEhp) ** 1.5
|
||||
armorScore += ehpWeight * (armorEhp / totalEhp) ** 1.5
|
||||
hullScore += ehpWeight * (hullEhp / totalEhp) ** 1.5
|
||||
# Resists scoring
|
||||
# We include it to have some extra points for receiving better reps from the outside
|
||||
resistWeight = 25
|
||||
bestResFactor = max(shieldResFactor, armorResFactor, hullResFactor)
|
||||
shieldScore += resistWeight * (shieldResFactor / bestResFactor) ** 1.5
|
||||
armorScore += resistWeight * (armorResFactor / bestResFactor) ** 1.5
|
||||
hullScore += resistWeight * (hullResFactor / bestResFactor) ** 1.5
|
||||
# Active tank
|
||||
activeWeight = 10000
|
||||
shieldScore += activeWeight * shieldTank * shieldResFactor / totalEhp
|
||||
armorScore += activeWeight * armorTank * armorResFactor / totalEhp
|
||||
hullScore += activeWeight * hullTank * hullResFactor / totalEhp
|
||||
# Shield regen
|
||||
regenWeight = 5000
|
||||
shieldScore += regenWeight * shieldRegen * shieldResFactor / totalEhp
|
||||
maxScore = max(shieldScore, armorScore, hullScore)
|
||||
if maxScore == shieldScore:
|
||||
return (*_getShieldResists(fit.ship), 'shield')
|
||||
if maxScore == armorScore:
|
||||
return (*_getArmorResists(fit.ship), 'armor')
|
||||
if maxScore == hullScore:
|
||||
return (*_getHullResists(fit.ship), 'hull')
|
||||
return 0, 0, 0, 0, None
|
||||
@@ -9,18 +9,17 @@ from gui.utils import anim_effects
|
||||
|
||||
|
||||
class AttributeGauge(wx.Window):
|
||||
def __init__(self, parent, max_range=100, animate=True, leading_edge=True, edge_on_neutral=True, guide_lines=False, size=(-1, 30), *args,
|
||||
**kargs):
|
||||
|
||||
super().__init__(parent, size=size, *args, **kargs)
|
||||
def __init__(
|
||||
self, parent, max_range=100, animate=True, leading_edge=True,
|
||||
edge_on_neutral=True, guide_lines=False, size=(-1, 30), *args, **kwargs
|
||||
):
|
||||
super().__init__(parent, size=size, *args, **kwargs)
|
||||
|
||||
self._size = size
|
||||
|
||||
self.guide_lines = guide_lines
|
||||
|
||||
self._border_colour = wx.BLACK
|
||||
self._bar_colour = None
|
||||
self._bar_gradient = None
|
||||
|
||||
self.leading_edge = leading_edge
|
||||
self.edge_on_neutral = edge_on_neutral
|
||||
@@ -29,8 +28,6 @@ class AttributeGauge(wx.Window):
|
||||
self._max_range = max_range
|
||||
self._value = 0
|
||||
|
||||
self._fraction_digits = 0
|
||||
|
||||
self._timer_id = wx.NewId()
|
||||
self._timer = None
|
||||
|
||||
@@ -44,14 +41,6 @@ class AttributeGauge(wx.Window):
|
||||
self._anim_direction = 0
|
||||
self.anim_effect = anim_effects.OUT_QUAD
|
||||
|
||||
# transition colors used based on how full (or overfilled) the gauge is.
|
||||
self.transition_colors = [
|
||||
(wx.Colour(191, 191, 191), wx.Colour(96, 191, 0)), # < 0-100%
|
||||
(wx.Colour(191, 167, 96), wx.Colour(255, 191, 0)), # < 100-101%
|
||||
(wx.Colour(255, 191, 0), wx.Colour(255, 128, 0)), # < 101-103%
|
||||
(wx.Colour(255, 128, 0), wx.Colour(255, 0, 0)) # < 103-105%
|
||||
]
|
||||
|
||||
self.goodColor = wx.Colour(96, 191, 0)
|
||||
self.badColor = wx.Colour(255, 64, 0)
|
||||
|
||||
@@ -68,51 +57,17 @@ class AttributeGauge(wx.Window):
|
||||
|
||||
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
||||
self.Bind(wx.EVT_TIMER, self.OnTimer)
|
||||
self.Bind(wx.EVT_ENTER_WINDOW, self.OnWindowEnter)
|
||||
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnWindowLeave)
|
||||
self.SetBackgroundStyle(wx.BG_STYLE_PAINT)
|
||||
|
||||
def OnEraseBackground(self, event):
|
||||
pass
|
||||
|
||||
def OnWindowEnter(self, event):
|
||||
self._show_remaining = True
|
||||
self.Refresh()
|
||||
|
||||
def OnWindowLeave(self, event):
|
||||
self._show_remaining = False
|
||||
self.Refresh()
|
||||
|
||||
def GetBorderColour(self):
|
||||
return self._border_colour
|
||||
|
||||
def SetBorderColour(self, colour):
|
||||
self._border_colour = colour
|
||||
|
||||
def GetBarColour(self):
|
||||
return self._bar_colour
|
||||
|
||||
def SetBarColour(self, colour):
|
||||
self._bar_colour = colour
|
||||
|
||||
def SetFractionDigits(self, digits):
|
||||
self._fraction_digits = digits
|
||||
|
||||
def GetBarGradient(self):
|
||||
if self._bar_gradient is None:
|
||||
return None
|
||||
|
||||
return self._bar_gradient[0]
|
||||
|
||||
def SetBarGradient(self, gradient=None):
|
||||
if gradient is None:
|
||||
self._bar_gradient = None
|
||||
else:
|
||||
if not isinstance(gradient, list):
|
||||
self._bar_gradient = [gradient]
|
||||
else:
|
||||
self._bar_gradient = list(gradient)
|
||||
|
||||
def GetBorderPadding(self):
|
||||
return self._border_padding
|
||||
|
||||
@@ -134,6 +89,11 @@ class AttributeGauge(wx.Window):
|
||||
self._anim_value = self._percentage
|
||||
self.Refresh()
|
||||
|
||||
def FreezeAnimation(self):
|
||||
self._animate = False
|
||||
if self._timer:
|
||||
self._timer.Stop()
|
||||
|
||||
def SetRange(self, range, reinit=False, animate=True):
|
||||
"""
|
||||
Sets the range of the gauge. The gauge length is its
|
||||
@@ -231,51 +191,50 @@ class AttributeGauge(wx.Window):
|
||||
pad = 1 + self.GetBorderPadding()
|
||||
rect.Deflate(pad, pad)
|
||||
|
||||
if True:
|
||||
# if we have a bar color set, then we will use this
|
||||
colour = self.goodColor if value >= 0 else self.badColor
|
||||
# if we have a bar color set, then we will use this
|
||||
colour = self.goodColor if value >= 0 else self.badColor
|
||||
|
||||
is_even = rect.width % 2 == 0
|
||||
is_even = rect.width % 2 == 0
|
||||
|
||||
# the size of half our available drawing area (since we're only working in halves)
|
||||
half = (rect.width / 2)
|
||||
# the size of half our available drawing area (since we're only working in halves)
|
||||
half = (rect.width / 2)
|
||||
|
||||
# calculate width of bar as a percentage of half the space
|
||||
w = abs(half * (value / 100))
|
||||
w = min(w, half) # Ensure that we don't overshoot our drawing area
|
||||
w = math.ceil(w) # round up to nearest pixel, this ensures that we don't lose representation for sub pixels
|
||||
# calculate width of bar as a percentage of half the space
|
||||
w = abs(half * (value / 100))
|
||||
w = min(w, half) # Ensure that we don't overshoot our drawing area
|
||||
w = math.ceil(w) # round up to nearest pixel, this ensures that we don't lose representation for sub pixels
|
||||
|
||||
# print("Percentage: {}\t\t\t\t\tValue: {}\t\t\t\t\tWidth: {}\t\t\t\t\tHalf: {}\t\t\t\t\tRect Width: {}".format(
|
||||
# round(self._percentage, 3), round(value,3), w, half, rect.width))
|
||||
# print("Percentage: {}\t\t\t\t\tValue: {}\t\t\t\t\tWidth: {}\t\t\t\t\tHalf: {}\t\t\t\t\tRect Width: {}".format(
|
||||
# round(self._percentage, 3), round(value,3), w, half, rect.width))
|
||||
|
||||
# set guide_lines every 10 pixels of the main gauge (not including borders)
|
||||
if self.guide_lines:
|
||||
for x in range(1, 20):
|
||||
dc.SetBrush(wx.Brush(wx.LIGHT_GREY))
|
||||
dc.SetPen(wx.Pen(wx.LIGHT_GREY))
|
||||
dc.DrawRectangle(x * 10, 1, 1, rect.height)
|
||||
# set guide_lines every 10 pixels of the main gauge (not including borders)
|
||||
if self.guide_lines:
|
||||
for x in range(1, 20):
|
||||
dc.SetBrush(wx.Brush(wx.LIGHT_GREY))
|
||||
dc.SetPen(wx.Pen(wx.LIGHT_GREY))
|
||||
dc.DrawRectangle(x * 10, 1, 1, rect.height)
|
||||
|
||||
dc.SetBrush(wx.Brush(colour))
|
||||
dc.SetPen(wx.Pen(colour))
|
||||
dc.SetBrush(wx.Brush(colour))
|
||||
dc.SetPen(wx.Pen(colour))
|
||||
|
||||
# If we have an even width, we can simply dedicate the middle-most pixels to both sides
|
||||
# However, if there is an odd width, the middle pixel is shared between the left and right gauge
|
||||
# If we have an even width, we can simply dedicate the middle-most pixels to both sides
|
||||
# However, if there is an odd width, the middle pixel is shared between the left and right gauge
|
||||
|
||||
if value >= 0:
|
||||
padding = (half if is_even else math.ceil(half - 1)) + 1
|
||||
dc.DrawRectangle(padding, 1, w, rect.height)
|
||||
if value >= 0:
|
||||
padding = (half if is_even else math.ceil(half - 1)) + 1
|
||||
dc.DrawRectangle(padding, 1, w, rect.height)
|
||||
else:
|
||||
padding = half - w + 1 if is_even else math.ceil(half) - (w - 1)
|
||||
dc.DrawRectangle(padding, 1, w, rect.height)
|
||||
|
||||
if self.leading_edge and (self.edge_on_neutral or value != 0):
|
||||
dc.SetPen(wx.Pen(wx.WHITE))
|
||||
dc.SetBrush(wx.Brush(wx.WHITE))
|
||||
|
||||
if value > 0:
|
||||
dc.DrawRectangle(min(padding + w, rect.width), 1, 1, rect.height)
|
||||
else:
|
||||
padding = half - w + 1 if is_even else math.ceil(half) - (w - 1)
|
||||
dc.DrawRectangle(padding, 1, w, rect.height)
|
||||
|
||||
if self.leading_edge and (self.edge_on_neutral or value != 0):
|
||||
dc.SetPen(wx.Pen(wx.WHITE))
|
||||
dc.SetBrush(wx.Brush(wx.WHITE))
|
||||
|
||||
if value > 0:
|
||||
dc.DrawRectangle(min(padding + w, rect.width), 1, 1, rect.height)
|
||||
else:
|
||||
dc.DrawRectangle(max(padding - 1, 1), 1, 1, rect.height)
|
||||
dc.DrawRectangle(max(padding - 1, 1), 1, 1, rect.height)
|
||||
|
||||
def OnTimer(self, event):
|
||||
old_value = self._old_percentage
|
||||
@@ -315,186 +274,3 @@ class AttributeGauge(wx.Window):
|
||||
self._timer.Stop()
|
||||
|
||||
self.Refresh()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import random
|
||||
|
||||
def frange(x, y, jump):
|
||||
while x < y:
|
||||
yield x
|
||||
x += jump
|
||||
|
||||
class MyPanel(wx.Panel):
|
||||
def __init__(self, parent, size=(500, 500)):
|
||||
wx.Panel.__init__(self, parent, size=size)
|
||||
box = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
self.gauge = gauge = AttributeGauge(self, size=(204, 4))
|
||||
gauge.SetBackgroundColour(wx.Colour(52, 86, 98))
|
||||
gauge.SetBarColour(wx.Colour(255, 128, 0))
|
||||
gauge.SetValue(100)
|
||||
gauge.SetFractionDigits(1)
|
||||
box.Add(gauge, 0, wx.ALL | wx.CENTER, 10)
|
||||
|
||||
self.gauge11 = gauge = AttributeGauge(self, size=(204, 6))
|
||||
gauge.SetBackgroundColour(wx.Colour(52, 86, 98))
|
||||
gauge.SetBarColour(wx.Colour(255, 128, 0))
|
||||
gauge.SetValue(100)
|
||||
gauge.SetFractionDigits(1)
|
||||
box.Add(gauge, 0, wx.ALL | wx.CENTER, 10)
|
||||
|
||||
self.gauge12 = gauge = AttributeGauge(self, size=(204, 8))
|
||||
gauge.SetBackgroundColour(wx.Colour(52, 86, 98))
|
||||
gauge.SetBarColour(wx.Colour(255, 128, 0))
|
||||
gauge.SetValue(100)
|
||||
gauge.SetFractionDigits(1)
|
||||
box.Add(gauge, 0, wx.ALL | wx.CENTER, 10)
|
||||
|
||||
self.gauge13 = gauge = AttributeGauge(self, size=(204, 10))
|
||||
gauge.SetBackgroundColour(wx.Colour(52, 86, 98))
|
||||
gauge.SetBarColour(wx.Colour(255, 128, 0))
|
||||
gauge.SetValue(100)
|
||||
gauge.SetFractionDigits(1)
|
||||
box.Add(gauge, 0, wx.ALL | wx.CENTER, 10)
|
||||
|
||||
self.value = wx.StaticText(self, label="Text")
|
||||
box.Add(self.value, 0, wx.ALL | wx.CENTER, 5)
|
||||
|
||||
self.btn = wx.Button(self, label="Toggle Timer")
|
||||
box.Add(self.btn, 0, wx.ALL | wx.CENTER, 5)
|
||||
self.btn.Bind(wx.EVT_BUTTON, self.ToggleTimer)
|
||||
|
||||
self.spinCtrl = wx.SpinCtrl(self, min=-10000, max=10000)
|
||||
box.Add(self.spinCtrl, 0, wx.ALL | wx.CENTER, 5)
|
||||
self.spinCtrl.Bind(wx.EVT_SPINCTRL, self.UpdateValue)
|
||||
|
||||
self.m_staticline2 = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL)
|
||||
box.Add(self.m_staticline2, 0, wx.EXPAND, 5)
|
||||
|
||||
self.spinCtrl2 = wx.SpinCtrl(self, min=0, max=10000)
|
||||
box.Add(self.spinCtrl2, 0, wx.ALL | wx.CENTER, 5)
|
||||
self.spinCtrl2.Bind(wx.EVT_SPINCTRL, self.UpdateValue2)
|
||||
|
||||
box.Add(wx.StaticText(self, label="Large Even Pixel Test"), 0, wx.ALL | wx.CENTER, 5)
|
||||
|
||||
guide_lines = False
|
||||
|
||||
self.gauge2 = gauge = AttributeGauge(self, guide_lines=guide_lines, size=(204, 8))
|
||||
gauge.SetBackgroundColour(wx.Colour(52, 86, 98))
|
||||
gauge.SetBarColour(wx.Colour(255, 128, 0))
|
||||
gauge.SetValue(2)
|
||||
gauge.SetFractionDigits(1)
|
||||
box.Add(gauge, 0, wx.ALL | wx.CENTER, 10)
|
||||
|
||||
self.gauge3 = gauge = AttributeGauge(self, guide_lines=guide_lines, size=(204, 8))
|
||||
gauge.SetBackgroundColour(wx.Colour(52, 86, 98))
|
||||
gauge.SetBarColour(wx.Colour(255, 128, 0))
|
||||
gauge.SetValue(-2)
|
||||
gauge.SetFractionDigits(1)
|
||||
box.Add(gauge, 0, wx.ALL | wx.CENTER, 10)
|
||||
|
||||
box.Add(wx.StaticText(self, label="Large Odd Pixel Test"), 0, wx.ALL | wx.CENTER, 5)
|
||||
|
||||
self.gauge4 = gauge = AttributeGauge(self, guide_lines=guide_lines, size=(205, 8))
|
||||
gauge.SetBackgroundColour(wx.Colour(52, 86, 98))
|
||||
gauge.SetBarColour(wx.Colour(255, 128, 0))
|
||||
gauge.SetValue(2)
|
||||
gauge.SetFractionDigits(1)
|
||||
box.Add(gauge, 0, wx.ALL | wx.CENTER, 10)
|
||||
|
||||
self.gauge5 = gauge = AttributeGauge(self, guide_lines=guide_lines, size=(205, 8))
|
||||
gauge.SetBackgroundColour(wx.Colour(52, 86, 98))
|
||||
gauge.SetBarColour(wx.Colour(255, 128, 0))
|
||||
gauge.SetValue(-2)
|
||||
gauge.SetFractionDigits(1)
|
||||
box.Add(gauge, 0, wx.ALL | wx.CENTER, 10)
|
||||
|
||||
box.Add(wx.StaticText(self, label="Small Even Pixel Test"), 0, wx.ALL | wx.CENTER, 5)
|
||||
|
||||
self.gauge6 = gauge = AttributeGauge(self, guide_lines=guide_lines, size=(100, 8))
|
||||
gauge.SetBackgroundColour(wx.Colour(52, 86, 98))
|
||||
gauge.SetBarColour(wx.Colour(255, 128, 0))
|
||||
gauge.SetValue(75)
|
||||
gauge.SetFractionDigits(1)
|
||||
box.Add(gauge, 0, wx.ALL | wx.CENTER, 10)
|
||||
|
||||
self.gauge7 = gauge = AttributeGauge(self, guide_lines=guide_lines, size=(100, 8))
|
||||
gauge.SetBackgroundColour(wx.Colour(52, 86, 98))
|
||||
gauge.SetBarColour(wx.Colour(255, 128, 0))
|
||||
gauge.SetValue(-75)
|
||||
gauge.SetFractionDigits(1)
|
||||
box.Add(gauge, 0, wx.ALL | wx.CENTER, 10)
|
||||
|
||||
box.Add(wx.StaticText(self, label="Small Odd Pixel Test"), 0, wx.ALL | wx.CENTER, 5)
|
||||
|
||||
self.gauge8 = gauge = AttributeGauge(self, guide_lines=guide_lines, max_range=100, size=(101, 8))
|
||||
gauge.SetBackgroundColour(wx.Colour(52, 86, 98))
|
||||
gauge.SetBarColour(wx.Colour(255, 128, 0))
|
||||
gauge.SetValue(1)
|
||||
gauge.SetFractionDigits(1)
|
||||
box.Add(gauge, 0, wx.ALL | wx.CENTER, 10)
|
||||
|
||||
self.gauge9 = gauge = AttributeGauge(self, guide_lines=guide_lines, max_range=100, size=(101, 8))
|
||||
gauge.SetBackgroundColour(wx.Colour(52, 86, 98))
|
||||
gauge.SetBarColour(wx.Colour(255, 128, 0))
|
||||
gauge.SetValue(-1)
|
||||
gauge.SetFractionDigits(1)
|
||||
box.Add(gauge, 0, wx.ALL | wx.CENTER, 10)
|
||||
|
||||
self.SetSizer(box)
|
||||
self.Layout()
|
||||
|
||||
self.animTimer = wx.Timer(self, wx.NewId())
|
||||
self.Bind(wx.EVT_TIMER, self.OnTimer)
|
||||
|
||||
self.animTimer.Start(1000)
|
||||
|
||||
def ToggleTimer(self, evt):
|
||||
if self.animTimer.IsRunning:
|
||||
self.animTimer.Stop()
|
||||
else:
|
||||
self.animTimer.Start(1000)
|
||||
|
||||
def UpdateValue(self, event):
|
||||
if self.animTimer.IsRunning:
|
||||
self.animTimer.Stop()
|
||||
num = self.spinCtrl.GetValue()
|
||||
self.gauge.SetValue(num)
|
||||
self.gauge11.SetValue(num)
|
||||
self.gauge12.SetValue(num)
|
||||
self.gauge13.SetValue(num)
|
||||
self.value.SetLabel(str(num))
|
||||
|
||||
def UpdateValue2(self, event):
|
||||
num = self.spinCtrl2.GetValue()
|
||||
self.gauge2.SetValue(num)
|
||||
self.gauge3.SetValue(num * -1)
|
||||
self.gauge4.SetValue(num)
|
||||
self.gauge5.SetValue(num * -1)
|
||||
self.gauge6.SetValue(num)
|
||||
self.gauge7.SetValue(num * -1)
|
||||
self.gauge8.SetValue(num)
|
||||
self.gauge9.SetValue(num * -1)
|
||||
|
||||
def OnTimer(self, evt):
|
||||
num = random.randint(-100, 100)
|
||||
self.gauge.SetValue(num)
|
||||
self.gauge11.SetValue(num)
|
||||
self.gauge12.SetValue(num)
|
||||
self.gauge13.SetValue(num)
|
||||
self.value.SetLabel(str(num))
|
||||
|
||||
class Frame(wx.Frame):
|
||||
def __init__(self, title, size=(500, 800)):
|
||||
wx.Frame.__init__(self, None, title=title, size=size)
|
||||
self.statusbar = self.CreateStatusBar()
|
||||
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
panel = MyPanel(self, size=size)
|
||||
main_sizer.Add(panel)
|
||||
self.SetSizer(main_sizer)
|
||||
|
||||
app = wx.App(redirect=False) # Error messages go to popup window
|
||||
top = Frame("Test Attribute Bar")
|
||||
top.Show()
|
||||
app.MainLoop()
|
||||
|
||||
66
gui/auxFrame.py
Normal file
66
gui/auxFrame.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# =============================================================================
|
||||
# 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
|
||||
|
||||
|
||||
class AuxiliaryFrame(wx.Frame):
|
||||
|
||||
_instance = None
|
||||
|
||||
def __init__(self, parent, id=None, title=None, pos=None, size=None, style=None, name=None, resizeable=False):
|
||||
baseStyle = wx.FRAME_NO_TASKBAR | wx.FRAME_FLOAT_ON_PARENT | wx.CAPTION | wx.CLOSE_BOX | wx.SYSTEM_MENU
|
||||
if resizeable:
|
||||
baseStyle = baseStyle | wx.RESIZE_BORDER | wx.MAXIMIZE_BOX
|
||||
kwargs = {
|
||||
'parent': parent,
|
||||
'style': baseStyle if style is None else baseStyle | style}
|
||||
if id is not None:
|
||||
kwargs['id'] = id
|
||||
if title is not None:
|
||||
kwargs['title'] = title
|
||||
if pos is not None:
|
||||
kwargs['pos'] = pos
|
||||
if size is not None:
|
||||
kwargs['size'] = size
|
||||
if name is not None:
|
||||
kwargs['name'] = name
|
||||
super().__init__(**kwargs)
|
||||
# Intercept copy-paste actions and do nothing in secondary windows,
|
||||
# otherwise on Mac OS X Cmd-C brings up copy fit dialog
|
||||
if 'wxMac' in wx.PlatformInfo:
|
||||
self.Bind(wx.EVT_MENU, self.OnSuppressedAction, id=wx.ID_COPY)
|
||||
self.Bind(wx.EVT_MENU, self.OnSuppressedAction, id=wx.ID_PASTE)
|
||||
if 'wxMSW' in wx.PlatformInfo:
|
||||
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
|
||||
|
||||
@classmethod
|
||||
def openOne(cls, parent):
|
||||
"""If window is open and alive - raise it, open otherwise"""
|
||||
if not cls._instance:
|
||||
frame = cls(parent)
|
||||
cls._instance = frame
|
||||
frame.Show()
|
||||
else:
|
||||
cls._instance.Raise()
|
||||
|
||||
def OnSuppressedAction(self, event):
|
||||
return
|
||||
@@ -31,7 +31,7 @@ import config
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class BitmapLoader(object):
|
||||
class BitmapLoader:
|
||||
# try:
|
||||
# archive = zipfile.ZipFile(os.path.join(config.pyfaPath, 'imgs.zip'), 'r')
|
||||
# logging.info("Using zipped image files.")
|
||||
|
||||
@@ -99,16 +99,20 @@ class BoosterView(d.Display):
|
||||
event.Skip()
|
||||
|
||||
def fitChanged(self, event):
|
||||
event.Skip()
|
||||
activeFitID = self.mainFrame.getActiveFit()
|
||||
if activeFitID is not None and activeFitID not in event.fitIDs:
|
||||
return
|
||||
|
||||
sFit = Fit.getInstance()
|
||||
fit = sFit.getFit(event.fitID)
|
||||
fit = sFit.getFit(activeFitID)
|
||||
|
||||
self.Parent.Parent.DisablePage(self, not fit or fit.isStructure)
|
||||
|
||||
# Clear list and get out if current fitId is None
|
||||
if event.fitID is None and self.lastFitId is not None:
|
||||
if activeFitID is None and self.lastFitId is not None:
|
||||
self.DeleteAllItems()
|
||||
self.lastFitId = None
|
||||
event.Skip()
|
||||
return
|
||||
|
||||
self.original = fit.boosters if fit is not None else None
|
||||
@@ -116,8 +120,8 @@ class BoosterView(d.Display):
|
||||
if self.boosters is not None:
|
||||
self.boosters.sort(key=lambda booster: booster.slot or 0)
|
||||
|
||||
if event.fitID != self.lastFitId:
|
||||
self.lastFitId = event.fitID
|
||||
if activeFitID != self.lastFitId:
|
||||
self.lastFitId = activeFitID
|
||||
|
||||
item = self.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_DONTCARE)
|
||||
|
||||
@@ -127,7 +131,6 @@ class BoosterView(d.Display):
|
||||
self.unselectAll()
|
||||
|
||||
self.update(self.boosters)
|
||||
event.Skip()
|
||||
|
||||
def addItem(self, event):
|
||||
item = Market.getInstance().getItem(event.itemID, eager='group')
|
||||
@@ -196,8 +199,10 @@ class BoosterView(d.Display):
|
||||
event.Skip()
|
||||
|
||||
def spawnMenu(self, event):
|
||||
selection = self.getSelectedBoosters()
|
||||
clickedPos = self.getRowByAbs(event.Position)
|
||||
self.ensureSelection(clickedPos)
|
||||
|
||||
selection = self.getSelectedBoosters()
|
||||
mainBooster = None
|
||||
if clickedPos != -1:
|
||||
try:
|
||||
@@ -207,9 +212,8 @@ class BoosterView(d.Display):
|
||||
else:
|
||||
if booster in self.original:
|
||||
mainBooster = booster
|
||||
sourceContext = "boosterItem"
|
||||
itemContext = None if mainBooster is None else "Booster"
|
||||
menu = ContextMenu.getMenu(mainBooster, selection, (sourceContext, itemContext))
|
||||
menu = ContextMenu.getMenu(self, mainBooster, selection, ("boosterItem", itemContext), ("boosterItemMisc", itemContext))
|
||||
if menu:
|
||||
self.PopupMenu(menu)
|
||||
|
||||
|
||||
@@ -135,16 +135,20 @@ class CargoView(d.Display):
|
||||
copy=wx.GetMouseState().GetModifiers() == wx.MOD_CONTROL))
|
||||
|
||||
def fitChanged(self, event):
|
||||
event.Skip()
|
||||
activeFitID = self.mainFrame.getActiveFit()
|
||||
if activeFitID is not None and activeFitID not in event.fitIDs:
|
||||
return
|
||||
|
||||
sFit = Fit.getInstance()
|
||||
fit = sFit.getFit(event.fitID)
|
||||
fit = sFit.getFit(activeFitID)
|
||||
|
||||
# self.Parent.Parent.DisablePage(self, not fit or fit.isStructure)
|
||||
|
||||
# Clear list and get out if current fitId is None
|
||||
if event.fitID is None and self.lastFitId is not None:
|
||||
if activeFitID is None and self.lastFitId is not None:
|
||||
self.DeleteAllItems()
|
||||
self.lastFitId = None
|
||||
event.Skip()
|
||||
return
|
||||
|
||||
self.original = fit.cargo if fit is not None else None
|
||||
@@ -152,8 +156,8 @@ class CargoView(d.Display):
|
||||
if self.cargo is not None:
|
||||
self.cargo.sort(key=lambda c: (c.item.group.category.name, c.item.group.name, c.item.name))
|
||||
|
||||
if event.fitID != self.lastFitId:
|
||||
self.lastFitId = event.fitID
|
||||
if activeFitID != self.lastFitId:
|
||||
self.lastFitId = activeFitID
|
||||
|
||||
item = self.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_DONTCARE)
|
||||
|
||||
@@ -164,7 +168,6 @@ class CargoView(d.Display):
|
||||
|
||||
self.populate(self.cargo)
|
||||
self.refresh(self.cargo)
|
||||
event.Skip()
|
||||
|
||||
def onLeftDoubleClick(self, event):
|
||||
row, _ = self.HitTest(event.Position)
|
||||
@@ -184,8 +187,10 @@ class CargoView(d.Display):
|
||||
self.mainFrame.command.Submit(cmd.GuiRemoveCargosCommand(fitID=fitID, itemIDs=itemIDs))
|
||||
|
||||
def spawnMenu(self, event):
|
||||
selection = self.getSelectedCargos()
|
||||
clickedPos = self.getRowByAbs(event.Position)
|
||||
self.ensureSelection(clickedPos)
|
||||
|
||||
selection = self.getSelectedCargos()
|
||||
mainCargo = None
|
||||
if clickedPos != -1:
|
||||
try:
|
||||
@@ -195,9 +200,8 @@ class CargoView(d.Display):
|
||||
else:
|
||||
if cargo in self.original:
|
||||
mainCargo = cargo
|
||||
sourceContext = "cargoItem"
|
||||
itemContext = None if mainCargo is None else Market.getInstance().getCategoryByItem(mainCargo.item).name
|
||||
menu = ContextMenu.getMenu(mainCargo, selection, (sourceContext, itemContext))
|
||||
menu = ContextMenu.getMenu(self, mainCargo, selection, ("cargoItem", itemContext), ("cargoItemMisc", itemContext))
|
||||
if menu:
|
||||
self.PopupMenu(menu)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user