mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Compare commits
905 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9520b76246 | ||
|
|
67183c31da | ||
|
|
54759f1f94 | ||
|
|
0fcf308079 | ||
|
|
1ad27e27ea | ||
|
|
eae5e1af30 | ||
|
|
b268d09432 | ||
|
|
220b83a22d | ||
|
|
83530af35d | ||
|
|
00475d767b | ||
|
|
1a8faff717 | ||
|
|
68e0d43a1d | ||
|
|
0510af8b6b | ||
|
|
c54ac4e21a | ||
|
|
bbc41f4c1a | ||
|
|
8bc8eb0e28 | ||
|
|
86f647cc6d | ||
|
|
2f16916ac4 | ||
|
|
06044dd4c3 | ||
|
|
c46cce01b6 | ||
|
|
d78e570d9d | ||
|
|
f4a1908088 | ||
|
|
23341e69fa | ||
|
|
11fc28ad3a | ||
|
|
9b96b7be26 | ||
|
|
f7ee923cbf | ||
|
|
3d788e3ae8 | ||
|
|
624f2a00fd | ||
|
|
6c3cbbf0de | ||
|
|
96d0b2b7d9 | ||
|
|
855d269420 | ||
|
|
c0748e2a3e | ||
|
|
9f10ecc884 | ||
|
|
31c80dfd02 | ||
|
|
362ce66f80 | ||
|
|
96ee73dd50 | ||
|
|
f82fc3a930 | ||
|
|
cf575fba2a | ||
|
|
96f940bc59 | ||
|
|
98c71c255b | ||
|
|
0e5567ceee | ||
|
|
2832ece316 | ||
|
|
5cfd6c86f3 | ||
|
|
27ef269435 | ||
|
|
8b46ef5e35 | ||
|
|
e243d074ff | ||
|
|
cc39e6579b | ||
|
|
d7eaf1330f | ||
|
|
c44ea0025d | ||
|
|
1216c90891 | ||
|
|
ea80ddc321 | ||
|
|
121ca20edb | ||
|
|
e4586add72 | ||
|
|
32b2e40b37 | ||
|
|
e8190748eb | ||
|
|
d2aa615133 | ||
|
|
756602e682 | ||
|
|
204950e487 | ||
|
|
6290607eb6 | ||
|
|
afad73ac27 | ||
|
|
52fbc75e1c | ||
|
|
5c5af40bd5 | ||
|
|
d1f66ed040 | ||
|
|
6467193c88 | ||
|
|
b2670ba30b | ||
|
|
dcf4da57b3 | ||
|
|
6f825db086 | ||
|
|
afa9122885 | ||
|
|
df8e09a1ad | ||
|
|
5d038e8247 | ||
|
|
ce70952ee0 | ||
|
|
1d16aba678 | ||
|
|
f3156ab51a | ||
|
|
ed9fa37341 | ||
|
|
12aa1ee287 | ||
|
|
7e755f1eb1 | ||
|
|
3da1689767 | ||
|
|
0671e45dde | ||
|
|
c15b4ac83f | ||
|
|
f1eb514295 | ||
|
|
df8f1c4291 | ||
|
|
c32525b542 | ||
|
|
db14b691cc | ||
|
|
052a04ca05 | ||
|
|
ad100a1a17 | ||
|
|
4412a0fa91 | ||
|
|
23a7add2f1 | ||
|
|
040362b333 | ||
|
|
4155afc69a | ||
|
|
cf84c03d8f | ||
|
|
336384cd74 | ||
|
|
da7d3a3f1a | ||
|
|
1231cb145d | ||
|
|
47488a0bdd | ||
|
|
015859e91e | ||
|
|
9c0d114085 | ||
|
|
f907356f93 | ||
|
|
fcb610423b | ||
|
|
3086f64e98 | ||
|
|
5592a834c9 | ||
|
|
1e48affc1c | ||
|
|
f27b6e28e0 | ||
|
|
d0ff271a79 | ||
|
|
9a00a60cd4 | ||
|
|
5254ef3442 | ||
|
|
fe8efbe905 | ||
|
|
48040d7306 | ||
|
|
f3ed25b5b2 | ||
|
|
2d15db1932 | ||
|
|
1fa1f830d4 | ||
|
|
0c0291fc5e | ||
|
|
24c6b62ad4 | ||
|
|
8c9343e0db | ||
|
|
a33da01af6 | ||
|
|
ce8ccb0227 | ||
|
|
68a9dbd141 | ||
|
|
4c3f0a82d9 | ||
|
|
dc85287cd8 | ||
|
|
65f4a2cf86 | ||
|
|
02de6d325a | ||
|
|
7b16967641 | ||
|
|
0e7ea0050f | ||
|
|
ec0e64eaf4 | ||
|
|
b12169f180 | ||
|
|
3a5d0e831c | ||
|
|
903a6c4d5f | ||
|
|
e36f07a3ff | ||
|
|
2d65a038ee | ||
|
|
09617adc56 | ||
|
|
01832b6594 | ||
|
|
30ad71f93b | ||
|
|
4f8ce77e50 | ||
|
|
464ece60da | ||
|
|
cafaea897d | ||
|
|
58820dd6c6 | ||
|
|
8e845e6a27 | ||
|
|
e1b55816e3 | ||
|
|
e95661c6a2 | ||
|
|
c96e0d8f28 | ||
|
|
dfb65facb5 | ||
|
|
8c6ca4de5f | ||
|
|
1c3dee2098 | ||
|
|
170e432008 | ||
|
|
c8e3274c4e | ||
|
|
94105158de | ||
|
|
410821d63e | ||
|
|
2140ed9b67 | ||
|
|
cd77e3ee11 | ||
|
|
780078fec4 | ||
|
|
f8b4c7bcf5 | ||
|
|
de1f8a7abc | ||
|
|
1db84307fe | ||
|
|
ace9f6bbc5 | ||
|
|
8cd2b55e04 | ||
|
|
c404ea83fc | ||
|
|
a544b96f2c | ||
|
|
ffaf905bf6 | ||
|
|
1520c8c3a3 | ||
|
|
fc1a768e4c | ||
|
|
89f159edcb | ||
|
|
b671d520c9 | ||
|
|
889b7299d3 | ||
|
|
631321950e | ||
|
|
af4bba7396 | ||
|
|
c3fea788f3 | ||
|
|
ee8552a4c6 | ||
|
|
2cb1695fb1 | ||
|
|
07aea3985a | ||
|
|
0e0f1ef315 | ||
|
|
3610062976 | ||
|
|
e7c3e41396 | ||
|
|
019141f25b | ||
|
|
0a930fa2ce | ||
|
|
398bb0775d | ||
|
|
71504afd01 | ||
|
|
5744b12699 | ||
|
|
f03f0bf103 | ||
|
|
aaa47191af | ||
|
|
083475b40c | ||
|
|
2660affa63 | ||
|
|
cb988f854d | ||
|
|
d4371ccbf6 | ||
|
|
308805e09b | ||
|
|
34e7093198 | ||
|
|
459940308e | ||
|
|
5fc5e27ee0 | ||
|
|
394a9d05b2 | ||
|
|
1e74f8f124 | ||
|
|
4ef2f6c22a | ||
|
|
5270ad42db | ||
|
|
7e297135be | ||
|
|
60bfde3b61 | ||
|
|
5fc967cfd2 | ||
|
|
e41c923ad4 | ||
|
|
1bb0db920b | ||
|
|
eeaae101d2 | ||
|
|
a1df78c099 | ||
|
|
0be821b972 | ||
|
|
10b3dbf7f8 | ||
|
|
e48bf4a435 | ||
|
|
3c955036d2 | ||
|
|
2570293416 | ||
|
|
f31098419e | ||
|
|
1d4b0608de | ||
|
|
431ec1c382 | ||
|
|
7c8a0fc032 | ||
|
|
da96904016 | ||
|
|
a76fe34f3f | ||
|
|
05e80fa144 | ||
|
|
5db63a2ebf | ||
|
|
0613c8f423 | ||
|
|
db73a3aa27 | ||
|
|
431492fa77 | ||
|
|
942faeeaa3 | ||
|
|
0b40777d68 | ||
|
|
bb3c657158 | ||
|
|
83aac3df44 | ||
|
|
72910b3353 | ||
|
|
587741383c | ||
|
|
6965133a33 | ||
|
|
ed4961cdbc | ||
|
|
f07cc83c5b | ||
|
|
a10b94dcee | ||
|
|
f112f227cc | ||
|
|
807bb16ddf | ||
|
|
4c9dba2fe5 | ||
|
|
e93ebbe78c | ||
|
|
3e1f0bc694 | ||
|
|
4e6b4a9b5d | ||
|
|
ec81e6c659 | ||
|
|
bc157630e9 | ||
|
|
918789cee4 | ||
|
|
dec57fccf9 | ||
|
|
dd408f392b | ||
|
|
164873d3b1 | ||
|
|
7ea144e7c7 | ||
|
|
0d04e0c72e | ||
|
|
f36526b5de | ||
|
|
56e0c901c5 | ||
|
|
592b1a7833 | ||
|
|
cf38113cde | ||
|
|
10e7ee7902 | ||
|
|
f2eec530db | ||
|
|
9c50c062e9 | ||
|
|
a173fa7e5b | ||
|
|
63785174db | ||
|
|
ffe0ede285 | ||
|
|
d1b484f560 | ||
|
|
3ef6ae20c7 | ||
|
|
f3b1964e06 | ||
|
|
a9e784c0e3 | ||
|
|
3012fb8661 | ||
|
|
1c5445c254 | ||
|
|
db3670b759 | ||
|
|
ea09b6be32 | ||
|
|
881228d273 | ||
|
|
c75bf23701 | ||
|
|
020281bf80 | ||
|
|
ef77637c9a | ||
|
|
15e6f81bb5 | ||
|
|
8b03bc496d | ||
|
|
add81472c6 | ||
|
|
0e1f2e4185 | ||
|
|
1a628888f7 | ||
|
|
2c2573240a | ||
|
|
75c29dd907 | ||
|
|
f1dc431d2f | ||
|
|
db9cf441fb | ||
|
|
a06e9d0ec7 | ||
|
|
dd7e4c908e | ||
|
|
a4671571bc | ||
|
|
5470cb416c | ||
|
|
6d0dbd4d1c | ||
|
|
266e69a6ce | ||
|
|
8f384b35dc | ||
|
|
e02698d8a8 | ||
|
|
d2fa027cdd | ||
|
|
4c7aa8ddde | ||
|
|
9d6f1552c8 | ||
|
|
06d3805946 | ||
|
|
caf624832f | ||
|
|
cc90627ee5 | ||
|
|
aae4c905b9 | ||
|
|
64d60e5ccf | ||
|
|
0eb06f9af5 | ||
|
|
80b3af18df | ||
|
|
3d778a1ce0 | ||
|
|
09d7a4babf | ||
|
|
94a14f40cb | ||
|
|
6d9478e0d4 | ||
|
|
1c36d9ec75 | ||
|
|
772a15bab5 | ||
|
|
98f0c93012 | ||
|
|
5e2f86de96 | ||
|
|
94baaecf8c | ||
|
|
057f3fe53b | ||
|
|
ce4c73098d | ||
|
|
eb06a9a9e8 | ||
|
|
a6b1805290 | ||
|
|
08e55d9e43 | ||
|
|
27c8f01853 | ||
|
|
84c4b992e0 | ||
|
|
b5225f03a1 | ||
|
|
add26ab18e | ||
|
|
aef931fc3a | ||
|
|
2091fdbb27 | ||
|
|
e959933861 | ||
|
|
eaa73c84ae | ||
|
|
3147ae6e5c | ||
|
|
7b9bdf84dc | ||
|
|
9e370e1d8d | ||
|
|
e85d1e8ca2 | ||
|
|
696e2dbbe8 | ||
|
|
6a5097659b | ||
|
|
b124c6cc61 | ||
|
|
4de7b1c923 | ||
|
|
4711473b64 | ||
|
|
6ae32b13b8 | ||
|
|
5c56e7f64e | ||
|
|
5fdf38c663 | ||
|
|
11a9d67b91 | ||
|
|
216d9bc048 | ||
|
|
3bb6c43aca | ||
|
|
af9416e225 | ||
|
|
73feccde9d | ||
|
|
ff2ec07d83 | ||
|
|
4aacc68f4a | ||
|
|
69c6f8f2f2 | ||
|
|
4c3b16e1da | ||
|
|
2b780e3d69 | ||
|
|
a68bcb9ea1 | ||
|
|
0770fafbcb | ||
|
|
169ef26329 | ||
|
|
168ba7bd1e | ||
|
|
59f98a4c16 | ||
|
|
9bd6c6c404 | ||
|
|
96e8208e3c | ||
|
|
44658f8eec | ||
|
|
dae57abeff | ||
|
|
87ca233f20 | ||
|
|
126d70792f | ||
|
|
9d2345ea3a | ||
|
|
7a84181bbc | ||
|
|
a7d091c142 | ||
|
|
be554c54b3 | ||
|
|
49211dae4f | ||
|
|
73d2f8fda1 | ||
|
|
2cba1f4124 | ||
|
|
f5bb653ad8 | ||
|
|
d35962b784 | ||
|
|
f63939ce3c | ||
|
|
8f8ee8c48e | ||
|
|
7e8cbe0de2 | ||
|
|
8a47dce4b5 | ||
|
|
242d1a14a7 | ||
|
|
1f21ed0c1d | ||
|
|
d44218ba55 | ||
|
|
aa26a2fa79 | ||
|
|
ced69c98fc | ||
|
|
4082cad3cf | ||
|
|
fd42b03d78 | ||
|
|
98360c8388 | ||
|
|
daee4d96d7 | ||
|
|
3e488a738e | ||
|
|
26bd7629f0 | ||
|
|
9b857d7b0e | ||
|
|
1388985048 | ||
|
|
ac6c8c3e14 | ||
|
|
171f0722dc | ||
|
|
3a1d90ce6a | ||
|
|
818803254b | ||
|
|
a9a4a1fed4 | ||
|
|
ea9c459942 | ||
|
|
16162f2698 | ||
|
|
a8c9a81ccb | ||
|
|
9cf52ab11d | ||
|
|
86a93efc71 | ||
|
|
c262bb3681 | ||
|
|
86e4f45152 | ||
|
|
bc3ec4ffb9 | ||
|
|
5a25b97a9e | ||
|
|
9e2c5c06b9 | ||
|
|
5b4ed979b1 | ||
|
|
d29f5a26a1 | ||
|
|
97c65cfefa | ||
|
|
9dd25e933e | ||
|
|
8796d629e8 | ||
|
|
9dba85e95b | ||
|
|
583c87b2e3 | ||
|
|
4405764006 | ||
|
|
ba25f6a8df | ||
|
|
d2547def51 | ||
|
|
532515fad7 | ||
|
|
f79e96a474 | ||
|
|
5aca992140 | ||
|
|
b1efaf6448 | ||
|
|
ecb81ec32d | ||
|
|
242ef30469 | ||
|
|
3d1a5bea67 | ||
|
|
dcc424d7e1 | ||
|
|
f12a1350ce | ||
|
|
d2c45c97fe | ||
|
|
3bdda3b852 | ||
|
|
d604905f52 | ||
|
|
5bb70626d2 | ||
|
|
8eb30445c2 | ||
|
|
1b757ec544 | ||
|
|
65e529aa55 | ||
|
|
e0d4a031bb | ||
|
|
ef0e6bb4f5 | ||
|
|
8011d711af | ||
|
|
3b360cfcf5 | ||
|
|
e5b06e6ba4 | ||
|
|
8bc902fe64 | ||
|
|
0ecbd81a20 | ||
|
|
dd6267c5ed | ||
|
|
ecf11363f2 | ||
|
|
9e9ea09f70 | ||
|
|
cc6fbc0379 | ||
|
|
5ad4dc51fa | ||
|
|
140410f1ed | ||
|
|
b2ae10ce6f | ||
|
|
a658e41c54 | ||
|
|
2b65d2f800 | ||
|
|
7ea1569f22 | ||
|
|
8d11feae4a | ||
|
|
1c9b4b2317 | ||
|
|
9cca4e8244 | ||
|
|
e280763a39 | ||
|
|
b7ad1048bd | ||
|
|
98b49d5fca | ||
|
|
4423391287 | ||
|
|
ad87182cf9 | ||
|
|
510e23ebba | ||
|
|
5bb5294826 | ||
|
|
3de59064de | ||
|
|
da64cdbab7 | ||
|
|
e9d0273056 | ||
|
|
f405ffdfe2 | ||
|
|
8dcf25d9ee | ||
|
|
e88a4e1c51 | ||
|
|
c24fba0ba4 | ||
|
|
44daa002ab | ||
|
|
7d898f8b48 | ||
|
|
6194ec7301 | ||
|
|
6702a91802 | ||
|
|
dc2d541b50 | ||
|
|
141fc642d5 | ||
|
|
0d56c271df | ||
|
|
75dd67a145 | ||
|
|
eb841f1bf4 | ||
|
|
9b54414525 | ||
|
|
319b2f768a | ||
|
|
1752465c9b | ||
|
|
b0d10f07de | ||
|
|
ed7a358699 | ||
|
|
4c05837b61 | ||
|
|
16239d4089 | ||
|
|
69b828ddf4 | ||
|
|
93da86eed3 | ||
|
|
655ed23d79 | ||
|
|
209f2c7b67 | ||
|
|
9b48acfe28 | ||
|
|
ecc7b603b9 | ||
|
|
0f0dc97376 | ||
|
|
b2c66c10dc | ||
|
|
f9e7772e09 | ||
|
|
2c270306a1 | ||
|
|
08ecbb14f1 | ||
|
|
d9788148c2 | ||
|
|
8918d4e64f | ||
|
|
4b51c23c43 | ||
|
|
6cfe948ef2 | ||
|
|
068165c588 | ||
|
|
b09d095723 | ||
|
|
19f6bafff1 | ||
|
|
6238830a20 | ||
|
|
eb6701ab89 | ||
|
|
ff311bd6fe | ||
|
|
597da1c84d | ||
|
|
206f0c5196 | ||
|
|
739405d716 | ||
|
|
3626cdc086 | ||
|
|
8e8bc3f907 | ||
|
|
eb5cceff16 | ||
|
|
eb5062579f | ||
|
|
9a0ddd683c | ||
|
|
02568eaeee | ||
|
|
7e6810a299 | ||
|
|
2f33277a49 | ||
|
|
c7644ba9e5 | ||
|
|
d57521c03d | ||
|
|
db12ac4f54 | ||
|
|
3cda9fb36b | ||
|
|
b8b39bf2fe | ||
|
|
65a62b3951 | ||
|
|
ec7982f181 | ||
|
|
81ad559ca1 | ||
|
|
081c981c86 | ||
|
|
e704d2aa53 | ||
|
|
59fa77e10b | ||
|
|
2b62f5919f | ||
|
|
0b8aa9b96e | ||
|
|
1a6d73f055 | ||
|
|
9f5db20195 | ||
|
|
24fc438fda | ||
|
|
02161c02cf | ||
|
|
07cc85f6fa | ||
|
|
d84a0ebc78 | ||
|
|
d0a1a01b12 | ||
|
|
1df042fcc6 | ||
|
|
e9a14f066a | ||
|
|
4762b490c0 | ||
|
|
3bda2929aa | ||
|
|
4ca0137a24 | ||
|
|
824312e19d | ||
|
|
06960db5e5 | ||
|
|
eceec83a01 | ||
|
|
a9f968f7cc | ||
|
|
7d1e3c94f5 | ||
|
|
6dc5296130 | ||
|
|
22f7767b99 | ||
|
|
c27f3e67c0 | ||
|
|
9a6d912a30 | ||
|
|
6e3f66437e | ||
|
|
b27141cb1e | ||
|
|
af83088dfd | ||
|
|
e6683c0237 | ||
|
|
e5f7b577ca | ||
|
|
08de46a3b6 | ||
|
|
9c820f12cf | ||
|
|
b45821de52 | ||
|
|
192fb2087f | ||
|
|
c22f7cbff3 | ||
|
|
38e62e7636 | ||
|
|
78fdc1bcfc | ||
|
|
7f7821f878 | ||
|
|
cc7ff3f1c2 | ||
|
|
00356ea3ec | ||
|
|
7eb2c59b86 | ||
|
|
3c0f0b28ea | ||
|
|
8a57e8e5f3 | ||
|
|
e6fc0a9758 | ||
|
|
31630b4d42 | ||
|
|
b2315efb6c | ||
|
|
9a55b4de1c | ||
|
|
d190d23655 | ||
|
|
f8e5a2e38d | ||
|
|
ec4f19e930 | ||
|
|
7f74203013 | ||
|
|
dc52457d80 | ||
|
|
a9950d4b80 | ||
|
|
6577d6a417 | ||
|
|
c6bd1e0cc3 | ||
|
|
dd1f080f97 | ||
|
|
68bdb86c71 | ||
|
|
5cd0859004 | ||
|
|
1d02917c52 | ||
|
|
6a3a33170a | ||
|
|
50e3f89199 | ||
|
|
4c8d99e40a | ||
|
|
163841f8b0 | ||
|
|
bf3d34ff5d | ||
|
|
b501009285 | ||
|
|
c399002d1d | ||
|
|
8c9f62a2fb | ||
|
|
f88c103db5 | ||
|
|
bb42a1f63a | ||
|
|
58b4e5284e | ||
|
|
8e5b6b1e0c | ||
|
|
030762a1dd | ||
|
|
8df77dec2a | ||
|
|
28efecc03a | ||
|
|
25e2da2b91 | ||
|
|
46f9cb62c1 | ||
|
|
b801136145 | ||
|
|
0fd0e2c3c2 | ||
|
|
bfe008775d | ||
|
|
7802ab992e | ||
|
|
a1fa58326c | ||
|
|
db93e56d6d | ||
|
|
9775d3b97c | ||
|
|
3d6da6ae3d | ||
|
|
8562c48753 | ||
|
|
12d0632415 | ||
|
|
0ab07c3878 | ||
|
|
b3f9cdfa74 | ||
|
|
0f53d00d25 | ||
|
|
16b6c00c0d | ||
|
|
8482829f94 | ||
|
|
881e9a4301 | ||
|
|
4bc7b41828 | ||
|
|
ee1a0cc9f1 | ||
|
|
13509bbda5 | ||
|
|
692ceefadb | ||
|
|
c09a5b68d5 | ||
|
|
c947ac0ef1 | ||
|
|
3b65a0ab88 | ||
|
|
b07aae3d6e | ||
|
|
562be59765 | ||
|
|
179936146f | ||
|
|
6108727841 | ||
|
|
c4d2f0e833 | ||
|
|
2cc8e90e2f | ||
|
|
75ddfb9e17 | ||
|
|
3a46bdcd1e | ||
|
|
2ad3bff988 | ||
|
|
88b3264fe3 | ||
|
|
cbd90a5e36 | ||
|
|
0f9781ff30 | ||
|
|
0f612ac9d9 | ||
|
|
2b7113ec2a | ||
|
|
a8bf817b5a | ||
|
|
9d9295ed2d | ||
|
|
f203a5cf7a | ||
|
|
b61f625828 | ||
|
|
779f66670f | ||
|
|
86ef189da4 | ||
|
|
d3e1a1df92 | ||
|
|
150a4dde49 | ||
|
|
48861c53da | ||
|
|
85f4f66f21 | ||
|
|
a27663e4b6 | ||
|
|
4c455426e9 | ||
|
|
04e60fb08d | ||
|
|
16377fe563 | ||
|
|
3bf77042fd | ||
|
|
0ab7dd2f15 | ||
|
|
61bfebb6fa | ||
|
|
ea6591463f | ||
|
|
e34a8c7875 | ||
|
|
557b441d13 | ||
|
|
c11575fcff | ||
|
|
cbd230862f | ||
|
|
e69708ada9 | ||
|
|
c795ed01a0 | ||
|
|
79d79645b0 | ||
|
|
4b75913420 | ||
|
|
4ef95a1939 | ||
|
|
51a82f431d | ||
|
|
5891fb7441 | ||
|
|
0ffe2f11bc | ||
|
|
40a01218dd | ||
|
|
e81d4068be | ||
|
|
4043e2452f | ||
|
|
71965e7183 | ||
|
|
9fc34a56a3 | ||
|
|
19523e35cf | ||
|
|
015aa98f4c | ||
|
|
c34ff3eea0 | ||
|
|
2ce776ff55 | ||
|
|
55bb54b1c2 | ||
|
|
4f7169da2a | ||
|
|
d005e03218 | ||
|
|
b1cc2cace1 | ||
|
|
e9f39fec2c | ||
|
|
f587132da3 | ||
|
|
79515c6dfd | ||
|
|
8b4a93ad6b | ||
|
|
ae719e520f | ||
|
|
215f15fb48 | ||
|
|
09ddc3c3c6 | ||
|
|
f76fe6eeec | ||
|
|
243eed27bb | ||
|
|
31a9391fd0 | ||
|
|
377d0a2fad | ||
|
|
3cf4729f64 | ||
|
|
f1eb8f9a87 | ||
|
|
6bcd8a6910 | ||
|
|
07eeb3c627 | ||
|
|
a7e4cad3b7 | ||
|
|
50808c3c06 | ||
|
|
e86e49cb6f | ||
|
|
61fe67660f | ||
|
|
327c934ed3 | ||
|
|
d2fd7bbb4e | ||
|
|
e416e07366 | ||
|
|
0a7b885626 | ||
|
|
c5897950b6 | ||
|
|
64b1410de8 | ||
|
|
55b173ec10 | ||
|
|
d4234dd7b3 | ||
|
|
d051774464 | ||
|
|
a9a56bfcc4 | ||
|
|
185911942a | ||
|
|
dd2fb432f2 | ||
|
|
bd7f7615b0 | ||
|
|
4bbe28838d | ||
|
|
60fafd262d | ||
|
|
52b25821af | ||
|
|
6ee74cf079 | ||
|
|
6b1b612c4b | ||
|
|
a56aa7a766 | ||
|
|
ab32c44b9d | ||
|
|
a7d65866e8 | ||
|
|
36ef246fcd | ||
|
|
cbaf509375 | ||
|
|
c4bc515e19 | ||
|
|
ae2f1dddf4 | ||
|
|
0784efc1c7 | ||
|
|
7306427487 | ||
|
|
ff096b7d8d | ||
|
|
a49a1e9461 | ||
|
|
e816e6990a | ||
|
|
e549a923d5 | ||
|
|
2edc0a7305 | ||
|
|
200812a39b | ||
|
|
a4d8c14579 | ||
|
|
969f0e26c7 | ||
|
|
49ba40aaf3 | ||
|
|
5cfeee7b2a | ||
|
|
100eafcffd | ||
|
|
c0a887b258 | ||
|
|
cafc6b5d30 | ||
|
|
3ab0f2a554 | ||
|
|
3d8956434c | ||
|
|
3d435c7821 | ||
|
|
76bab86e67 | ||
|
|
00760b831e | ||
|
|
f4e8e30cb1 | ||
|
|
e15aca8c54 | ||
|
|
93ab146cf5 | ||
|
|
72d1c105a7 | ||
|
|
5b60d64437 | ||
|
|
2808d0f72c | ||
|
|
64e425012c | ||
|
|
85db8c908e | ||
|
|
b6449a7056 | ||
|
|
91a9a9b88f | ||
|
|
b36215b808 | ||
|
|
ced7aa4d72 | ||
|
|
08930f4a9a | ||
|
|
921b1ee9fd | ||
|
|
713cd98d10 | ||
|
|
cf9cc9ba33 | ||
|
|
39c80cb974 | ||
|
|
cf9538f7df | ||
|
|
3d15505347 | ||
|
|
11a0713e50 | ||
|
|
dd0217a220 | ||
|
|
d870099f41 | ||
|
|
c6a8673b5e | ||
|
|
cc3bef7937 | ||
|
|
0bdb989a2c | ||
|
|
765c85b639 | ||
|
|
7b9ddb1ece | ||
|
|
5fa7757ae1 | ||
|
|
064d17aab0 | ||
|
|
47ee0e5340 | ||
|
|
6fd60d150e | ||
|
|
946d578ffb | ||
|
|
95bac8ec17 | ||
|
|
91cfff58d9 | ||
|
|
585b3e74a0 | ||
|
|
4ff31ea86d | ||
|
|
231a8609a1 | ||
|
|
a6104968e8 | ||
|
|
77060031da | ||
|
|
51a4b6603d | ||
|
|
4e4eeff9ec | ||
|
|
4b092e0763 | ||
|
|
aa641e1ff6 | ||
|
|
d0ff363bb5 | ||
|
|
a97b3fee99 | ||
|
|
51462673a2 | ||
|
|
3c139d8038 | ||
|
|
b99a719d1c | ||
|
|
992beb5f45 | ||
|
|
ae13a422c3 | ||
|
|
0b8fa9099c | ||
|
|
13e7e976c3 | ||
|
|
529841bfe4 | ||
|
|
c0e26b3b7f | ||
|
|
a869c2a758 | ||
|
|
60fde46249 | ||
|
|
27b1c92815 | ||
|
|
d965f90bb4 | ||
|
|
4ad87aef3e | ||
|
|
daa3259e59 | ||
|
|
545210b35d | ||
|
|
1dd9089477 | ||
|
|
0faa27dca5 | ||
|
|
1db232417f | ||
|
|
8253d41d37 | ||
|
|
d8b0283efe | ||
|
|
8dfb05b0d1 | ||
|
|
aad48c0b78 | ||
|
|
ecf659097a | ||
|
|
15fafc20df | ||
|
|
7d0234820c | ||
|
|
9f271cdb91 | ||
|
|
7b2424c746 | ||
|
|
5c9a5a5e35 | ||
|
|
b6f0ec4da0 | ||
|
|
c95e746687 | ||
|
|
d58f06be6d | ||
|
|
f752d422df | ||
|
|
1322c3ca54 | ||
|
|
dc02c6f857 | ||
|
|
3d781d89de | ||
|
|
9b54730191 | ||
|
|
af4cf03335 | ||
|
|
31fb340ac8 | ||
|
|
975471e942 | ||
|
|
a24925bc45 | ||
|
|
3d8eed7334 | ||
|
|
6e7844982d | ||
|
|
1835743af0 | ||
|
|
3831f4f309 | ||
|
|
226efe1151 | ||
|
|
db620a9056 | ||
|
|
2011a5f555 | ||
|
|
6c9115f517 | ||
|
|
17b9d9c3d5 | ||
|
|
2b63f45392 | ||
|
|
c830a6e7c0 | ||
|
|
59f6a3dcba | ||
|
|
09362cade4 | ||
|
|
85cec06098 | ||
|
|
4c201f25ba | ||
|
|
de0802a3c2 | ||
|
|
7908858433 | ||
|
|
595c468ab2 | ||
|
|
87975b9aad | ||
|
|
b508ef9aec | ||
|
|
18a421f78e | ||
|
|
2b5c415159 | ||
|
|
a3575995bd | ||
|
|
04c32073aa | ||
|
|
4f742b51f7 | ||
|
|
e7b84e18d1 | ||
|
|
6c2e14c6d9 | ||
|
|
dc504005a1 | ||
|
|
359a579e65 | ||
|
|
473e70c44b | ||
|
|
9be06d9601 | ||
|
|
9edb4ecc45 | ||
|
|
ffac036266 | ||
|
|
687ae88685 | ||
|
|
4d8031ef96 | ||
|
|
a60d1dce19 | ||
|
|
d1b8e20fde | ||
|
|
9d9740cdb3 | ||
|
|
bd4e2d7189 | ||
|
|
8f883d51d7 | ||
|
|
fbbc2536a1 | ||
|
|
6f4a12658d | ||
|
|
1bcecd5b3a | ||
|
|
7653866ddb | ||
|
|
ecaa68f32a | ||
|
|
bd0deed16a | ||
|
|
d890ef0808 | ||
|
|
8520764308 | ||
|
|
1e3f6baceb | ||
|
|
00bcd547d3 | ||
|
|
a30fb0f368 | ||
|
|
087a38d105 | ||
|
|
bc92a2ade3 | ||
|
|
bc27dc4621 | ||
|
|
94c1455249 | ||
|
|
4be122e11a | ||
|
|
f7c6f14220 | ||
|
|
c7ec3283bd | ||
|
|
1d6c8a2453 | ||
|
|
730b982247 | ||
|
|
8aea905640 | ||
|
|
3809e28a6a | ||
|
|
19984636e3 | ||
|
|
f4c5c96fba | ||
|
|
2e889325e1 | ||
|
|
a787cd4f70 | ||
|
|
a975e2c2dc | ||
|
|
ddc5709a17 | ||
|
|
34fae51189 | ||
|
|
5621649f10 | ||
|
|
f4a239aaad | ||
|
|
46e7f89da9 | ||
|
|
b73a18b7b9 | ||
|
|
4a859e00ae | ||
|
|
acf035697f | ||
|
|
04993ddf1b | ||
|
|
93265040f6 | ||
|
|
d517b1cdeb | ||
|
|
5687c15c21 | ||
|
|
c4effe88ee | ||
|
|
ab76d18621 | ||
|
|
f7e618bd0f | ||
|
|
73f7d94bda | ||
|
|
33a2c27f5e | ||
|
|
17533399b3 | ||
|
|
cc713f089a | ||
|
|
84c20e8d81 | ||
|
|
c126d1dff7 | ||
|
|
f500ec916e | ||
|
|
842c2f8aaf | ||
|
|
db993697f0 | ||
|
|
c8f78c1bf4 | ||
|
|
9135a91bb9 | ||
|
|
94d755e9a0 | ||
|
|
d09cb70948 | ||
|
|
86e32b1990 | ||
|
|
676e4dcebf | ||
|
|
d6ab4e9891 | ||
|
|
db95fc17dc | ||
|
|
8402e5d9bb |
3
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -31,9 +31,8 @@ body:
|
|||||||
If the bug was found in a test/development build, select "Test build" or "Development build"
|
If the bug was found in a test/development build, select "Test build" or "Development build"
|
||||||
and provide a link to the applicable build in the field below.
|
and provide a link to the applicable build in the field below.
|
||||||
options:
|
options:
|
||||||
- v1.2.1
|
- v1.4.1
|
||||||
- Test build
|
- Test build
|
||||||
- Development build
|
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Test/Development build
|
label: Test/Development build
|
||||||
|
|||||||
3
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
3
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
@@ -39,9 +39,8 @@ body:
|
|||||||
If the bug was found in a test/development build, select "Test build" or "Development build"
|
If the bug was found in a test/development build, select "Test build" or "Development build"
|
||||||
and provide a link to the applicable build in the field below.
|
and provide a link to the applicable build in the field below.
|
||||||
options:
|
options:
|
||||||
- 1.0.0
|
- 1.4.1
|
||||||
- Test build
|
- Test build
|
||||||
- Development build
|
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Test/Development build
|
label: Test/Development build
|
||||||
|
|||||||
4
.github/actions/build-app/action.yaml
vendored
4
.github/actions/build-app/action.yaml
vendored
@@ -1,5 +1,5 @@
|
|||||||
name: Build Liberation package
|
name: Build Retribution package
|
||||||
description: Assembles the full Liberation application.
|
description: Assembles the full Retribution application.
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
name: Liberation JS set-up
|
name: Retribution JS set-up
|
||||||
description: Sets up the Liberation Javascript environment.
|
description: Sets up the Retribution Javascript environment.
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
node-version: "16"
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
name: Liberation Python set-up
|
name: Retribution Python set-up
|
||||||
description: Sets up the Liberation Python environment.
|
description: Sets up the Retribution Python environment.
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.11"
|
||||||
cache: pip
|
cache: pip
|
||||||
|
|
||||||
- name: Install environment
|
- name: Install environment
|
||||||
|
|||||||
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
Compress-Archive -Path .\dist\dcs-retribution\ -DestinationPath
|
Compress-Archive -Path .\dist\dcs-retribution\ -DestinationPath
|
||||||
dist\dcs-retribution.zip
|
dist\dcs-retribution.zip
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dcs-retribution
|
name: dcs-retribution
|
||||||
path: dist/dcs-retribution.zip
|
path: dist/dcs-retribution.zip
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
include_image: true
|
include_image: true
|
||||||
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
||||||
username: GitHub Build Notifier
|
username: GitHub Build Notifier
|
||||||
if: startsWith(github.ref_name, 'test')
|
if: github.repository == 'dcs-retribution/dcs-retribution' && startsWith(github.ref_name, 'test')
|
||||||
|
|
||||||
- name: Send status to Discord
|
- name: Send status to Discord
|
||||||
uses: stegzilla/discord-notify@v2
|
uses: stegzilla/discord-notify@v2
|
||||||
@@ -60,4 +60,4 @@ jobs:
|
|||||||
include_image: true
|
include_image: true
|
||||||
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
||||||
username: GitHub Build Notifier
|
username: GitHub Build Notifier
|
||||||
if: github.ref_name == 'dev'
|
if: github.repository == 'dcs-retribution/dcs-retribution' && github.ref_name == 'dev'
|
||||||
|
|||||||
7
.github/workflows/lint.yml
vendored
7
.github/workflows/lint.yml
vendored
@@ -7,19 +7,18 @@ jobs:
|
|||||||
name: Black
|
name: Black
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v5
|
||||||
- uses: psf/black@stable
|
- uses: psf/black@stable
|
||||||
with:
|
with:
|
||||||
src: "."
|
src: "."
|
||||||
options: "--check"
|
options: "--check"
|
||||||
version: "23.9.1"
|
|
||||||
|
|
||||||
mypy:
|
mypy:
|
||||||
name: Type checking
|
name: Type checking
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
release: true
|
release: true
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dcs-retribution
|
name: dcs-retribution
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
needs: [build]
|
needs: [build]
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v2
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dcs-retribution
|
name: dcs-retribution
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -5,7 +5,7 @@ jobs:
|
|||||||
name: Python tests
|
name: Python tests
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
name: Typescript tests
|
name: Typescript tests
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up JS environment
|
- name: Set up JS environment
|
||||||
uses: ./.github/actions/setup-liberation-js
|
uses: ./.github/actions/setup-liberation-js
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,5 +22,6 @@ env/
|
|||||||
|
|
||||||
/logs/
|
/logs/
|
||||||
/resources/logging.yaml
|
/resources/logging.yaml
|
||||||
|
/resources/plugins/pretense/pretense_output.lua
|
||||||
|
|
||||||
*.psd
|
*.psd
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 22.6.0
|
rev: 24.8.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python3
|
language_version: python3
|
||||||
@@ -68,7 +68,7 @@ Our plans for future releases can be found on our
|
|||||||
[Projects page](https://github.com/dcs-retribution/dcs-retribution/projects).
|
[Projects page](https://github.com/dcs-retribution/dcs-retribution/projects).
|
||||||
Each planned release has a Project, and the page for that project has columns for to do,
|
Each planned release has a Project, and the page for that project has columns for to do,
|
||||||
in progress, and done. Items in the Done column are in the
|
in progress, and done. Items in the Done column are in the
|
||||||
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Betas)
|
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Preview-builds)
|
||||||
for that release. Items in the To do column are planned to be added to that release.
|
for that release. Items in the To do column are planned to be added to that release.
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
@@ -90,5 +90,7 @@ Excellent lua scripts DCS Liberation/Retribution uses as plugins:
|
|||||||
|
|
||||||
* For the JTAC feature, DCS Retribution embeds Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
|
* For the JTAC feature, DCS Retribution embeds Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
|
||||||
* Walder's [Skynet-IADS](https://github.com/walder/Skynet-IADS) is used for Integrated Air Defense System.
|
* Walder's [Skynet-IADS](https://github.com/walder/Skynet-IADS) is used for Integrated Air Defense System.
|
||||||
|
* Carstens Arty Spotter https://www.digitalcombatsimulator.com/en/files/3339128/ is an amazing force multiplyer to drop the hammer on enemies.
|
||||||
|
* MBot's [Call Artillery Script](https://forum.dcs.world/topic/310506-call-artillery-script/) uses in-map artillery and forward observers to enable artillery fire missions.
|
||||||
|
|
||||||
Please also show some support to these projects !
|
Please also show some support to these projects !
|
||||||
|
|||||||
121
changelog.md
121
changelog.md
@@ -1,3 +1,123 @@
|
|||||||
|
# Retribution v1.5.0
|
||||||
|
|
||||||
|
## Features/Improvements
|
||||||
|
* **[Campaigns]** Ability to define invisible FOBs
|
||||||
|
* **[Campaigns]** Ability to define influence zones for Control Points
|
||||||
|
* **[Plugins]** Improvements to AI support for EW Script 2.0
|
||||||
|
* **[Plugins]** Add moose as a base plugin
|
||||||
|
* **[Config]** New preference setting to trigger the first-start window on every start (could help in scenarios multiple Retribution instances need to run concurrently)
|
||||||
|
* **[Modding]** Update Grinelli Designs F-22A Mod to 2.0.0 (May 2025)
|
||||||
|
* **[Campaign Design]** Added support for Germany Cold War terrain by Ugra Media
|
||||||
|
* **[Options]** New option to control EPLRS-task injection in mission generator
|
||||||
|
* **[Modding]** Add Tornado F3 ADV (v1.0, requires FC3 fix)
|
||||||
|
* **[Modding]** Add Military Aircraft Mod 1.7.2 (See VSN Discord)
|
||||||
|
* **[Payload Editor]** Add Sniper POD
|
||||||
|
* **[Modding]** Update SU-30 mod to v2.8.04 Beta + CWS 3.72
|
||||||
|
* **[Plugins]** EW Script - DEAD added to applicable flight types.
|
||||||
|
* **[Plugins]** EW Script - Offensive Jamming restricted to aircraft with ALQ99/249 pods, or "has_built_in_jamming: true" in aircarft yaml (AI and Player)
|
||||||
|
* **[Modding]** Update to CJS Super Hornet Mod v2.4.4
|
||||||
|
* **[Engine]** Support for DCS 2.9.20.15010, including MiG-29A Fulcrum support.
|
||||||
|
* **[Campaigns]** Ability to define neutral FOBs and Airfields
|
||||||
|
* **[Modding]** Add Airboss Moose Module
|
||||||
|
* **[AirWing]** Use aircraft display names for easier differentiation between modules
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
* **[Flight Plans]** Fixed bugs wrt planning escort flights
|
||||||
|
* **[Flight Plans]** Added AntiShipStrike as a fallback task for OCA/Aircraft to fix a bug where the S-3B could not do OCA/Aircraft
|
||||||
|
* **[Squadrons]** Fixed a bug where loading an air wing config would not properly load all squadrons
|
||||||
|
* **[Flight Plans]** Fixed a bug where SEAD flights would fire one ARM and RTB
|
||||||
|
* **[Plugins]** EW Script - Fix radar detection routine.
|
||||||
|
* **[Campaign]** Fixed a bug where sinking a destroyer in a carrier group would cause squadrons to be removed from the carrier
|
||||||
|
* **[Engine]** Fixed a bug with transfers to Helipads
|
||||||
|
* **[Engine]** Fixed a bug with parking allocation
|
||||||
|
* **[Flight Plans]** Fixed a bug where divert airfield was broken for opfor
|
||||||
|
* **[Engine]** Fixed a bug with state.json loading wrt transfers
|
||||||
|
* **[Engine]** Fixed a bug wrt pretense generation and moose script conflicts
|
||||||
|
* **[Engine]** Fixed a bug where frontline debriefing was not properly calculated
|
||||||
|
* **[Mission Generation]** Fixed an issue where blue and red units spawned next to each other on frontlines
|
||||||
|
|
||||||
|
# Retribution v1.4.1 (hotfix)
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
* **[Plugins]** Avoid injecting jamming-tasks for player-controller aircraft when EW-Jamming plugin is disabled
|
||||||
|
|
||||||
|
|
||||||
|
# Retribution v1.4.0
|
||||||
|
|
||||||
|
## Features/Improvements
|
||||||
|
* **[Payload Editor]** Ability to configure liveries on flight/flight-member level
|
||||||
|
* **[Factions]** Support for definitions in yml/yaml format
|
||||||
|
* **[Campaigns/Factions]** Support for inline recommended faction in campaign's yaml file
|
||||||
|
* **[Squadrons]** Ability to define a livery-set for each squadron from which Retribution will randomly choose during mission generation
|
||||||
|
* **[Modding]** Updated support for F/A-18E/F/G mod version 2.2.5
|
||||||
|
* **[Modding]** Added VSN F-106 Delta Dart mod support (v2.9.4.101)
|
||||||
|
* **[Modding]** Added OH-6 Cayuse (v1.2) mod support, including the Vietnam Asset Pack v1.0
|
||||||
|
* **[Modding]** Added VSN EA-6B Prowler mod support (v2.9.4.102)
|
||||||
|
* **[Modding]** Added tripod3 Cold War assets mod support (v1.2)
|
||||||
|
* **[Modding]** Added VSN Mirage III mod support (2.5.7.01)
|
||||||
|
* **[Campaign Setup]** Allow adjustments to naval TGOs (except carriers) on turn 0
|
||||||
|
* **[Campaign Design]** Ability to configure specific carrier names & types in campaign's yaml file
|
||||||
|
* **[Mission Generation]** Ability to inject custom kneeboards
|
||||||
|
* **[Options]** Extend option (so it can be disabled when fixed in DCS) to force air-starts (except for the slots that work) at Ramon Airbase, similar to the Nevatim fix in Retribution 1.3.0
|
||||||
|
* **[Options]** New option in Settings: Default start type for Player flights.
|
||||||
|
* **[AirWing]** Expose OPFOR Squadrons, giving the ability to change liveries, auto-assignable mission types & an easy way to retrieve debug information.
|
||||||
|
* **[ATO]** Allow planning as OPFOR
|
||||||
|
* **[Campaign Design]** Support for latest maps (Kola, Afghanistan, Iraq)
|
||||||
|
* **[UI]** Zoom level retained when switching campaigns
|
||||||
|
* **[UX]** Allow changing squadrons in flight's edit dialog
|
||||||
|
* **[Cheats]** Sink/Resurrect carriers instead of showing an error during cheat-capture (use AWCD-cheat to add squadrons upon resurrection)
|
||||||
|
* **[UI/UX]** Allow changing conditions such as Time, Date & Weather
|
||||||
|
* **[Modding]** Added support for Su-15 Flagon mod (v1.0)
|
||||||
|
* **[Plugins]** Support for Carsten's Arty Spotter script
|
||||||
|
* **[Plugins]** Support for MBot's Call Artillery script (using on-map artillery)
|
||||||
|
* **[Modding]** Added support for SK-60 mod (v1.2.1)
|
||||||
|
* **[Mission Generation]** Introducing the Armed Recon flight plan, i.e. CAS against any Theater Ground Object
|
||||||
|
* **[Doctrine]** Ability to customize the startup time allocated to the player
|
||||||
|
* **[Mission Generation]** Ability to choose whether player flights can spawn on the sixpack or not
|
||||||
|
* **[Options]** New options in Mission Generator section: Limit AI radio callouts & Suppress AI radio callouts.
|
||||||
|
* **[Options]** New option to use the combat landing flag in the landing waypoint task for helicopters.
|
||||||
|
* **[UI/UX]** Sync package waypoints when primary flight's waypoints are updated and recreate other flights within the package to ensure JOIN, INGRESS & SPLIT are synced
|
||||||
|
* **[UI/UX]** Allow changing loadout on flight creation
|
||||||
|
* **[UI]** Display TOT for all waypoints in the flight plan
|
||||||
|
* **[UI]** Edit basic datalink properties for applicable aircraft
|
||||||
|
* **[Mission Generation]** Automatic datalink network setup for applicable aircraft (_should_ in theory avoid the need to re-save the mission)
|
||||||
|
* **[Options]** New option to force-enable deck-crew for super-carriers on dedicated server.
|
||||||
|
* **[Mission Generation]** Enable Supercarrier's LSO & Airboss stations
|
||||||
|
* **[UX]** Default settings are now loaded from Default.zip
|
||||||
|
* **[Autoplanner]** Plan Air-to-Air Escorts for AWACS & Tankers
|
||||||
|
* **[Package Planning]** Ability to plan recovery tanker flights
|
||||||
|
* **[Modding]** Support for Bandit's cloud presets mod (v15)
|
||||||
|
* **[UX]** Reduce size of save-file by loading landmap data on the fly, which also implies no new campaign needs to be started to benefit from an updated landmap
|
||||||
|
* **[New Game Wizard]** Ability to save an edited faction during new game creation
|
||||||
|
* **[Options]** New option to make AI helicopters prefer vertical takeoff and landing
|
||||||
|
* **[Campaign Design/Mission Generation]** Introduction of "rebel zones" which randomly spawn units according to the campaign's definitions.
|
||||||
|
* **[Mission Generation]** Missile sites now fire at random times instead of all at the beginning of the mission
|
||||||
|
* **[Modding]** Support for CurrentHill's Chinese Asset Pack (v1.1.4)
|
||||||
|
* **[Modding]** Updated support for CurrentHill's Swedish Asset Pack (v1.1.0)
|
||||||
|
* **[Modding]** Support for CurrentHill's Russian Asset Pack (v1.2.0)
|
||||||
|
* **[Modding]** Support for CurrentHill's USA Asset Pack (v1.1.5)
|
||||||
|
* **[Modding]** Update CJS Super Hornet to 2.4.2
|
||||||
|
* **[Modding]** Support for Cowboy's E-7A Wedgetail mod (Supports EW Script Offensive Jamming)
|
||||||
|
* **[Modding]** Support for szcz's MiG-31BM (v2.0)
|
||||||
|
* **[Plugins]** Added initial AI support for EW Script 2.0
|
||||||
|
* **[Options]** Ability to configure certain forced-options via a file (WIP: https://github.com/dcs-retribution/dcs-retribution/issues/490).
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
* **[UI/UX]** A-10A flights can be edited again
|
||||||
|
* **[Mission Generation]** IADS bug sometimes triggering "no skynet usable units" error during mission generation
|
||||||
|
* **[New Game Wizard]** Campaign errors show a dialog again and avoid CTDs
|
||||||
|
* **[UI]** Landmap wasn't updating when switching to a different theater
|
||||||
|
* **[Mission Results Processor]** Squadrons of a sunken carrier are now disbanded
|
||||||
|
* **[Mission Generation]** Introduced option to switch alt-type to AMSL during mission generation to avoid helicopters wanting to submerge over certain parts of the sea.
|
||||||
|
|
||||||
|
# Retribution v1.3.1
|
||||||
|
#### Note: Re-save your missions in DCS' Mission Editor to avoid possible crashes due to datalink (usually the case when F-16C blk50s are used) when hosting missions on a dedicated server.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
* **[UX]** Fix save-compatibility issue
|
||||||
|
* **[UX]** Avoid crash on startup due to incompatible save
|
||||||
|
|
||||||
|
|
||||||
# Retribution v1.3.0
|
# Retribution v1.3.0
|
||||||
#### Note: Re-save your missions in DCS' Mission Editor to avoid possible crashes due to datalink (usually the case when F-16C blk50s are used) when hosting missions on a dedicated server.
|
#### Note: Re-save your missions in DCS' Mission Editor to avoid possible crashes due to datalink (usually the case when F-16C blk50s are used) when hosting missions on a dedicated server.
|
||||||
|
|
||||||
@@ -40,6 +160,7 @@
|
|||||||
* **[Mission Generator]** Set F-14's IP waypoint according to the flight-plan's ingress point
|
* **[Mission Generator]** Set F-14's IP waypoint according to the flight-plan's ingress point
|
||||||
* **[Mission Generator]** Automatically de-spawn aircraft when arrival/divert is an off-map spawn
|
* **[Mission Generator]** Automatically de-spawn aircraft when arrival/divert is an off-map spawn
|
||||||
* **[Options]** Option to de-spawn AI flights in the air if their start-type was manually set to In-Flight
|
* **[Options]** Option to de-spawn AI flights in the air if their start-type was manually set to In-Flight
|
||||||
|
* **[Campaign Design]** Ability to add separate ground spawns for C-130 and other large aircraft to campaigns.
|
||||||
* **[Config]** Preference setting to use custom Liberation payloads instead of prioritizing Retribution's default
|
* **[Config]** Preference setting to use custom Liberation payloads instead of prioritizing Retribution's default
|
||||||
* **[Config]** Preference setting to configure the server-port on which Retribution's back-end will run
|
* **[Config]** Preference setting to configure the server-port on which Retribution's back-end will run
|
||||||
* **[Options]** Made AI jettisoning empty fuel tanks optional (disabled by default)
|
* **[Options]** Made AI jettisoning empty fuel tanks optional (disabled by default)
|
||||||
|
|||||||
28708
client/package-lock.json
generated
28708
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,26 +6,26 @@
|
|||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^1.8.5",
|
"@reduxjs/toolkit": "^1.9.7",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/jest": "^29.1.2",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^18.8.3",
|
"@types/node": "^18.19.26",
|
||||||
"@types/react": "^18.0.21",
|
"@types/react": "^18.2.69",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.2.22",
|
||||||
"@types/react-redux": "^7.1.24",
|
"@types/react-redux": "^7.1.33",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.7.4",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"esri-leaflet": "^3.0.8",
|
"esri-leaflet": "^3.0.12",
|
||||||
"leaflet": "^1.9.2",
|
"leaflet": "^1.9.4",
|
||||||
"leaflet-ruler": "^1.0.0",
|
"leaflet-ruler": "^1.0.0",
|
||||||
"milsymbol": "^2.0.0",
|
"milsymbol": "^2.2.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-esri-leaflet": "^2.0.1",
|
"react-esri-leaflet": "^2.0.1",
|
||||||
"react-leaflet": "^4.1.0",
|
"react-leaflet": "^4.2.1",
|
||||||
"react-redux": "^8.0.4",
|
"react-redux": "^8.1.3",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"typescript": "~4.8.4"
|
"typescript": "~4.8.4"
|
||||||
},
|
},
|
||||||
@@ -61,20 +61,20 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rtk-query/codegen-openapi": "^1.0.0",
|
"@rtk-query/codegen-openapi": "^1.2.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
"@types/leaflet": "^1.8.0",
|
"@types/leaflet": "^1.9.8",
|
||||||
"@types/redux-logger": "^3.0.9",
|
"@types/redux-logger": "^3.0.13",
|
||||||
"@types/websocket": "^1.0.5",
|
"@types/websocket": "^1.0.10",
|
||||||
"electron": "^22.3.25",
|
"electron": "^28.3.2",
|
||||||
"electron-is-dev": "^2.0.0",
|
"electron-is-dev": "^2.0.0",
|
||||||
"generate-license-file": "^2.0.0",
|
"generate-license-file": "^2.0.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
"nth-check": "^2.0.1",
|
"nth-check": "^2.1.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.2",
|
||||||
"wait-on": "^6.0.1"
|
"wait-on": "^7.2.0"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
|
|||||||
@@ -466,6 +466,7 @@ export type Game = {
|
|||||||
navmeshes: NavMeshes;
|
navmeshes: NavMeshes;
|
||||||
map_center?: LatLng;
|
map_center?: LatLng;
|
||||||
unculled_zones: UnculledZone[];
|
unculled_zones: UnculledZone[];
|
||||||
|
map_zones: MapZones;
|
||||||
};
|
};
|
||||||
export type MapZones = {
|
export type MapZones = {
|
||||||
inclusion: LatLng[][][];
|
inclusion: LatLng[][][];
|
||||||
|
|||||||
34
client/src/api/mapZonesSlice.ts
Normal file
34
client/src/api/mapZonesSlice.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { RootState } from "../app/store";
|
||||||
|
import { gameLoaded, gameUnloaded } from "./actions";
|
||||||
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
import { MapZones } from "./_liberationApi";
|
||||||
|
|
||||||
|
interface MapZonesState {
|
||||||
|
mapZones: MapZones;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: MapZonesState = {
|
||||||
|
mapZones: { inclusion: [], exclusion: [], sea: [] },
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapZonesSlice = createSlice({
|
||||||
|
name: "map",
|
||||||
|
initialState: initialState,
|
||||||
|
reducers: {},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder.addCase(gameLoaded, (state, action) => {
|
||||||
|
if (action.payload != null) {
|
||||||
|
state.mapZones.exclusion = action.payload.map_zones.exclusion;
|
||||||
|
state.mapZones.inclusion = action.payload.map_zones.inclusion;
|
||||||
|
state.mapZones.sea = action.payload.map_zones.sea;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.addCase(gameUnloaded, (state) => {
|
||||||
|
state.mapZones = initialState.mapZones;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectMapZones = (state: RootState) => state.mapZones;
|
||||||
|
|
||||||
|
export default mapZonesSlice.reducer;
|
||||||
@@ -10,6 +10,7 @@ import supplyRoutesReducer from "../api/supplyRoutesSlice";
|
|||||||
import tgosReducer from "../api/tgosSlice";
|
import tgosReducer from "../api/tgosSlice";
|
||||||
import threatZonesReducer from "../api/threatZonesSlice";
|
import threatZonesReducer from "../api/threatZonesSlice";
|
||||||
import unculledZonesReducer from "../api/unculledZonesSlice";
|
import unculledZonesReducer from "../api/unculledZonesSlice";
|
||||||
|
import mapZonesReducer from "../api/mapZonesSlice";
|
||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
PreloadedState,
|
PreloadedState,
|
||||||
@@ -31,6 +32,7 @@ const rootReducer = combineReducers({
|
|||||||
threatZones: threatZonesReducer,
|
threatZones: threatZonesReducer,
|
||||||
[baseApi.reducerPath]: baseApi.reducer,
|
[baseApi.reducerPath]: baseApi.reducer,
|
||||||
unculledZones: unculledZonesReducer,
|
unculledZones: unculledZonesReducer,
|
||||||
|
mapZones: mapZonesReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function setupStore(preloadedState?: PreloadedState<RootState>) {
|
export function setupStore(preloadedState?: PreloadedState<RootState>) {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Flight } from "../../api/liberationApi";
|
import { Flight } from "../../api/liberationApi";
|
||||||
import { Icon, Point } from "leaflet";
|
import { Icon, Point } from "leaflet";
|
||||||
import { Symbol } from "milsymbol";
|
import ms from "milsymbol";
|
||||||
import { Marker } from "react-leaflet";
|
import { Marker } from "react-leaflet";
|
||||||
|
|
||||||
function iconForFlight(flight: Flight) {
|
function iconForFlight(flight: Flight) {
|
||||||
const symbol = new Symbol(flight.sidc, {
|
const symbol = new ms.Symbol(flight.sidc, {
|
||||||
size: 20,
|
size: 20,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ControlPoint } from "../../api/_liberationApi";
|
import { ControlPoint } from "../../api/_liberationApi";
|
||||||
import { Icon, Point } from "leaflet";
|
import { Icon, Point } from "leaflet";
|
||||||
import { Symbol } from "milsymbol";
|
import ms from "milsymbol";
|
||||||
|
|
||||||
export const iconForControlPoint = (cp: ControlPoint) => {
|
export const iconForControlPoint = (cp: ControlPoint) => {
|
||||||
const symbol = new Symbol(cp.sidc, {
|
const symbol = new ms.Symbol(cp.sidc, {
|
||||||
size: 24,
|
size: 24,
|
||||||
colorMode: "Dark",
|
colorMode: "Dark",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ describe("FlightPlansLayer", () => {
|
|||||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
it("are not drawn when only selected flights are to be drawn", () => {
|
it("are not drawn when only selected flights are to be drawn", () => {
|
||||||
renderWithProviders(<FlightPlansLayer blue={true} selectedOnly />, {
|
renderWithProviders(<FlightPlansLayer selectedOnly />, {
|
||||||
preloadedState: {
|
preloadedState: {
|
||||||
flights: {
|
flights: {
|
||||||
flights: {
|
flights: {
|
||||||
@@ -355,7 +355,7 @@ describe("FlightPlansLayer", () => {
|
|||||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
it("are not drawn if red", () => {
|
it("are not drawn if red", () => {
|
||||||
renderWithProviders(<FlightPlansLayer blue={false} selectedOnly />, {
|
renderWithProviders(<FlightPlansLayer selectedOnly />, {
|
||||||
preloadedState: {
|
preloadedState: {
|
||||||
flights: {
|
flights: {
|
||||||
flights: {
|
flights: {
|
||||||
@@ -397,7 +397,7 @@ describe("FlightPlansLayer", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(mockPolyline).not.toHaveBeenCalled();
|
expect(mockPolyline).toHaveBeenCalled();
|
||||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import FlightPlan from "../flightplan";
|
|||||||
import { LayerGroup } from "react-leaflet";
|
import { LayerGroup } from "react-leaflet";
|
||||||
|
|
||||||
interface FlightPlansLayerProps {
|
interface FlightPlansLayerProps {
|
||||||
blue: boolean;
|
blue?: boolean;
|
||||||
selectedOnly?: true;
|
selectedOnly?: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,11 +15,6 @@ function SelectedFlightPlan(props: FlightPlansLayerProps) {
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.blue) {
|
|
||||||
// We don't currently support playing as red, so nothing to draw.
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlightPlan
|
<FlightPlan
|
||||||
key={flight.id}
|
key={flight.id}
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ export default function LiberationMap() {
|
|||||||
const map = useRef<Map>(null);
|
const map = useRef<Map>(null);
|
||||||
const mapCenter = useAppSelector(selectMapCenter);
|
const mapCenter = useAppSelector(selectMapCenter);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
map.current?.setView(mapCenter, 8, { animate: true, duration: 1 });
|
map.current?.setView(mapCenter, map.current?.getZoom() ?? 8, { animate: true, duration: 1 });
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<MapContainer zoom={8} zoomControl={false} ref={map}>
|
<MapContainer zoom={map.current?.getZoom() ?? 8} zoomControl={false} ref={map}>
|
||||||
<ScaleControl />
|
<ScaleControl />
|
||||||
<LeafletRuler />
|
<LeafletRuler />
|
||||||
<LayersControl collapsed={false}>
|
<LayersControl collapsed={false}>
|
||||||
@@ -98,8 +98,8 @@ export default function LiberationMap() {
|
|||||||
<LayersControl.Overlay name="Allied IADS Network">
|
<LayersControl.Overlay name="Allied IADS Network">
|
||||||
<Iadsnetworklayer blue={true} />
|
<Iadsnetworklayer blue={true} />
|
||||||
</LayersControl.Overlay>
|
</LayersControl.Overlay>
|
||||||
<LayersControl.Overlay name="Selected blue flight plan">
|
<LayersControl.Overlay name="Selected flight plan">
|
||||||
<FlightPlansLayer blue={true} selectedOnly />
|
<FlightPlansLayer selectedOnly />
|
||||||
</LayersControl.Overlay>
|
</LayersControl.Overlay>
|
||||||
<LayersControl.Overlay name="All blue flight plans" checked>
|
<LayersControl.Overlay name="All blue flight plans" checked>
|
||||||
<FlightPlansLayer blue={true} />
|
<FlightPlansLayer blue={true} />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useGetTerrainZonesQuery } from "../../api/liberationApi";
|
import { useAppSelector } from "../../app/hooks";
|
||||||
import { LatLngLiteral } from "leaflet";
|
import { LatLngLiteral } from "leaflet";
|
||||||
import { LayerGroup, LayersControl, Polygon } from "react-leaflet";
|
import { LayerGroup, LayersControl, Polygon } from "react-leaflet";
|
||||||
|
import { selectMapZones } from "../../api/mapZonesSlice";
|
||||||
|
|
||||||
interface TerrainZoneLayerProps {
|
interface TerrainZoneLayerProps {
|
||||||
zones: LatLngLiteral[][][];
|
zones: LatLngLiteral[][][];
|
||||||
@@ -28,33 +29,28 @@ function TerrainZoneLayer(props: TerrainZoneLayerProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TerrainZonesLayers() {
|
export default function TerrainZonesLayers() {
|
||||||
const { data, error, isLoading } = useGetTerrainZonesQuery();
|
const zones = useAppSelector(selectMapZones).mapZones;
|
||||||
var exclusion = <></>;
|
var exclusion = <></>;
|
||||||
var inclusion = <></>;
|
var inclusion = <></>;
|
||||||
var sea = <></>;
|
var sea = <></>;
|
||||||
|
|
||||||
if (error) {
|
if (zones) {
|
||||||
console.error("Error while loading terrain zones", error);
|
|
||||||
} else if (isLoading) {
|
|
||||||
} else if (!data) {
|
|
||||||
console.log("Empty response when loading terrain zones");
|
|
||||||
} else {
|
|
||||||
exclusion = (
|
exclusion = (
|
||||||
<TerrainZoneLayer
|
<TerrainZoneLayer
|
||||||
zones={data.exclusion}
|
zones={zones.exclusion}
|
||||||
color="#969696"
|
color="#969696"
|
||||||
fillColor="#303030"
|
fillColor="#303030"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
inclusion = (
|
inclusion = (
|
||||||
<TerrainZoneLayer
|
<TerrainZoneLayer
|
||||||
zones={data.inclusion}
|
zones={zones.inclusion}
|
||||||
color="#969696"
|
color="#969696"
|
||||||
fillColor="#4b4b4b"
|
fillColor="#4b4b4b"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
sea = (
|
sea = (
|
||||||
<TerrainZoneLayer zones={data.sea} color="#344455" fillColor="#344455" />
|
<TerrainZoneLayer zones={zones.sea} color="#344455" fillColor="#344455" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import {
|
|||||||
import { Tgo as TgoModel } from "../../api/liberationApi";
|
import { Tgo as TgoModel } from "../../api/liberationApi";
|
||||||
import SplitLines from "../splitlines/SplitLines";
|
import SplitLines from "../splitlines/SplitLines";
|
||||||
import { Icon, Point } from "leaflet";
|
import { Icon, Point } from "leaflet";
|
||||||
import { Symbol as MilSymbol } from "milsymbol";
|
import ms from "milsymbol";
|
||||||
import { Marker, Tooltip } from "react-leaflet";
|
import { Marker, Tooltip } from "react-leaflet";
|
||||||
|
|
||||||
function iconForTgo(cp: TgoModel) {
|
function iconForTgo(cp: TgoModel) {
|
||||||
const symbol = new MilSymbol(cp.sidc, {
|
const symbol = new ms.Symbol(cp.sidc, {
|
||||||
size: 24,
|
size: 24,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const WaypointMarker = (props: WaypointMarkerProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const waypoint = props.waypoint;
|
const waypoint = props.waypoint;
|
||||||
marker.current?.setTooltipContent(
|
marker.current?.setTooltipContent(
|
||||||
`${props.number} ${waypoint.name}<br />` +
|
`${props.number-1} ${waypoint.name}<br />` +
|
||||||
`${waypoint.altitude_ft.toFixed()} ft ${waypoint.altitude_reference}<br />` +
|
`${waypoint.altitude_ft.toFixed()} ft ${waypoint.altitude_reference}<br />` +
|
||||||
waypoint.timing
|
waypoint.timing
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
|
||||||
project = "DCS Liberation"
|
project = "DCS Retribution"
|
||||||
copyright = "2023, DCS Liberation Team"
|
copyright = "2025, DCS Retribution Team"
|
||||||
author = "DCS Liberation Team"
|
author = "DCS Retribution Team"
|
||||||
release = "7.0.0"
|
release = "1.5.0"
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class ArmedForces:
|
|||||||
# all layouts which are usable by the given faction.
|
# all layouts which are usable by the given faction.
|
||||||
for layout in LAYOUTS.layouts:
|
for layout in LAYOUTS.layouts:
|
||||||
if layout.generic and layout.usable_by_faction(faction):
|
if layout.generic and layout.usable_by_faction(faction):
|
||||||
# Creates a faction compatible GorceGroup
|
# Creates a faction compatible ForceGroup
|
||||||
self.add_or_update_force_group(ForceGroup.for_layout(layout, faction))
|
self.add_or_update_force_group(ForceGroup.for_layout(layout, faction))
|
||||||
|
|
||||||
# Add all preset groups afterwards to prevent them being merged with generics
|
# Add all preset groups afterwards to prevent them being merged with generics
|
||||||
|
|||||||
@@ -9,20 +9,30 @@ from typing import Any, ClassVar, Iterator, Optional, TYPE_CHECKING, Type
|
|||||||
import yaml
|
import yaml
|
||||||
from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType, VehicleType
|
from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType, VehicleType
|
||||||
|
|
||||||
|
from game import persistency
|
||||||
from game.data.groups import GroupTask
|
from game.data.groups import GroupTask
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.dcs.helpers import static_type_from_name
|
from game.dcs.helpers import static_type_from_name
|
||||||
from game.dcs.shipunittype import ShipUnitType
|
from game.dcs.shipunittype import ShipUnitType
|
||||||
from game.dcs.unittype import UnitType
|
from game.dcs.unittype import UnitType
|
||||||
from game.layout import LAYOUTS
|
from game.layout import LAYOUTS
|
||||||
from game.layout.layout import TgoLayout, TgoLayoutUnitGroup
|
from game.layout.layout import (
|
||||||
|
TgoLayout,
|
||||||
|
TgoLayoutUnitGroup,
|
||||||
|
)
|
||||||
from game.point_with_heading import PointWithHeading
|
from game.point_with_heading import PointWithHeading
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
IadsGroundObject,
|
IadsGroundObject,
|
||||||
IadsBuildingGroundObject,
|
IadsBuildingGroundObject,
|
||||||
NavalGroundObject,
|
NavalGroundObject,
|
||||||
)
|
)
|
||||||
from game.theater.theatergroup import IadsGroundGroup, IadsRole, TheaterGroup
|
from game.theater.theatergroup import (
|
||||||
|
IadsGroundGroup,
|
||||||
|
IadsRole,
|
||||||
|
TheaterGroup,
|
||||||
|
FIXED_POS_ARG,
|
||||||
|
FIXED_HDG_ARG,
|
||||||
|
)
|
||||||
from game.utils import escape_string_for_lua
|
from game.utils import escape_string_for_lua
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -248,8 +258,15 @@ class ForceGroup:
|
|||||||
if unit_count == 0:
|
if unit_count == 0:
|
||||||
# No units to be created so dont create a theater group for them
|
# No units to be created so dont create a theater group for them
|
||||||
return
|
return
|
||||||
|
# If the control point is neutral, don't create any units at all
|
||||||
|
if ground_object.control_point.captured.is_neutral:
|
||||||
|
return
|
||||||
# Generate Units
|
# Generate Units
|
||||||
units = unit_group.generate_units(ground_object, unit_type, unit_count)
|
fixed_pos = FIXED_POS_ARG in unit_group.name
|
||||||
|
fixed_hdg = FIXED_HDG_ARG in unit_group.name
|
||||||
|
units = unit_group.generate_units(
|
||||||
|
ground_object, unit_type, unit_count, fixed_pos, fixed_hdg
|
||||||
|
)
|
||||||
# Get or create the TheaterGroup
|
# Get or create the TheaterGroup
|
||||||
ground_group = ground_object.group_by_name(group_name)
|
ground_group = ground_object.group_by_name(group_name)
|
||||||
if ground_group is not None:
|
if ground_group is not None:
|
||||||
@@ -304,11 +321,22 @@ class ForceGroup:
|
|||||||
# Reverse the heading of the unit
|
# Reverse the heading of the unit
|
||||||
unit.position.heading = unit.position.heading.opposite
|
unit.position.heading = unit.position.heading.opposite
|
||||||
# Rotate unit around the center to align the orientation of the group
|
# Rotate unit around the center to align the orientation of the group
|
||||||
unit.position.rotate(ground_object.position, rotation)
|
unit.rotate_position_clockwise(ground_object.position, rotation)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _load_all(cls) -> None:
|
def _load_all(cls) -> None:
|
||||||
for file in Path("resources/groups").glob("*.yaml"):
|
locations = [
|
||||||
|
Path("resources/groups"),
|
||||||
|
persistency.groups_dir(),
|
||||||
|
]
|
||||||
|
for path in locations:
|
||||||
|
cls._process_path(path)
|
||||||
|
|
||||||
|
cls._loaded = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _process_path(cls, path: Path) -> None:
|
||||||
|
for file in path.glob("*.yaml"):
|
||||||
if not file.is_file():
|
if not file.is_file():
|
||||||
raise RuntimeError(f"{file.name} is not a valid ForceGroup")
|
raise RuntimeError(f"{file.name} is not a valid ForceGroup")
|
||||||
|
|
||||||
@@ -356,5 +384,3 @@ class ForceGroup:
|
|||||||
)
|
)
|
||||||
|
|
||||||
cls._by_name[force_group.name] = force_group
|
cls._by_name[force_group.name] = force_group
|
||||||
|
|
||||||
cls._loaded = True
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
from game.ato import FlightType
|
||||||
from game.ato.package import Package
|
from game.ato.package import Package
|
||||||
|
|
||||||
|
|
||||||
@@ -11,6 +12,16 @@ class AirTaskingOrder:
|
|||||||
#: The set of all planned packages in the ATO.
|
#: The set of all planned packages in the ATO.
|
||||||
packages: List[Package] = field(default_factory=list)
|
packages: List[Package] = field(default_factory=list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_awacs_package(self) -> bool:
|
||||||
|
return any(
|
||||||
|
[
|
||||||
|
p
|
||||||
|
for p in self.packages
|
||||||
|
if any([f for f in p.flights if f.flight_type is FlightType.AEWC])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def add_package(self, package: Package) -> None:
|
def add_package(self, package: Package) -> None:
|
||||||
"""Adds a package to the ATO."""
|
"""Adds a package to the ATO."""
|
||||||
self.packages.append(package)
|
self.packages.append(package)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Objective adjacency lists."""
|
"""Objective adjacency lists."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING
|
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ if TYPE_CHECKING:
|
|||||||
from game.sim.gameupdateevents import GameUpdateEvents
|
from game.sim.gameupdateevents import GameUpdateEvents
|
||||||
from game.sim.simulationresults import SimulationResults
|
from game.sim.simulationresults import SimulationResults
|
||||||
from game.squadrons import Squadron, Pilot
|
from game.squadrons import Squadron, Pilot
|
||||||
|
from game.theater.player import Player
|
||||||
from game.transfers import TransferOrder
|
from game.transfers import TransferOrder
|
||||||
from game.data.weapons import WeaponType
|
from game.data.weapons import WeaponType
|
||||||
from .flightmember import FlightMember
|
from .flightmember import FlightMember
|
||||||
@@ -89,6 +90,7 @@ class Flight(
|
|||||||
|
|
||||||
self.initialize_fuel()
|
self.initialize_fuel()
|
||||||
self.use_same_loadout_for_all_members = True
|
self.use_same_loadout_for_all_members = True
|
||||||
|
self.use_same_livery_for_all_members = True
|
||||||
|
|
||||||
# Only used by transport missions.
|
# Only used by transport missions.
|
||||||
self.cargo = cargo
|
self.cargo = cargo
|
||||||
@@ -173,7 +175,7 @@ class Flight(
|
|||||||
self.roster = FlightMembers.from_roster(self, self.roster)
|
self.roster = FlightMembers.from_roster(self, self.roster)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def blue(self) -> bool:
|
def blue(self) -> Player:
|
||||||
return self.squadron.player
|
return self.squadron.player
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -274,8 +276,6 @@ class Flight(
|
|||||||
self.fuel = unit_type.fuel_max * 0.5
|
self.fuel = unit_type.fuel_max * 0.5
|
||||||
elif unit_type == Hercules:
|
elif unit_type == Hercules:
|
||||||
self.fuel = unit_type.fuel_max * 0.75
|
self.fuel = unit_type.fuel_max * 0.75
|
||||||
elif self.departure.cptype.name in ["FARP", "FOB"] and not self.is_helo:
|
|
||||||
self.fuel = unit_type.fuel_max * 0.75
|
|
||||||
|
|
||||||
def any_member_has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
|
def any_member_has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
|
||||||
return any(
|
return any(
|
||||||
@@ -286,7 +286,7 @@ class Flight(
|
|||||||
return self.__str__()
|
return self.__str__()
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
string = f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
string = f"[{self.flight_type}] {self.count} x {self.unit_type} - {self.start_type.value}"
|
||||||
if self.custom_name:
|
if self.custom_name:
|
||||||
return f"{self.custom_name} - {string}"
|
return f"{self.custom_name} - {string}"
|
||||||
return string
|
return string
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any, Optional
|
||||||
|
|
||||||
from game.ato.loadouts import Loadout
|
from game.ato.loadouts import Loadout
|
||||||
from game.lasercodes import LaserCode
|
from game.lasercodes import LaserCode
|
||||||
@@ -16,14 +16,14 @@ class FlightMember:
|
|||||||
self.use_custom_loadout = False
|
self.use_custom_loadout = False
|
||||||
self.tgp_laser_code: LaserCode | None = None
|
self.tgp_laser_code: LaserCode | None = None
|
||||||
self.weapon_laser_code: LaserCode | None = None
|
self.weapon_laser_code: LaserCode | None = None
|
||||||
self.properties: dict[str, bool | float | int] = {}
|
self.properties: dict[str, bool | float | int | str] = {}
|
||||||
|
self.livery: Optional[str] = None
|
||||||
|
self.use_livery_set: bool = True
|
||||||
|
|
||||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
if "tgp_laser_code" not in state:
|
new_state = FlightMember(state["pilot"], state["loadout"])
|
||||||
state["tgp_laser_code"] = None
|
new_state.__dict__.update(state)
|
||||||
if "weapon_laser_code" not in state:
|
self.__dict__.update(new_state.__dict__)
|
||||||
state["weapon_laser_code"] = None
|
|
||||||
self.__dict__.update(state)
|
|
||||||
|
|
||||||
def assign_tgp_laser_code(self, code: LaserCode) -> None:
|
def assign_tgp_laser_code(self, code: LaserCode) -> None:
|
||||||
if self.tgp_laser_code is not None:
|
if self.tgp_laser_code is not None:
|
||||||
|
|||||||
@@ -95,6 +95,13 @@ class FlightMembers(IFlightRoster):
|
|||||||
# across all flight members.
|
# across all flight members.
|
||||||
member.loadout = loadout
|
member.loadout = loadout
|
||||||
|
|
||||||
|
def use_same_livery_for_all_members(self) -> None:
|
||||||
|
if not self.members:
|
||||||
|
return
|
||||||
|
livery = self.members[0].livery
|
||||||
|
for member in self.members[1:]:
|
||||||
|
member.livery = livery
|
||||||
|
|
||||||
def use_distinct_loadouts_for_each_member(self) -> None:
|
def use_distinct_loadouts_for_each_member(self) -> None:
|
||||||
for member in self.members:
|
for member in self.members:
|
||||||
member.loadout = member.loadout.clone()
|
member.loadout = member.loadout.clone()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from game.utils import Distance, Heading, Speed, knots, meters, nautical_miles
|
|||||||
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
||||||
@property
|
@property
|
||||||
def patrol_duration(self) -> timedelta:
|
def patrol_duration(self) -> timedelta:
|
||||||
return timedelta(hours=4)
|
return self.flight.coalition.game.settings.desired_awacs_mission_duration
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_speed(self) -> Speed:
|
def patrol_speed(self) -> Speed:
|
||||||
|
|||||||
@@ -75,13 +75,6 @@ class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
|||||||
)
|
)
|
||||||
return tot - travel_time
|
return tot - travel_time
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
|
||||||
if waypoint is self.tot_waypoint:
|
|
||||||
return self.tot
|
|
||||||
elif waypoint is self.layout.ingress:
|
|
||||||
return self.ingress_time
|
|
||||||
return None
|
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -89,10 +82,6 @@ class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
|||||||
def ctld_target_zone_radius(self) -> Distance:
|
def ctld_target_zone_radius(self) -> Distance:
|
||||||
return meters(2500)
|
return meters(2500)
|
||||||
|
|
||||||
@property
|
|
||||||
def mission_begin_on_station_time(self) -> datetime | None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_departure_time(self) -> datetime:
|
def mission_departure_time(self) -> datetime:
|
||||||
return self.package.time_over_target
|
return self.package.time_over_target
|
||||||
@@ -136,10 +125,18 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
|||||||
pickup.alt = altitude
|
pickup.alt = altitude
|
||||||
pickup_position = pickup.position
|
pickup_position = pickup.position
|
||||||
|
|
||||||
ingress = builder.ingress(
|
ingress = (
|
||||||
FlightWaypointType.INGRESS_AIR_ASSAULT,
|
builder.ingress(
|
||||||
self.package.waypoints.ingress,
|
FlightWaypointType.INGRESS_AIR_ASSAULT,
|
||||||
self.package.target,
|
self.package.waypoints.ingress,
|
||||||
|
self.package.target,
|
||||||
|
)
|
||||||
|
if not self.flight.is_hercules
|
||||||
|
else builder.ingress(
|
||||||
|
FlightWaypointType.INGRESS_AIR_ASSAULT,
|
||||||
|
self.package.waypoints.initial,
|
||||||
|
self.package.target,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assault_area = builder.assault_area(self.package.target)
|
assault_area = builder.assault_area(self.package.target)
|
||||||
@@ -165,7 +162,7 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
|||||||
pickup=pickup,
|
pickup=pickup,
|
||||||
nav_to=builder.nav_path(
|
nav_to=builder.nav_path(
|
||||||
pickup_position,
|
pickup_position,
|
||||||
self.package.waypoints.ingress,
|
ingress.position,
|
||||||
altitude,
|
altitude,
|
||||||
altitude_is_agl,
|
altitude_is_agl,
|
||||||
),
|
),
|
||||||
@@ -182,7 +179,7 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
|||||||
divert=builder.divert(self.flight.divert),
|
divert=builder.divert(self.flight.divert),
|
||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
hold=None,
|
hold=None,
|
||||||
join=builder.join(ingress.position),
|
join=builder.join(self.package.waypoints.ingress),
|
||||||
split=builder.split(self.flight.arrival.position),
|
split=builder.split(self.flight.arrival.position),
|
||||||
refuel=None,
|
refuel=None,
|
||||||
custom_waypoints=list(),
|
custom_waypoints=list(),
|
||||||
|
|||||||
@@ -201,9 +201,11 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
|||||||
|
|
||||||
return_ascent = self._create_ascent_or_descent(
|
return_ascent = self._create_ascent_or_descent(
|
||||||
builder,
|
builder,
|
||||||
cargo.next_stop.position
|
(
|
||||||
if cargo.next_stop != self.flight.arrival
|
cargo.next_stop.position
|
||||||
else cargo.origin.position,
|
if cargo.next_stop != self.flight.arrival
|
||||||
|
else cargo.origin.position
|
||||||
|
),
|
||||||
self.flight.arrival.position,
|
self.flight.arrival.position,
|
||||||
altitude,
|
altitude,
|
||||||
altitude_is_agl,
|
altitude_is_agl,
|
||||||
@@ -211,9 +213,11 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
|||||||
return_descent = self._create_ascent_or_descent(
|
return_descent = self._create_ascent_or_descent(
|
||||||
builder,
|
builder,
|
||||||
self.flight.arrival.position,
|
self.flight.arrival.position,
|
||||||
cargo.next_stop.position
|
(
|
||||||
if cargo.next_stop != self.flight.arrival
|
cargo.next_stop.position
|
||||||
else cargo.origin.position,
|
if cargo.next_stop != self.flight.arrival
|
||||||
|
else cargo.origin.position
|
||||||
|
),
|
||||||
altitude,
|
altitude,
|
||||||
altitude_is_agl,
|
altitude_is_agl,
|
||||||
)
|
)
|
||||||
|
|||||||
34
game/ato/flightplans/armedrecon.py
Normal file
34
game/ato/flightplans/armedrecon.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from .formationattack import (
|
||||||
|
FormationAttackBuilder,
|
||||||
|
FormationAttackFlightPlan,
|
||||||
|
FormationAttackLayout,
|
||||||
|
)
|
||||||
|
from .uizonedisplay import UiZone, UiZoneDisplay
|
||||||
|
from ..flightwaypointtype import FlightWaypointType
|
||||||
|
from ...utils import nautical_miles
|
||||||
|
|
||||||
|
|
||||||
|
class ArmedReconFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||||
|
@staticmethod
|
||||||
|
def builder_type() -> Type[Builder]:
|
||||||
|
return Builder
|
||||||
|
|
||||||
|
def ui_zone(self) -> UiZone:
|
||||||
|
return UiZone(
|
||||||
|
[self.tot_waypoint.position],
|
||||||
|
nautical_miles(
|
||||||
|
self.flight.coalition.game.settings.armed_recon_engagement_range_distance
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(FormationAttackBuilder[ArmedReconFlightPlan, FormationAttackLayout]):
|
||||||
|
def layout(self) -> FormationAttackLayout:
|
||||||
|
return self._build(FlightWaypointType.INGRESS_ARMED_RECON)
|
||||||
|
|
||||||
|
def build(self, dump_debug_info: bool = False) -> ArmedReconFlightPlan:
|
||||||
|
return ArmedReconFlightPlan(self.flight, self.layout())
|
||||||
@@ -18,7 +18,7 @@ class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_duration(self) -> timedelta:
|
def patrol_duration(self) -> timedelta:
|
||||||
return self.flight.coalition.doctrine.cap_duration
|
return self.flight.coalition.game.settings.desired_barcap_mission_duration
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_speed(self) -> Speed:
|
def patrol_speed(self) -> Speed:
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|||||||
closest_airfield = airfield
|
closest_airfield = airfield
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise PlanningError("Could not find any enemy airfields")
|
for airfield in closest_cache.closest_airfields:
|
||||||
|
if airfield.captured != self.is_player:
|
||||||
|
closest_airfield = airfield
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise PlanningError("Could not find any enemy airfields")
|
||||||
|
|
||||||
heading = Heading.from_degrees(
|
heading = Heading.from_degrees(
|
||||||
location.position.heading_between_point(closest_airfield.position)
|
location.position.heading_between_point(closest_airfield.position)
|
||||||
|
|||||||
@@ -125,16 +125,12 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
|||||||
ingress_point_shapely.x, ingress_point_shapely.y
|
ingress_point_shapely.x, ingress_point_shapely.y
|
||||||
)
|
)
|
||||||
|
|
||||||
patrol_start_waypoint = builder.nav(
|
patrol_start_waypoint = builder.cas(patrol_start, ingress_egress_altitude)
|
||||||
patrol_start, ingress_egress_altitude, use_agl_patrol_altitude
|
|
||||||
)
|
|
||||||
patrol_start_waypoint.name = "FLOT START"
|
patrol_start_waypoint.name = "FLOT START"
|
||||||
patrol_start_waypoint.pretty_name = "FLOT start"
|
patrol_start_waypoint.pretty_name = "FLOT start"
|
||||||
patrol_start_waypoint.description = "FLOT boundary"
|
patrol_start_waypoint.description = "FLOT boundary"
|
||||||
|
|
||||||
patrol_end_waypoint = builder.nav(
|
patrol_end_waypoint = builder.cas(patrol_end, ingress_egress_altitude)
|
||||||
patrol_end, ingress_egress_altitude, use_agl_patrol_altitude
|
|
||||||
)
|
|
||||||
patrol_end_waypoint.name = "FLOT END"
|
patrol_end_waypoint.name = "FLOT END"
|
||||||
patrol_end_waypoint.pretty_name = "FLOT end"
|
patrol_end_waypoint.pretty_name = "FLOT end"
|
||||||
patrol_end_waypoint.description = "FLOT boundary"
|
patrol_end_waypoint.description = "FLOT boundary"
|
||||||
|
|||||||
@@ -60,6 +60,16 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
|
|||||||
def mission_departure_time(self) -> datetime:
|
def mission_departure_time(self) -> datetime:
|
||||||
return self.package.time_over_target
|
return self.package.time_over_target
|
||||||
|
|
||||||
|
@property
|
||||||
|
def landing_time(self) -> datetime:
|
||||||
|
arrival = (
|
||||||
|
self.layout.custom_waypoints[-1]
|
||||||
|
if self.layout.custom_waypoints
|
||||||
|
else self.layout.departure
|
||||||
|
)
|
||||||
|
return_time = self.total_time_between_waypoints(self.tot_waypoint, arrival)
|
||||||
|
return self.mission_departure_time + return_time
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder[CustomFlightPlan, CustomLayout]):
|
class Builder(IBuilder[CustomFlightPlan, CustomLayout]):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from .airassault import AirAssaultLayout
|
from .airassault import AirAssaultLayout
|
||||||
@@ -11,6 +12,8 @@ from .formationattack import (
|
|||||||
)
|
)
|
||||||
from .waypointbuilder import WaypointBuilder
|
from .waypointbuilder import WaypointBuilder
|
||||||
from .. import FlightType
|
from .. import FlightType
|
||||||
|
from ..packagewaypoints import PackageWaypoints
|
||||||
|
from ...utils import feet
|
||||||
|
|
||||||
|
|
||||||
class EscortFlightPlan(FormationAttackFlightPlan):
|
class EscortFlightPlan(FormationAttackFlightPlan):
|
||||||
@@ -18,23 +21,48 @@ class EscortFlightPlan(FormationAttackFlightPlan):
|
|||||||
def builder_type() -> Type[Builder]:
|
def builder_type() -> Type[Builder]:
|
||||||
return Builder
|
return Builder
|
||||||
|
|
||||||
|
@property
|
||||||
|
def split_time(self) -> datetime:
|
||||||
|
# Avoid infinite recursion when this escort flight is itself the primary flight
|
||||||
|
# This can happen when only escort flights remain in a package
|
||||||
|
if (
|
||||||
|
self.package.primary_flight
|
||||||
|
and self.package.primary_flight != self.flight
|
||||||
|
and self.package.primary_flight.flight_plan
|
||||||
|
):
|
||||||
|
return self.package.primary_flight.flight_plan.mission_departure_time
|
||||||
|
else:
|
||||||
|
return super().split_time
|
||||||
|
|
||||||
|
|
||||||
class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||||
def layout(self) -> FormationAttackLayout:
|
def layout(self) -> FormationAttackLayout:
|
||||||
assert self.package.waypoints is not None
|
non_formation_escort = False
|
||||||
|
if self.package.waypoints is None:
|
||||||
|
self.package.waypoints = PackageWaypoints.create(
|
||||||
|
self.package, self.coalition, dump_debug_info=False
|
||||||
|
)
|
||||||
|
if self.package.primary_flight:
|
||||||
|
departure = self.package.primary_flight.flight_plan.layout.departure
|
||||||
|
self.package.waypoints.join = departure.position.lerp(
|
||||||
|
self.package.target.position, 0.2
|
||||||
|
)
|
||||||
|
non_formation_escort = True
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight)
|
builder = WaypointBuilder(self.flight)
|
||||||
ingress, target = builder.escort(
|
ingress, target = builder.escort(
|
||||||
self.package.waypoints.ingress, self.package.target
|
self.package.waypoints.ingress, self.package.target
|
||||||
)
|
)
|
||||||
|
if non_formation_escort:
|
||||||
|
target.position = self.package.waypoints.join
|
||||||
ingress.only_for_player = True
|
ingress.only_for_player = True
|
||||||
target.only_for_player = True
|
target.only_for_player = True
|
||||||
hold = None
|
hold = None
|
||||||
if not self.flight.is_helo:
|
if not (self.flight.is_helo or non_formation_escort):
|
||||||
hold = builder.hold(self._hold_point())
|
hold = builder.hold(self._hold_point())
|
||||||
|
|
||||||
join_pos = (
|
join_pos = (
|
||||||
self.package.waypoints.ingress
|
WaypointBuilder.perturb(self.package.waypoints.ingress, feet(500))
|
||||||
if self.flight.is_helo
|
if self.flight.is_helo
|
||||||
else self.package.waypoints.join
|
else self.package.waypoints.join
|
||||||
)
|
)
|
||||||
@@ -59,8 +87,6 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
|||||||
join = builder.join(ascent.position)
|
join = builder.join(ascent.position)
|
||||||
if layout.pickup and layout.drop_off_ascent:
|
if layout.pickup and layout.drop_off_ascent:
|
||||||
join = builder.join(layout.drop_off_ascent.position)
|
join = builder.join(layout.drop_off_ascent.position)
|
||||||
elif layout.pickup:
|
|
||||||
join = builder.join(layout.pickup.position)
|
|
||||||
split = builder.split(layout.arrival.position)
|
split = builder.split(layout.arrival.position)
|
||||||
if layout.drop_off:
|
if layout.drop_off:
|
||||||
initial = builder.escort_hold(
|
initial = builder.escort_hold(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ MissionPlanner. Those only plan basic information like the objective, aircraft
|
|||||||
type, and the size of the flight. The FlightPlanBuilder is responsible for
|
type, and the size of the flight. The FlightPlanBuilder is responsible for
|
||||||
generating the waypoints for the mission.
|
generating the waypoints for the mission.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
@@ -105,19 +106,6 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
#
|
#
|
||||||
# Plus, it's a loiter point so there's no reason to hurry.
|
# Plus, it's a loiter point so there's no reason to hurry.
|
||||||
factor = 0.75
|
factor = 0.75
|
||||||
elif (
|
|
||||||
self.flight.is_helo
|
|
||||||
and (
|
|
||||||
a.waypoint_type == FlightWaypointType.JOIN
|
|
||||||
or "INGRESS" in a.waypoint_type.name
|
|
||||||
or a.waypoint_type == FlightWaypointType.CUSTOM
|
|
||||||
)
|
|
||||||
and self.package.primary_flight
|
|
||||||
and not self.package.primary_flight.flight_plan.is_airassault
|
|
||||||
):
|
|
||||||
# Helicopter flights should be slowed down between JOIN & INGRESS
|
|
||||||
# to allow the escort to keep up while engaging targets along the way.
|
|
||||||
factor = 0.50
|
|
||||||
# TODO: Adjust if AGL.
|
# TODO: Adjust if AGL.
|
||||||
# We don't have an exact heightmap, but we should probably be performing
|
# We don't have an exact heightmap, but we should probably be performing
|
||||||
# *some* adjustment for NTTR since the minimum altitude of the map is
|
# *some* adjustment for NTTR since the minimum altitude of the map is
|
||||||
@@ -268,7 +256,9 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
def estimate_startup(self) -> timedelta:
|
def estimate_startup(self) -> timedelta:
|
||||||
if self.flight.start_type is StartType.COLD:
|
if self.flight.start_type is StartType.COLD:
|
||||||
if self.flight.client_count:
|
if self.flight.client_count:
|
||||||
return timedelta(minutes=10)
|
return timedelta(
|
||||||
|
minutes=self.flight.coalition.game.settings.player_startup_time
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# The AI doesn't seem to have a real startup procedure.
|
# The AI doesn't seem to have a real startup procedure.
|
||||||
return timedelta(minutes=2)
|
return timedelta(minutes=2)
|
||||||
@@ -306,6 +296,10 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
"""The time that the mission is complete and the flight RTBs."""
|
"""The time that the mission is complete and the flight RTBs."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def landing_time(self) -> datetime:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@self_type_guard
|
@self_type_guard
|
||||||
def is_loiter(self, flight_plan: FlightPlan[Any]) -> TypeGuard[LoiterFlightPlan]:
|
def is_loiter(self, flight_plan: FlightPlan[Any]) -> TypeGuard[LoiterFlightPlan]:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ from __future__ import annotations
|
|||||||
from typing import Any, TYPE_CHECKING, Type
|
from typing import Any, TYPE_CHECKING, Type
|
||||||
|
|
||||||
from game.ato import FlightType
|
from game.ato import FlightType
|
||||||
|
from game.theater.controlpoint import NavalControlPoint
|
||||||
from .aewc import AewcFlightPlan
|
from .aewc import AewcFlightPlan
|
||||||
from .airassault import AirAssaultFlightPlan
|
from .airassault import AirAssaultFlightPlan
|
||||||
from .airlift import AirliftFlightPlan
|
from .airlift import AirliftFlightPlan
|
||||||
from .antiship import AntiShipFlightPlan
|
from .antiship import AntiShipFlightPlan
|
||||||
|
from .armedrecon import ArmedReconFlightPlan
|
||||||
from .bai import BaiFlightPlan
|
from .bai import BaiFlightPlan
|
||||||
from .barcap import BarCapFlightPlan
|
from .barcap import BarCapFlightPlan
|
||||||
from .cas import CasFlightPlan
|
from .cas import CasFlightPlan
|
||||||
@@ -18,8 +20,10 @@ from .ocaaircraft import OcaAircraftFlightPlan
|
|||||||
from .ocarunway import OcaRunwayFlightPlan
|
from .ocarunway import OcaRunwayFlightPlan
|
||||||
from .packagerefueling import PackageRefuelingFlightPlan
|
from .packagerefueling import PackageRefuelingFlightPlan
|
||||||
from .planningerror import PlanningError
|
from .planningerror import PlanningError
|
||||||
|
from .pretensecargo import PretenseCargoFlightPlan
|
||||||
from .sead import SeadFlightPlan
|
from .sead import SeadFlightPlan
|
||||||
from .seadsweep import SeadSweepFlightPlan
|
from .seadsweep import SeadSweepFlightPlan
|
||||||
|
from .shiprecoverytanker import RecoveryTankerFlightPlan
|
||||||
from .strike import StrikeFlightPlan
|
from .strike import StrikeFlightPlan
|
||||||
from .sweep import SweepFlightPlan
|
from .sweep import SweepFlightPlan
|
||||||
from .tarcap import TarCapFlightPlan
|
from .tarcap import TarCapFlightPlan
|
||||||
@@ -60,6 +64,9 @@ class FlightPlanBuilderTypes:
|
|||||||
FlightType.TRANSPORT: AirliftFlightPlan.builder_type(),
|
FlightType.TRANSPORT: AirliftFlightPlan.builder_type(),
|
||||||
FlightType.FERRY: FerryFlightPlan.builder_type(),
|
FlightType.FERRY: FerryFlightPlan.builder_type(),
|
||||||
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
|
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
|
||||||
|
FlightType.PRETENSE_CARGO: PretenseCargoFlightPlan.builder_type(),
|
||||||
|
FlightType.ARMED_RECON: ArmedReconFlightPlan.builder_type(),
|
||||||
|
FlightType.RECOVERY: RecoveryTankerFlightPlan.builder_type(),
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
return builder_dict[flight.flight_type]
|
return builder_dict[flight.flight_type]
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ class FormationLayout(LoiterLayout, ABC):
|
|||||||
class FormationFlightPlan(LoiterFlightPlan, ABC):
|
class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def package_speed_waypoints(self) -> set[FlightWaypoint]:
|
def package_speed_waypoints(self) -> set[FlightWaypoint]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||||
@@ -64,8 +63,10 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
|||||||
return min(speeds)
|
return min(speeds)
|
||||||
|
|
||||||
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
|
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
|
||||||
if self.package.formation_speed and b in self.package_speed_waypoints:
|
if (
|
||||||
return self.package.formation_speed
|
speed := self.package.formation_speed(self.flight.is_helo)
|
||||||
|
) and b in self.package_speed_waypoints:
|
||||||
|
return speed
|
||||||
return super().speed_between_waypoints(a, b)
|
return super().speed_between_waypoints(a, b)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -75,13 +76,11 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def join_time(self) -> datetime:
|
def join_time(self) -> datetime: ...
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def split_time(self) -> datetime:
|
def split_time(self) -> datetime: ...
|
||||||
...
|
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
if waypoint == self.layout.join:
|
if waypoint == self.layout.join:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from dcs import Point
|
|||||||
|
|
||||||
from game.flightplan import HoldZoneGeometry
|
from game.flightplan import HoldZoneGeometry
|
||||||
from game.theater import MissionTarget
|
from game.theater import MissionTarget
|
||||||
from game.utils import Speed, meters, nautical_miles
|
from game.utils import nautical_miles, Speed, feet
|
||||||
from .flightplan import FlightPlan
|
from .flightplan import FlightPlan
|
||||||
from .formation import FormationFlightPlan, FormationLayout
|
from .formation import FormationFlightPlan, FormationLayout
|
||||||
from .ibuilder import IBuilder
|
from .ibuilder import IBuilder
|
||||||
@@ -39,8 +39,9 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
|||||||
if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC:
|
if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC:
|
||||||
# Should be impossible, as any package with at least one
|
# Should be impossible, as any package with at least one
|
||||||
# FormationFlightPlan flight needs a formation speed.
|
# FormationFlightPlan flight needs a formation speed.
|
||||||
assert self.package.formation_speed is not None
|
speed = self.package.formation_speed(self.flight.is_helo)
|
||||||
return self.package.formation_speed
|
assert speed is not None
|
||||||
|
return speed
|
||||||
return super().speed_between_waypoints(a, b)
|
return super().speed_between_waypoints(a, b)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -53,7 +54,7 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
|||||||
"TARGET AREA",
|
"TARGET AREA",
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
self.package.target.position,
|
self.package.target.position,
|
||||||
meters(0),
|
feet(0),
|
||||||
"RADIO",
|
"RADIO",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -192,6 +193,7 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|||||||
join_pos = self.package.waypoints.join
|
join_pos = self.package.waypoints.join
|
||||||
if self.flight.is_helo:
|
if self.flight.is_helo:
|
||||||
join_pos = self.package.waypoints.ingress
|
join_pos = self.package.waypoints.ingress
|
||||||
|
join_pos = WaypointBuilder.perturb(join_pos, feet(500))
|
||||||
join = builder.join(join_pos)
|
join = builder.join(join_pos)
|
||||||
split = builder.split(self._get_split())
|
split = builder.split(self._get_split())
|
||||||
refuel = self._build_refuel(builder)
|
refuel = self._build_refuel(builder)
|
||||||
@@ -286,6 +288,8 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|||||||
return builder.sead_area(location)
|
return builder.sead_area(location)
|
||||||
elif flight.flight_type == FlightType.OCA_AIRCRAFT:
|
elif flight.flight_type == FlightType.OCA_AIRCRAFT:
|
||||||
return builder.oca_strike_area(location)
|
return builder.oca_strike_area(location)
|
||||||
|
elif flight.flight_type == FlightType.ARMED_RECON:
|
||||||
|
return builder.armed_recon_area(location)
|
||||||
else:
|
else:
|
||||||
return builder.strike_area(location)
|
return builder.strike_area(location)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from ..packagewaypoints import PackageWaypoints
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.coalition import Coalition
|
from game.coalition import Coalition
|
||||||
from game.data.doctrine import Doctrine
|
from game.data.doctrine import Doctrine
|
||||||
from game.theater import ConflictTheater
|
from game.theater import ConflictTheater, Player
|
||||||
from game.threatzones import ThreatZones
|
from game.threatzones import ThreatZones
|
||||||
from ..flight import Flight
|
from ..flight import Flight
|
||||||
from ..package import Package
|
from ..package import Package
|
||||||
@@ -38,7 +38,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
|||||||
self._generate_package_waypoints_if_needed(dump_debug_info)
|
self._generate_package_waypoints_if_needed(dump_debug_info)
|
||||||
self._flight_plan = self.build(dump_debug_info)
|
self._flight_plan = self.build(dump_debug_info)
|
||||||
except NavMeshError as ex:
|
except NavMeshError as ex:
|
||||||
color = "blue" if self.flight.squadron.player else "red"
|
color = "blue" if self.flight.squadron.player.is_blue else "red"
|
||||||
raise PlanningError(
|
raise PlanningError(
|
||||||
f"Could not plan {color} {self.flight.flight_type.value} from "
|
f"Could not plan {color} {self.flight.flight_type.value} from "
|
||||||
f"{self.flight.departure} to {self.package.target}"
|
f"{self.flight.departure} to {self.package.target}"
|
||||||
@@ -60,8 +60,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
|||||||
return self.flight.departure.theater
|
return self.flight.departure.theater
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def build(self, dump_debug_info: bool = False) -> FlightPlanT:
|
def build(self, dump_debug_info: bool = False) -> FlightPlanT: ...
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def package(self) -> Package:
|
def package(self) -> Package:
|
||||||
@@ -72,7 +71,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
|||||||
return self.flight.coalition
|
return self.flight.coalition
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_player(self) -> bool:
|
def is_player(self) -> Player:
|
||||||
return self.coalition.player
|
return self.coalition.player
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ class LoiterFlightPlan(StandardFlightPlan[Any], ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def push_time(self) -> datetime:
|
def push_time(self) -> datetime: ...
|
||||||
...
|
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
if waypoint == self.layout.hold:
|
if waypoint == self.layout.hold:
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
|
|||||||
return self.patrol_end_time
|
return self.patrol_end_time
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def takeoff_time(self) -> datetime:
|
||||||
|
return self.patrol_start_time - self._travel_time_to_waypoint(self.tot_waypoint)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def package_speed_waypoints(self) -> set[FlightWaypoint]:
|
def package_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||||
return {self.layout.patrol_start, self.layout.patrol_end}
|
return {self.layout.patrol_start, self.layout.patrol_end}
|
||||||
|
|||||||
112
game/ato/flightplans/pretensecargo.py
Normal file
112
game/ato/flightplans/pretensecargo.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import random
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING, Type
|
||||||
|
|
||||||
|
from game.utils import feet
|
||||||
|
from .ferry import FerryLayout
|
||||||
|
from .ibuilder import IBuilder
|
||||||
|
from .planningerror import PlanningError
|
||||||
|
from .standard import StandardFlightPlan, StandardLayout
|
||||||
|
from .waypointbuilder import WaypointBuilder
|
||||||
|
from ...theater import OffMapSpawn
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..flightwaypoint import FlightWaypoint
|
||||||
|
|
||||||
|
|
||||||
|
PRETENSE_CARGO_FLIGHT_DISTANCE = 100000
|
||||||
|
PRETENSE_CARGO_FLIGHT_HEADING_RANGE = 20
|
||||||
|
|
||||||
|
|
||||||
|
class PretenseCargoFlightPlan(StandardFlightPlan[FerryLayout]):
|
||||||
|
@staticmethod
|
||||||
|
def builder_type() -> Type[Builder]:
|
||||||
|
return Builder
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tot_waypoint(self) -> FlightWaypoint:
|
||||||
|
return self.layout.arrival
|
||||||
|
|
||||||
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
|
# TOT planning isn't really useful for ferries. They're behind the front
|
||||||
|
# lines so no need to wait for escorts or for other missions to complete.
|
||||||
|
return None
|
||||||
|
|
||||||
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mission_begin_on_station_time(self) -> datetime | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mission_departure_time(self) -> datetime:
|
||||||
|
return self.package.time_over_target
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(IBuilder[PretenseCargoFlightPlan, FerryLayout]):
|
||||||
|
def layout(self) -> FerryLayout:
|
||||||
|
# Find the spawn location for off-map transport planes
|
||||||
|
distance_to_flot = 0.0
|
||||||
|
heading_from_flot = 0.0
|
||||||
|
offmap_transport_cp_id = self.flight.departure.id
|
||||||
|
for front_line_cp in self.coalition.game.theater.controlpoints:
|
||||||
|
if isinstance(front_line_cp, OffMapSpawn):
|
||||||
|
continue
|
||||||
|
for front_line in self.coalition.game.theater.conflicts():
|
||||||
|
if front_line_cp.captured == self.flight.coalition.player:
|
||||||
|
if (
|
||||||
|
front_line_cp.position.distance_to_point(front_line.position)
|
||||||
|
> distance_to_flot
|
||||||
|
):
|
||||||
|
distance_to_flot = front_line_cp.position.distance_to_point(
|
||||||
|
front_line.position
|
||||||
|
)
|
||||||
|
heading_from_flot = front_line.position.heading_between_point(
|
||||||
|
front_line_cp.position
|
||||||
|
)
|
||||||
|
offmap_transport_cp_id = front_line_cp.id
|
||||||
|
offmap_transport_cp = self.coalition.game.theater.find_control_point_by_id(
|
||||||
|
offmap_transport_cp_id
|
||||||
|
)
|
||||||
|
offmap_heading = random.randrange(
|
||||||
|
int(heading_from_flot - PRETENSE_CARGO_FLIGHT_HEADING_RANGE),
|
||||||
|
int(heading_from_flot + PRETENSE_CARGO_FLIGHT_HEADING_RANGE),
|
||||||
|
)
|
||||||
|
offmap_transport_spawn = offmap_transport_cp.position.point_from_heading(
|
||||||
|
offmap_heading, PRETENSE_CARGO_FLIGHT_DISTANCE
|
||||||
|
)
|
||||||
|
|
||||||
|
altitude_is_agl = self.flight.is_helo
|
||||||
|
altitude = (
|
||||||
|
feet(self.coalition.game.settings.heli_cruise_alt_agl)
|
||||||
|
if altitude_is_agl
|
||||||
|
else self.flight.unit_type.preferred_patrol_altitude
|
||||||
|
)
|
||||||
|
|
||||||
|
builder = WaypointBuilder(self.flight)
|
||||||
|
ferry_layout = FerryLayout(
|
||||||
|
departure=builder.join(offmap_transport_spawn),
|
||||||
|
nav_to=builder.nav_path(
|
||||||
|
offmap_transport_spawn,
|
||||||
|
self.flight.arrival.position,
|
||||||
|
altitude,
|
||||||
|
altitude_is_agl,
|
||||||
|
),
|
||||||
|
arrival=builder.land(self.flight.arrival),
|
||||||
|
divert=builder.divert(self.flight.divert),
|
||||||
|
bullseye=builder.bullseye(),
|
||||||
|
nav_from=[],
|
||||||
|
custom_waypoints=list(),
|
||||||
|
)
|
||||||
|
ferry_layout.departure = builder.join(offmap_transport_spawn)
|
||||||
|
ferry_layout.nav_to.append(builder.join(offmap_transport_spawn))
|
||||||
|
ferry_layout.nav_from.append(builder.join(offmap_transport_spawn))
|
||||||
|
return ferry_layout
|
||||||
|
|
||||||
|
def build(self, dump_debug_info: bool = False) -> PretenseCargoFlightPlan:
|
||||||
|
return PretenseCargoFlightPlan(self.flight, self.layout())
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
from abc import ABC
|
from abc import ABC
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from game.utils import Distance, Speed, knots, meters
|
from game.utils import Speed, knots, Distance, meters
|
||||||
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||||
|
|
||||||
|
|
||||||
class RefuelingFlightPlan(PatrollingFlightPlan[PatrollingLayout], ABC):
|
class RefuelingFlightPlan(PatrollingFlightPlan[PatrollingLayout], ABC):
|
||||||
|
@property
|
||||||
|
def patrol_duration(self) -> timedelta:
|
||||||
|
return self.flight.coalition.game.settings.desired_tanker_on_station_time
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_speed(self) -> Speed:
|
def patrol_speed(self) -> Speed:
|
||||||
# TODO: Could use self.flight.unit_type.preferred_patrol_speed(altitude).
|
# TODO: Could use self.flight.unit_type.preferred_patrol_speed(altitude).
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ from .formationattack import (
|
|||||||
FormationAttackFlightPlan,
|
FormationAttackFlightPlan,
|
||||||
FormationAttackLayout,
|
FormationAttackLayout,
|
||||||
)
|
)
|
||||||
|
from .uizonedisplay import UiZoneDisplay, UiZone
|
||||||
from ..flightwaypointtype import FlightWaypointType
|
from ..flightwaypointtype import FlightWaypointType
|
||||||
|
from ...utils import nautical_miles
|
||||||
|
|
||||||
|
|
||||||
class SeadSweepFlightPlan(FormationAttackFlightPlan):
|
class SeadSweepFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def builder_type() -> Type[Builder]:
|
def builder_type() -> Type[Builder]:
|
||||||
return Builder
|
return Builder
|
||||||
@@ -19,6 +21,14 @@ class SeadSweepFlightPlan(FormationAttackFlightPlan):
|
|||||||
def default_tot_offset(self) -> timedelta:
|
def default_tot_offset(self) -> timedelta:
|
||||||
return -timedelta(minutes=2)
|
return -timedelta(minutes=2)
|
||||||
|
|
||||||
|
def ui_zone(self) -> UiZone:
|
||||||
|
return UiZone(
|
||||||
|
[self.tot_waypoint.position],
|
||||||
|
nautical_miles(
|
||||||
|
self.flight.coalition.game.settings.sead_sweep_engagement_range_distance
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Builder(FormationAttackBuilder[SeadSweepFlightPlan, FormationAttackLayout]):
|
class Builder(FormationAttackBuilder[SeadSweepFlightPlan, FormationAttackLayout]):
|
||||||
def layout(self) -> FormationAttackLayout:
|
def layout(self) -> FormationAttackLayout:
|
||||||
|
|||||||
56
game/ato/flightplans/shiprecoverytanker.py
Normal file
56
game/ato/flightplans/shiprecoverytanker.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from game.ato.flightplans.ibuilder import IBuilder
|
||||||
|
from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
||||||
|
from .patrolling import PatrollingLayout
|
||||||
|
from .refuelingflightplan import RefuelingFlightPlan
|
||||||
|
from .. import FlightWaypoint
|
||||||
|
from ...utils import knots
|
||||||
|
|
||||||
|
|
||||||
|
class RecoveryTankerFlightPlan(RefuelingFlightPlan):
|
||||||
|
@staticmethod
|
||||||
|
def builder_type() -> Type[Builder]:
|
||||||
|
return Builder
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tot_waypoint(self) -> FlightWaypoint:
|
||||||
|
return self.layout.departure
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(IBuilder[RecoveryTankerFlightPlan, PatrollingLayout]):
|
||||||
|
def layout(self) -> PatrollingLayout:
|
||||||
|
|
||||||
|
builder = WaypointBuilder(self.flight)
|
||||||
|
altitude = builder.get_patrol_altitude
|
||||||
|
|
||||||
|
station_time = self.coalition.game.settings.desired_tanker_on_station_time
|
||||||
|
time_to_landing = station_time.total_seconds()
|
||||||
|
hdg = (self.coalition.game.conditions.weather.wind.at_0m.direction + 180) % 360
|
||||||
|
recovery_ship = self.package.target.position.point_from_heading(
|
||||||
|
hdg, time_to_landing * knots(20).meters_per_second
|
||||||
|
)
|
||||||
|
recovery_tanker = builder.recovery_tanker(recovery_ship)
|
||||||
|
patrol_end = builder.race_track_end(recovery_tanker.position, altitude)
|
||||||
|
patrol_end.only_for_player = True # avoid generating the waypoints
|
||||||
|
|
||||||
|
return PatrollingLayout(
|
||||||
|
departure=builder.takeoff(self.flight.departure),
|
||||||
|
nav_to=builder.nav_path(
|
||||||
|
self.flight.departure.position, recovery_ship, altitude
|
||||||
|
),
|
||||||
|
nav_from=builder.nav_path(
|
||||||
|
recovery_ship, self.flight.arrival.position, altitude
|
||||||
|
),
|
||||||
|
patrol_start=recovery_tanker,
|
||||||
|
patrol_end=patrol_end,
|
||||||
|
arrival=builder.land(self.flight.arrival),
|
||||||
|
divert=builder.divert(self.flight.divert),
|
||||||
|
bullseye=builder.bullseye(),
|
||||||
|
custom_waypoints=list(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def build(self, dump_debug_info: bool = False) -> RecoveryTankerFlightPlan:
|
||||||
|
return RecoveryTankerFlightPlan(self.flight, self.layout())
|
||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
from abc import ABC
|
from abc import ABC
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, TypeVar, Optional
|
from typing import TYPE_CHECKING, TypeVar, Optional
|
||||||
|
|
||||||
from game.ato.flightplans.flightplan import FlightPlan, Layout
|
from game.ato.flightplans.flightplan import FlightPlan, Layout
|
||||||
@@ -86,3 +87,10 @@ class StandardFlightPlan(FlightPlan[LayoutT], ABC):
|
|||||||
others are guaranteed to have certain properties like departure and arrival points,
|
others are guaranteed to have certain properties like departure and arrival points,
|
||||||
potentially a divert field, and a bullseye
|
potentially a divert field, and a bullseye
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def landing_time(self) -> datetime:
|
||||||
|
return_time = self.total_time_between_waypoints(
|
||||||
|
self.tot_waypoint, self.layout.arrival
|
||||||
|
)
|
||||||
|
return self.mission_departure_time + return_time
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from game.utils import Heading, meters, nautical_miles
|
from game.utils import Heading, meters, nautical_miles
|
||||||
@@ -15,10 +14,6 @@ class TheaterRefuelingFlightPlan(RefuelingFlightPlan):
|
|||||||
def builder_type() -> Type[Builder]:
|
def builder_type() -> Type[Builder]:
|
||||||
return Builder
|
return Builder
|
||||||
|
|
||||||
@property
|
|
||||||
def patrol_duration(self) -> timedelta:
|
|
||||||
return self.flight.coalition.game.settings.desired_tanker_on_station_time
|
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
|
class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
|
||||||
def layout(self) -> PatrollingLayout:
|
def layout(self) -> PatrollingLayout:
|
||||||
|
|||||||
@@ -14,5 +14,4 @@ class UiZone:
|
|||||||
|
|
||||||
class UiZoneDisplay(abc.ABC):
|
class UiZoneDisplay(abc.ABC):
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def ui_zone(self) -> UiZone:
|
def ui_zone(self) -> UiZone: ...
|
||||||
...
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import (
|
from typing import (
|
||||||
@@ -17,6 +18,7 @@ from dcs.mapping import Point, Vector2
|
|||||||
|
|
||||||
from game.ato.flightwaypoint import AltitudeReference, FlightWaypoint
|
from game.ato.flightwaypoint import AltitudeReference, FlightWaypoint
|
||||||
from game.ato.flightwaypointtype import FlightWaypointType
|
from game.ato.flightwaypointtype import FlightWaypointType
|
||||||
|
from game.data.weapons import WeaponType
|
||||||
from game.theater import (
|
from game.theater import (
|
||||||
ControlPoint,
|
ControlPoint,
|
||||||
MissionTarget,
|
MissionTarget,
|
||||||
@@ -24,13 +26,13 @@ from game.theater import (
|
|||||||
TheaterGroundObject,
|
TheaterGroundObject,
|
||||||
TheaterUnit,
|
TheaterUnit,
|
||||||
)
|
)
|
||||||
|
from game.theater.theatergroup import TheaterGroup
|
||||||
from game.utils import Distance, meters, nautical_miles, feet
|
from game.utils import Distance, meters, nautical_miles, feet
|
||||||
|
|
||||||
AGL_TRANSITION_ALT = 5000
|
AGL_TRANSITION_ALT = 5000
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.transfers import MultiGroupTransport
|
from game.transfers import MultiGroupTransport
|
||||||
from game.theater.theatergroup import TheaterGroup
|
|
||||||
from game.ato.flight import Flight
|
from game.ato.flight import Flight
|
||||||
|
|
||||||
|
|
||||||
@@ -249,6 +251,13 @@ class WaypointBuilder:
|
|||||||
objective: MissionTarget,
|
objective: MissionTarget,
|
||||||
) -> FlightWaypoint:
|
) -> FlightWaypoint:
|
||||||
alt = self.get_combat_altitude
|
alt = self.get_combat_altitude
|
||||||
|
if ingress_type in [
|
||||||
|
FlightWaypointType.INGRESS_CAS,
|
||||||
|
FlightWaypointType.INGRESS_OCA_AIRCRAFT,
|
||||||
|
FlightWaypointType.INGRESS_ARMED_RECON,
|
||||||
|
]:
|
||||||
|
alt = self._adjust_altitude_for_clouds(alt)
|
||||||
|
|
||||||
alt_type: AltitudeReference = "BARO"
|
alt_type: AltitudeReference = "BARO"
|
||||||
if self.is_helo or self.flight.is_hercules:
|
if self.is_helo or self.flight.is_hercules:
|
||||||
alt_type = "RADIO"
|
alt_type = "RADIO"
|
||||||
@@ -265,9 +274,11 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"INGRESS",
|
"INGRESS",
|
||||||
ingress_type,
|
ingress_type,
|
||||||
objective.position.point_from_heading(heading, nautical_miles(5).meters)
|
(
|
||||||
if self.is_helo
|
objective.position.point_from_heading(heading, nautical_miles(5).meters)
|
||||||
else position,
|
if self.is_helo
|
||||||
|
else position
|
||||||
|
),
|
||||||
alt,
|
alt,
|
||||||
alt_type,
|
alt_type,
|
||||||
description=f"INGRESS on {objective.name}",
|
description=f"INGRESS on {objective.name}",
|
||||||
@@ -275,6 +286,19 @@ class WaypointBuilder:
|
|||||||
targets=objective.strike_targets,
|
targets=objective.strike_targets,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _adjust_altitude_for_clouds(self, alt: Distance) -> Distance:
|
||||||
|
weather = self.flight.coalition.game.conditions.weather
|
||||||
|
max_alt = feet(math.inf)
|
||||||
|
if weather.clouds and (
|
||||||
|
weather.clouds.preset
|
||||||
|
and "overcast" in weather.clouds.preset.description.lower()
|
||||||
|
or weather.clouds.density > 5
|
||||||
|
):
|
||||||
|
max_alt = meters(
|
||||||
|
max(feet(500).meters, weather.clouds.base - feet(500).meters)
|
||||||
|
)
|
||||||
|
return min(alt, max_alt)
|
||||||
|
|
||||||
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
|
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
|
||||||
alt_type: AltitudeReference = "BARO"
|
alt_type: AltitudeReference = "BARO"
|
||||||
if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
|
if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
|
||||||
@@ -307,7 +331,11 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
target.name,
|
target.name,
|
||||||
FlightWaypointType.TARGET_POINT,
|
FlightWaypointType.TARGET_POINT,
|
||||||
target.target.position,
|
(
|
||||||
|
target.target.ground_object.position
|
||||||
|
if isinstance(target.target, TheaterGroup)
|
||||||
|
else target.target.position
|
||||||
|
),
|
||||||
meters(0),
|
meters(0),
|
||||||
"RADIO",
|
"RADIO",
|
||||||
description=description,
|
description=description,
|
||||||
@@ -336,6 +364,21 @@ class WaypointBuilder:
|
|||||||
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
|
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||||
return self._target_area(f"DEAD on {target.name}", target)
|
return self._target_area(f"DEAD on {target.name}", target)
|
||||||
|
|
||||||
|
def armed_recon_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||||
|
# Force AI aircraft to fly towards target area
|
||||||
|
alt = self.get_combat_altitude
|
||||||
|
alt = self._adjust_altitude_for_clouds(alt)
|
||||||
|
alt_type: AltitudeReference = "BARO"
|
||||||
|
if self.is_helo or alt.feet <= AGL_TRANSITION_ALT:
|
||||||
|
alt_type = "RADIO"
|
||||||
|
return self._target_area(
|
||||||
|
f"ARMED RECON {target.name}",
|
||||||
|
target,
|
||||||
|
altitude=alt,
|
||||||
|
alt_type=alt_type,
|
||||||
|
flyover=True,
|
||||||
|
)
|
||||||
|
|
||||||
def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint:
|
def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||||
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
|
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
|
||||||
|
|
||||||
@@ -380,14 +423,25 @@ class WaypointBuilder:
|
|||||||
waypoint.only_for_player = True
|
waypoint.only_for_player = True
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
def cas(self, position: Point) -> FlightWaypoint:
|
def cas(self, position: Point, altitude: Distance) -> FlightWaypoint:
|
||||||
|
weather = self.flight.coalition.game.conditions.weather
|
||||||
|
if weather.clouds and (
|
||||||
|
weather.clouds.preset
|
||||||
|
and "overcast" in weather.clouds.preset.description.lower()
|
||||||
|
or weather.clouds.density > 5
|
||||||
|
):
|
||||||
|
altitude = meters(
|
||||||
|
max(feet(500).meters, weather.clouds.base - feet(500).meters)
|
||||||
|
)
|
||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"CAS",
|
"CAS",
|
||||||
FlightWaypointType.CAS,
|
FlightWaypointType.CAS,
|
||||||
position,
|
position,
|
||||||
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
|
(
|
||||||
if self.is_helo
|
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
|
||||||
else meters(1000),
|
if self.is_helo
|
||||||
|
else max(meters(1000), altitude)
|
||||||
|
),
|
||||||
"RADIO",
|
"RADIO",
|
||||||
description="Provide CAS",
|
description="Provide CAS",
|
||||||
pretty_name="CAS",
|
pretty_name="CAS",
|
||||||
@@ -505,8 +559,13 @@ class WaypointBuilder:
|
|||||||
self.flight.coalition.game.settings.sead_threat_buffer_min_distance
|
self.flight.coalition.game.settings.sead_threat_buffer_min_distance
|
||||||
).meters
|
).meters
|
||||||
if target.strike_targets:
|
if target.strike_targets:
|
||||||
|
factor = (
|
||||||
|
0.8
|
||||||
|
if self.flight.any_member_has_weapon_of_type(WeaponType.ARM)
|
||||||
|
else 1.1
|
||||||
|
)
|
||||||
threat_range = (
|
threat_range = (
|
||||||
1.1 * max([x.threat_range for x in target.strike_targets]).meters
|
factor * max([x.threat_range for x in target.strike_targets]).meters
|
||||||
)
|
)
|
||||||
hdg = target.position.heading_between_point(ingress)
|
hdg = target.position.heading_between_point(ingress)
|
||||||
hold = target.position.point_from_heading(
|
hold = target.position.point_from_heading(
|
||||||
@@ -639,14 +698,13 @@ class WaypointBuilder:
|
|||||||
This waypoint is used to generate the Trigger Zone used for AirAssault and
|
This waypoint is used to generate the Trigger Zone used for AirAssault and
|
||||||
AirLift using the CTLD plugin (see LogisticsGenerator)
|
AirLift using the CTLD plugin (see LogisticsGenerator)
|
||||||
"""
|
"""
|
||||||
heli_alt = feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
|
alt = self.get_combat_altitude if self.flight.is_helo else meters(0)
|
||||||
altitude = heli_alt if self.flight.is_helo else meters(0)
|
|
||||||
|
|
||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"DROPOFFZONE",
|
"DROPOFFZONE",
|
||||||
FlightWaypointType.DROPOFF_ZONE,
|
FlightWaypointType.DROPOFF_ZONE,
|
||||||
drop_off.position,
|
drop_off.position,
|
||||||
altitude,
|
alt,
|
||||||
"RADIO",
|
"RADIO",
|
||||||
description=f"Drop off cargo at {drop_off.name}",
|
description=f"Drop off cargo at {drop_off.name}",
|
||||||
pretty_name="Drop-off zone",
|
pretty_name="Drop-off zone",
|
||||||
@@ -744,8 +802,22 @@ class WaypointBuilder:
|
|||||||
return previous_threatened and next_threatened
|
return previous_threatened and next_threatened
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def perturb(point: Point) -> Point:
|
def perturb(point: Point, deviation: Distance = nautical_miles(1)) -> Point:
|
||||||
deviation = nautical_miles(1)
|
|
||||||
x_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
x_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||||
y_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
y_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||||
return point + Vector2(x_adj, y_adj)
|
return point + Vector2(x_adj, y_adj)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def recovery_tanker(position: Point) -> FlightWaypoint:
|
||||||
|
alt_type: AltitudeReference = "BARO"
|
||||||
|
|
||||||
|
return FlightWaypoint(
|
||||||
|
"RECOVERY",
|
||||||
|
FlightWaypointType.RECOVERY_TANKER,
|
||||||
|
position,
|
||||||
|
feet(6000),
|
||||||
|
alt_type,
|
||||||
|
description="Recovery tanker for aircraft carriers",
|
||||||
|
pretty_name="Recovery",
|
||||||
|
only_for_player=True, # for visual purposes in Retribution only
|
||||||
|
)
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ class FlightRoster(IFlightRoster):
|
|||||||
def iter_pilots(self) -> Iterator[Pilot | None]:
|
def iter_pilots(self) -> Iterator[Pilot | None]:
|
||||||
yield from self.pilots
|
yield from self.pilots
|
||||||
|
|
||||||
|
@property
|
||||||
|
def player_count(self) -> int:
|
||||||
|
return len([p for p in self.pilots if p is not None and p.player])
|
||||||
|
|
||||||
def pilot_at(self, idx: int) -> Pilot | None:
|
def pilot_at(self, idx: int) -> Pilot | None:
|
||||||
return self.pilots[idx]
|
return self.pilots[idx]
|
||||||
|
|
||||||
|
|||||||
@@ -60,14 +60,12 @@ class FlightState(ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def cancelable(self) -> bool:
|
def cancelable(self) -> bool: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def on_game_tick(
|
def on_game_tick(
|
||||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||||
) -> None:
|
) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def in_flight(self) -> bool:
|
def in_flight(self) -> bool:
|
||||||
@@ -98,17 +96,14 @@ class FlightState(ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_waiting_for_start(self) -> bool:
|
def is_waiting_for_start(self) -> bool: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def estimate_position(self) -> Point:
|
def estimate_position(self) -> Point: ...
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def spawn_type(self) -> StartType:
|
def spawn_type(self) -> StartType: ...
|
||||||
...
|
|
||||||
|
|
||||||
def a2a_commit_region(self) -> Optional[ThreatPoly]:
|
def a2a_commit_region(self) -> Optional[ThreatPoly]:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -57,16 +57,13 @@ class InFlight(FlightState, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def estimate_position(self) -> Point:
|
def estimate_position(self) -> Point: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def estimate_altitude(self) -> tuple[Distance, str]:
|
def estimate_altitude(self) -> tuple[Distance, str]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def estimate_speed(self) -> Speed:
|
def estimate_speed(self) -> Speed: ...
|
||||||
...
|
|
||||||
|
|
||||||
def estimate_fuel_at_current_waypoint(self) -> float:
|
def estimate_fuel_at_current_waypoint(self) -> float:
|
||||||
initial_fuel = super().estimate_fuel()
|
initial_fuel = super().estimate_fuel()
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class Takeoff(AtDeparture):
|
|||||||
if (
|
if (
|
||||||
self.flight.client_count > 0
|
self.flight.client_count > 0
|
||||||
and self.settings.player_mission_interrupts_sim_at
|
and self.settings.player_mission_interrupts_sim_at
|
||||||
is [StartType.COLD, StartType.WARM, StartType.RUNWAY]
|
in [StartType.COLD, StartType.WARM, StartType.RUNWAY]
|
||||||
):
|
):
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Interrupting simulation because {self.flight} has players and has "
|
f"Interrupting simulation because {self.flight} has players and has "
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class Uninitialized(FlightState):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_waiting_for_start(self) -> bool:
|
def is_waiting_for_start(self) -> bool:
|
||||||
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
|
return True
|
||||||
|
|
||||||
def estimate_position(self) -> Point:
|
def estimate_position(self) -> Point:
|
||||||
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
|
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ class FlightType(Enum):
|
|||||||
FERRY = "Ferry"
|
FERRY = "Ferry"
|
||||||
AIR_ASSAULT = "Air Assault"
|
AIR_ASSAULT = "Air Assault"
|
||||||
SEAD_SWEEP = "SEAD Sweep" # Reintroduce legacy "engage-whatever-you-can-find" SEAD
|
SEAD_SWEEP = "SEAD Sweep" # Reintroduce legacy "engage-whatever-you-can-find" SEAD
|
||||||
|
PRETENSE_CARGO = "Cargo Transport" # For Pretense campaign AI cargo planes
|
||||||
|
ARMED_RECON = "Armed Recon"
|
||||||
|
RECOVERY = "Recovery"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.value
|
return self.value
|
||||||
@@ -93,6 +96,7 @@ class FlightType(Enum):
|
|||||||
FlightType.SEAD_ESCORT,
|
FlightType.SEAD_ESCORT,
|
||||||
FlightType.AIR_ASSAULT,
|
FlightType.AIR_ASSAULT,
|
||||||
FlightType.SEAD_SWEEP,
|
FlightType.SEAD_SWEEP,
|
||||||
|
FlightType.ARMED_RECON,
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -104,6 +108,7 @@ class FlightType(Enum):
|
|||||||
return {
|
return {
|
||||||
FlightType.AEWC: AirEntity.AIRBORNE_EARLY_WARNING,
|
FlightType.AEWC: AirEntity.AIRBORNE_EARLY_WARNING,
|
||||||
FlightType.ANTISHIP: AirEntity.ANTISURFACE_WARFARE,
|
FlightType.ANTISHIP: AirEntity.ANTISURFACE_WARFARE,
|
||||||
|
FlightType.ARMED_RECON: AirEntity.ATTACK_STRIKE,
|
||||||
FlightType.BAI: AirEntity.ATTACK_STRIKE,
|
FlightType.BAI: AirEntity.ATTACK_STRIKE,
|
||||||
FlightType.BARCAP: AirEntity.FIGHTER,
|
FlightType.BARCAP: AirEntity.FIGHTER,
|
||||||
FlightType.CAS: AirEntity.ATTACK_STRIKE,
|
FlightType.CAS: AirEntity.ATTACK_STRIKE,
|
||||||
@@ -113,6 +118,7 @@ class FlightType(Enum):
|
|||||||
FlightType.INTERCEPTION: AirEntity.FIGHTER,
|
FlightType.INTERCEPTION: AirEntity.FIGHTER,
|
||||||
FlightType.OCA_AIRCRAFT: AirEntity.ATTACK_STRIKE,
|
FlightType.OCA_AIRCRAFT: AirEntity.ATTACK_STRIKE,
|
||||||
FlightType.OCA_RUNWAY: AirEntity.ATTACK_STRIKE,
|
FlightType.OCA_RUNWAY: AirEntity.ATTACK_STRIKE,
|
||||||
|
FlightType.RECOVERY: AirEntity.TANKER,
|
||||||
FlightType.REFUELING: AirEntity.TANKER,
|
FlightType.REFUELING: AirEntity.TANKER,
|
||||||
FlightType.SEAD: AirEntity.SUPPRESSION_OF_ENEMY_AIR_DEFENCE,
|
FlightType.SEAD: AirEntity.SUPPRESSION_OF_ENEMY_AIR_DEFENCE,
|
||||||
FlightType.SEAD_ESCORT: AirEntity.SUPPRESSION_OF_ENEMY_AIR_DEFENCE,
|
FlightType.SEAD_ESCORT: AirEntity.SUPPRESSION_OF_ENEMY_AIR_DEFENCE,
|
||||||
@@ -121,5 +127,6 @@ class FlightType(Enum):
|
|||||||
FlightType.SWEEP: AirEntity.FIGHTER,
|
FlightType.SWEEP: AirEntity.FIGHTER,
|
||||||
FlightType.TARCAP: AirEntity.FIGHTER,
|
FlightType.TARCAP: AirEntity.FIGHTER,
|
||||||
FlightType.TRANSPORT: AirEntity.UTILITY,
|
FlightType.TRANSPORT: AirEntity.UTILITY,
|
||||||
|
FlightType.PRETENSE_CARGO: AirEntity.UTILITY,
|
||||||
FlightType.AIR_ASSAULT: AirEntity.ROTARY_WING,
|
FlightType.AIR_ASSAULT: AirEntity.ROTARY_WING,
|
||||||
}.get(self, AirEntity.UNSPECIFIED)
|
}.get(self, AirEntity.UNSPECIFIED)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal, TYPE_CHECKING
|
from typing import Literal, TYPE_CHECKING, Any, Dict, Optional
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
|
|
||||||
@@ -56,3 +57,17 @@ class FlightWaypoint:
|
|||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
return hash(id(self))
|
return hash(id(self))
|
||||||
|
|
||||||
|
def __deepcopy__(self, memo: Optional[Dict[int, Any]] = None) -> FlightWaypoint:
|
||||||
|
obj = FlightWaypoint(self.name, self.waypoint_type, self.position)
|
||||||
|
for attr in dir(self):
|
||||||
|
if attr == "control_point":
|
||||||
|
obj.control_point = self.control_point
|
||||||
|
elif attr == "targets":
|
||||||
|
obj.targets = self.targets
|
||||||
|
elif "__" in attr or attr not in obj.__dataclass_fields__:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
attr_copy = deepcopy(getattr(self, attr))
|
||||||
|
setattr(obj, attr, attr_copy)
|
||||||
|
return obj
|
||||||
|
|||||||
@@ -51,3 +51,5 @@ class FlightWaypointType(IntEnum):
|
|||||||
INGRESS_AIR_ASSAULT = 31
|
INGRESS_AIR_ASSAULT = 31
|
||||||
INGRESS_ANTI_SHIP = 32
|
INGRESS_ANTI_SHIP = 32
|
||||||
INGRESS_SEAD_SWEEP = 33
|
INGRESS_SEAD_SWEEP = 33
|
||||||
|
INGRESS_ARMED_RECON = 34
|
||||||
|
RECOVERY_TANKER = 35 # Tanker recovery point
|
||||||
|
|||||||
@@ -9,31 +9,28 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class IFlightRoster(ABC):
|
class IFlightRoster(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def iter_pilots(self) -> Iterator[Pilot | None]:
|
def iter_pilots(self) -> Iterator[Pilot | None]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def pilot_at(self, idx: int) -> Pilot | None:
|
def pilot_at(self, idx: int) -> Pilot | None: ...
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def squadron(self) -> Squadron:
|
def squadron(self) -> Squadron: ...
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def max_size(self) -> int:
|
def max_size(self) -> int: ...
|
||||||
...
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def player_count(self) -> int: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def resize(self, new_size: int) -> None:
|
def resize(self, new_size: int) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
|
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def clear(self) -> None:
|
def clear(self) -> None: ...
|
||||||
...
|
|
||||||
|
|||||||
@@ -137,11 +137,15 @@ class Loadout:
|
|||||||
continue
|
continue
|
||||||
name = payload["name"]
|
name = payload["name"]
|
||||||
pylons = payload["pylons"]
|
pylons = payload["pylons"]
|
||||||
yield Loadout(
|
try:
|
||||||
name,
|
yield Loadout(
|
||||||
{p["num"]: Weapon.with_clsid(p["CLSID"]) for p in pylons.values()},
|
name,
|
||||||
date=None,
|
{p["num"]: Weapon.with_clsid(p["CLSID"]) for p in pylons.values()},
|
||||||
)
|
date=None,
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
# invalid loadout
|
||||||
|
continue
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def valid_payload(pylons: Dict[int, Dict[str, str]]) -> bool:
|
def valid_payload(pylons: Dict[int, Dict[str, str]]) -> bool:
|
||||||
@@ -162,9 +166,11 @@ class Loadout:
|
|||||||
# last - the first element in the tuple will be tried first, then the second,
|
# last - the first element in the tuple will be tried first, then the second,
|
||||||
# etc.
|
# etc.
|
||||||
loadout_names = {
|
loadout_names = {
|
||||||
t: [f"Liberation {t.value}", f"Retribution {t.value}"]
|
t: (
|
||||||
if prefer_liberation_payloads()
|
[f"Liberation {t.value}", f"Retribution {t.value}"]
|
||||||
else [f"Retribution {t.value}", f"Liberation {t.value}"]
|
if prefer_liberation_payloads()
|
||||||
|
else [f"Retribution {t.value}", f"Liberation {t.value}"]
|
||||||
|
)
|
||||||
for t in FlightType
|
for t in FlightType
|
||||||
}
|
}
|
||||||
legacy_names = {
|
legacy_names = {
|
||||||
@@ -204,6 +210,7 @@ class Loadout:
|
|||||||
loadout_names[FlightType.INTERCEPTION].extend(loadout_names[FlightType.BARCAP])
|
loadout_names[FlightType.INTERCEPTION].extend(loadout_names[FlightType.BARCAP])
|
||||||
# OCA/Aircraft falls back to BAI, which falls back to CAS.
|
# OCA/Aircraft falls back to BAI, which falls back to CAS.
|
||||||
loadout_names[FlightType.BAI].extend(loadout_names[FlightType.CAS])
|
loadout_names[FlightType.BAI].extend(loadout_names[FlightType.CAS])
|
||||||
|
loadout_names[FlightType.ARMED_RECON].extend(loadout_names[FlightType.CAS])
|
||||||
loadout_names[FlightType.OCA_AIRCRAFT].extend(loadout_names[FlightType.BAI])
|
loadout_names[FlightType.OCA_AIRCRAFT].extend(loadout_names[FlightType.BAI])
|
||||||
# DEAD also falls back to BAI.
|
# DEAD also falls back to BAI.
|
||||||
loadout_names[FlightType.DEAD].extend(loadout_names[FlightType.BAI])
|
loadout_names[FlightType.DEAD].extend(loadout_names[FlightType.BAI])
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ class Package(RadioFrequencyContainer):
|
|||||||
def has_players(self) -> bool:
|
def has_players(self) -> bool:
|
||||||
return any(flight.client_count for flight in self.flights)
|
return any(flight.client_count for flight in self.flights)
|
||||||
|
|
||||||
@property
|
def formation_speed(self, is_helo: bool) -> Optional[Speed]:
|
||||||
def formation_speed(self) -> Optional[Speed]:
|
|
||||||
"""The speed of the package when in formation.
|
"""The speed of the package when in formation.
|
||||||
|
|
||||||
If none of the flights in the package will join a formation, this
|
If none of the flights in the package will join a formation, this
|
||||||
@@ -66,7 +65,10 @@ class Package(RadioFrequencyContainer):
|
|||||||
"""
|
"""
|
||||||
speeds = []
|
speeds = []
|
||||||
for flight in self.flights:
|
for flight in self.flights:
|
||||||
if isinstance(flight.flight_plan, FormationFlightPlan):
|
if (
|
||||||
|
isinstance(flight.flight_plan, FormationFlightPlan)
|
||||||
|
and flight.is_helo == is_helo
|
||||||
|
):
|
||||||
speeds.append(flight.flight_plan.best_flight_formation_speed)
|
speeds.append(flight.flight_plan.best_flight_formation_speed)
|
||||||
if not speeds:
|
if not speeds:
|
||||||
return None
|
return None
|
||||||
@@ -179,12 +181,14 @@ class Package(RadioFrequencyContainer):
|
|||||||
FlightType.DEAD,
|
FlightType.DEAD,
|
||||||
FlightType.TRANSPORT,
|
FlightType.TRANSPORT,
|
||||||
FlightType.AIR_ASSAULT,
|
FlightType.AIR_ASSAULT,
|
||||||
|
FlightType.ARMED_RECON,
|
||||||
FlightType.SEAD,
|
FlightType.SEAD,
|
||||||
FlightType.SEAD_SWEEP,
|
FlightType.SEAD_SWEEP,
|
||||||
FlightType.TARCAP,
|
FlightType.TARCAP,
|
||||||
FlightType.BARCAP,
|
FlightType.BARCAP,
|
||||||
FlightType.AEWC,
|
FlightType.AEWC,
|
||||||
FlightType.FERRY,
|
FlightType.FERRY,
|
||||||
|
FlightType.RECOVERY,
|
||||||
FlightType.REFUELING,
|
FlightType.REFUELING,
|
||||||
FlightType.SWEEP,
|
FlightType.SWEEP,
|
||||||
FlightType.SEAD_ESCORT,
|
FlightType.SEAD_ESCORT,
|
||||||
@@ -235,6 +239,14 @@ class Package(RadioFrequencyContainer):
|
|||||||
clone.time_over_target = deepcopy(package.time_over_target)
|
clone.time_over_target = deepcopy(package.time_over_target)
|
||||||
for f in package.flights:
|
for f in package.flights:
|
||||||
cf = Flight.clone_flight(f)
|
cf = Flight.clone_flight(f)
|
||||||
|
cf.flight_plan.layout = deepcopy(f.flight_plan.layout)
|
||||||
cf.package = clone
|
cf.package = clone
|
||||||
clone.add_flight(cf)
|
clone.add_flight(cf)
|
||||||
return clone
|
return clone
|
||||||
|
|
||||||
|
def all_flights_waiting_for_start(self) -> bool:
|
||||||
|
"""Returns True if all flights in the package are waiting for start."""
|
||||||
|
for flight in self.flights:
|
||||||
|
if not flight.state.is_waiting_for_start:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
|
|||||||
from game.coalition import Coalition
|
from game.coalition import Coalition
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass
|
||||||
class PackageWaypoints:
|
class PackageWaypoints:
|
||||||
join: Point
|
join: Point
|
||||||
ingress: Point
|
ingress: Point
|
||||||
@@ -38,7 +38,7 @@ class PackageWaypoints:
|
|||||||
dcs_to_shapely_point(origin.position),
|
dcs_to_shapely_point(origin.position),
|
||||||
dcs_to_shapely_point(package.target.position),
|
dcs_to_shapely_point(package.target.position),
|
||||||
coalition.doctrine,
|
coalition.doctrine,
|
||||||
coalition.opponent.threat_zone.all,
|
coalition.opponent.threat_zone.air_defenses,
|
||||||
)
|
)
|
||||||
ip_solver.set_debug_properties(
|
ip_solver.set_debug_properties(
|
||||||
waypoint_debug_directory() / "IP", coalition.game.theater.terrain
|
waypoint_debug_directory() / "IP", coalition.game.theater.terrain
|
||||||
@@ -51,10 +51,8 @@ class PackageWaypoints:
|
|||||||
ingress_point_shapely.x, ingress_point_shapely.y
|
ingress_point_shapely.x, ingress_point_shapely.y
|
||||||
)
|
)
|
||||||
|
|
||||||
hdg = package.target.position.heading_between_point(ingress_point)
|
tgt_point = package.target.position
|
||||||
# Generate a waypoint randomly between 7 & 9 NM
|
initial_point = PackageWaypoints.get_initial_point(ingress_point, tgt_point)
|
||||||
dist = nautical_miles(random.random() * 2 + 7).meters
|
|
||||||
initial_point = package.target.position.point_from_heading(hdg, dist)
|
|
||||||
|
|
||||||
join_point = JoinZoneGeometry(
|
join_point = JoinZoneGeometry(
|
||||||
package.target.position,
|
package.target.position,
|
||||||
@@ -79,3 +77,11 @@ class PackageWaypoints:
|
|||||||
WaypointBuilder.perturb(join_point),
|
WaypointBuilder.perturb(join_point),
|
||||||
refuel_point,
|
refuel_point,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_initial_point(ingress_point: Point, tgt_point: Point) -> Point:
|
||||||
|
hdg = tgt_point.heading_between_point(ingress_point)
|
||||||
|
# Generate a waypoint randomly between 7 & 9 NM
|
||||||
|
dist = nautical_miles(random.random() * 2 + 7).meters
|
||||||
|
initial_point = tgt_point.point_from_heading(hdg, dist)
|
||||||
|
return initial_point
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from game.utils import Distance, SPEED_OF_SOUND_AT_SEA_LEVEL, Speed, mach, meters
|
from game.utils import Distance, SPEED_OF_SOUND_AT_SEA_LEVEL, Speed, mach
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .flight import Flight
|
from .flight import Flight
|
||||||
@@ -30,8 +30,8 @@ class GroundSpeed:
|
|||||||
# as it can at sea level. This probably isn't great assumption, but
|
# as it can at sea level. This probably isn't great assumption, but
|
||||||
# might. be sufficient given the wiggle room. We can come up with
|
# might. be sufficient given the wiggle room. We can come up with
|
||||||
# another heuristic if needed.
|
# another heuristic if needed.
|
||||||
cruise_mach = max_speed.mach() * (0.60 if flight.is_helo else 0.85)
|
cruise_mach = max_speed.mach() * (0.7 if flight.is_helo else 0.85)
|
||||||
return mach(cruise_mach, altitude if not flight.is_helo else meters(0))
|
return mach(cruise_mach, altitude)
|
||||||
|
|
||||||
|
|
||||||
# TODO: Most if not all of this should move into FlightPlan.
|
# TODO: Most if not all of this should move into FlightPlan.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Support for working with DCS group callsigns."""
|
"""Support for working with DCS group callsigns."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import logging
|
|||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Tuple
|
from typing import Any, Dict, Tuple, Optional
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from packaging.version import Version
|
from packaging.version import Version
|
||||||
@@ -19,8 +19,10 @@ from game.theater.iadsnetwork.iadsnetwork import IadsNetwork
|
|||||||
from game.theater.theaterloader import TheaterLoader
|
from game.theater.theaterloader import TheaterLoader
|
||||||
from game.version import CAMPAIGN_FORMAT_VERSION
|
from game.version import CAMPAIGN_FORMAT_VERSION
|
||||||
from .campaignairwingconfig import CampaignAirWingConfig
|
from .campaignairwingconfig import CampaignAirWingConfig
|
||||||
|
from .campaigncarrierconfig import CampaignCarrierConfig
|
||||||
from .campaigngroundconfig import TgoConfig
|
from .campaigngroundconfig import TgoConfig
|
||||||
from .mizcampaignloader import MizCampaignLoader
|
from .mizcampaignloader import MizCampaignLoader
|
||||||
|
from ..factions import FACTIONS, Faction
|
||||||
|
|
||||||
PERF_FRIENDLY = 0
|
PERF_FRIENDLY = 0
|
||||||
PERF_MEDIUM = 1
|
PERF_MEDIUM = 1
|
||||||
@@ -90,6 +92,16 @@ class Campaign:
|
|||||||
f"Invalid value for recommended_start_date in {path}: {start_date_raw}"
|
f"Invalid value for recommended_start_date in {path}: {start_date_raw}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
player_faction = data.get("recommended_player_faction", "USA 2005")
|
||||||
|
if isinstance(player_faction, dict):
|
||||||
|
faction_name = cls.register_faction(campaign_file.name, player_faction)
|
||||||
|
player_faction = faction_name if faction_name else "USA 2005"
|
||||||
|
|
||||||
|
enemy_faction = data.get("recommended_enemy_faction", "Russia 1990")
|
||||||
|
if isinstance(enemy_faction, dict):
|
||||||
|
faction_name = cls.register_faction(campaign_file.name, enemy_faction)
|
||||||
|
enemy_faction = faction_name if faction_name else "Russia 1990"
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
data["name"],
|
data["name"],
|
||||||
TheaterLoader(data["theater"].lower()).menu_thumbnail_dcs_relative_path,
|
TheaterLoader(data["theater"].lower()).menu_thumbnail_dcs_relative_path,
|
||||||
@@ -97,8 +109,8 @@ class Campaign:
|
|||||||
data.get("authors", "???"),
|
data.get("authors", "???"),
|
||||||
data.get("description", ""),
|
data.get("description", ""),
|
||||||
(version.major, version.minor),
|
(version.major, version.minor),
|
||||||
data.get("recommended_player_faction", "USA 2005"),
|
player_faction,
|
||||||
data.get("recommended_enemy_faction", "Russia 1990"),
|
enemy_faction,
|
||||||
start_date,
|
start_date,
|
||||||
start_time,
|
start_time,
|
||||||
data.get("recommended_player_money", DEFAULT_BUDGET),
|
data.get("recommended_player_money", DEFAULT_BUDGET),
|
||||||
@@ -112,6 +124,19 @@ class Campaign:
|
|||||||
data.get("settings", {}),
|
data.get("settings", {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_faction(
|
||||||
|
cls, filename: str, player_faction: dict[str, Any]
|
||||||
|
) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
f = Faction.from_dict(player_faction)
|
||||||
|
FACTIONS.factions[f.name] = f
|
||||||
|
logging.info(f"Loaded faction from campaign: {filename}")
|
||||||
|
return f.name
|
||||||
|
except Exception:
|
||||||
|
logging.exception(f"Unable to load faction from campaign: {filename}")
|
||||||
|
return None
|
||||||
|
|
||||||
def load_theater(self, advanced_iads: bool) -> ConflictTheater:
|
def load_theater(self, advanced_iads: bool) -> ConflictTheater:
|
||||||
t = TheaterLoader(self.data["theater"].lower()).load()
|
t = TheaterLoader(self.data["theater"].lower()).load()
|
||||||
|
|
||||||
@@ -140,6 +165,13 @@ class Campaign:
|
|||||||
return CampaignAirWingConfig({})
|
return CampaignAirWingConfig({})
|
||||||
return CampaignAirWingConfig.from_campaign_data(squadron_data, theater)
|
return CampaignAirWingConfig.from_campaign_data(squadron_data, theater)
|
||||||
|
|
||||||
|
def load_carrier_config(self) -> CampaignCarrierConfig:
|
||||||
|
try:
|
||||||
|
carrier_data = self.data["carriers"]
|
||||||
|
except KeyError:
|
||||||
|
return CampaignCarrierConfig({})
|
||||||
|
return CampaignCarrierConfig.from_campaign_data(carrier_data)
|
||||||
|
|
||||||
def load_ground_forces_config(self) -> TgoConfig:
|
def load_ground_forces_config(self) -> TgoConfig:
|
||||||
ground_forces = self.data.get("ground_forces", {})
|
ground_forces = self.data.get("ground_forces", {})
|
||||||
if not ground_forces:
|
if not ground_forces:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Optional, TYPE_CHECKING, Union
|
from typing import Any, Optional, TYPE_CHECKING, Union
|
||||||
@@ -24,6 +25,7 @@ class SquadronConfig:
|
|||||||
name: Optional[str]
|
name: Optional[str]
|
||||||
nickname: Optional[str]
|
nickname: Optional[str]
|
||||||
female_pilot_percentage: Optional[int]
|
female_pilot_percentage: Optional[int]
|
||||||
|
aircraft_type: Optional[str]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auto_assignable(self) -> set[FlightType]:
|
def auto_assignable(self) -> set[FlightType]:
|
||||||
@@ -49,6 +51,7 @@ class SquadronConfig:
|
|||||||
data.get("name", None),
|
data.get("name", None),
|
||||||
data.get("nickname", None),
|
data.get("nickname", None),
|
||||||
data.get("female_pilot_percentage", None),
|
data.get("female_pilot_percentage", None),
|
||||||
|
data.get("aircraft_type", None),
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -71,13 +74,30 @@ class CampaignAirWingConfig:
|
|||||||
cls, data: dict[Union[str, int], Any], theater: ConflictTheater
|
cls, data: dict[Union[str, int], Any], theater: ConflictTheater
|
||||||
) -> CampaignAirWingConfig:
|
) -> CampaignAirWingConfig:
|
||||||
by_location: dict[ControlPoint, list[SquadronConfig]] = defaultdict(list)
|
by_location: dict[ControlPoint, list[SquadronConfig]] = defaultdict(list)
|
||||||
|
carriers = theater.find_carriers()
|
||||||
|
lhas = theater.find_lhas()
|
||||||
for base_id, squadron_configs in data.items():
|
for base_id, squadron_configs in data.items():
|
||||||
|
base: Optional[ControlPoint] = None
|
||||||
if isinstance(base_id, int):
|
if isinstance(base_id, int):
|
||||||
base = theater.find_control_point_by_airport_id(base_id)
|
base = theater.find_control_point_by_airport_id(base_id)
|
||||||
else:
|
else:
|
||||||
base = theater.control_point_named(base_id)
|
try:
|
||||||
|
base = theater.control_point_named(base_id)
|
||||||
|
except:
|
||||||
|
logging.warning(
|
||||||
|
f"Control point {base_id} not found, trying to match by full name"
|
||||||
|
)
|
||||||
|
if not base:
|
||||||
|
try:
|
||||||
|
base = theater.control_point_by_full_name(base_id)
|
||||||
|
except KeyError:
|
||||||
|
logging.error(f"Control point {base_id} not found, skipping")
|
||||||
for squadron_data in squadron_configs:
|
for squadron_data in squadron_configs:
|
||||||
by_location[base].append(SquadronConfig.from_data(squadron_data))
|
if base is None:
|
||||||
|
logging.warning(
|
||||||
|
f"Skipping squadron config for unknown base: {base_id}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
by_location[base].append(SquadronConfig.from_data(squadron_data))
|
||||||
|
|
||||||
return CampaignAirWingConfig(by_location)
|
return CampaignAirWingConfig(by_location)
|
||||||
|
|||||||
42
game/campaignloader/campaigncarrierconfig.py
Normal file
42
game/campaignloader/campaigncarrierconfig.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, TYPE_CHECKING
|
||||||
|
|
||||||
|
from game.dcs.shipunittype import ShipUnitType
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CarrierConfig:
|
||||||
|
preferred_name: str
|
||||||
|
preferred_type: ShipUnitType
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls, data: dict[str, Any]) -> CarrierConfig:
|
||||||
|
return CarrierConfig(
|
||||||
|
str(data["preferred_name"]), ShipUnitType.named(data["preferred_type"])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CampaignCarrierConfig:
|
||||||
|
by_original_name: dict[str, CarrierConfig]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_campaign_data(cls, data: dict[str, Any]) -> CampaignCarrierConfig:
|
||||||
|
by_original_name: dict[str, CarrierConfig] = defaultdict()
|
||||||
|
for original_name, carrier_config_data in data.items():
|
||||||
|
try:
|
||||||
|
carrier_config = CarrierConfig.from_data(carrier_config_data)
|
||||||
|
by_original_name[original_name] = carrier_config
|
||||||
|
except KeyError:
|
||||||
|
logging.warning(
|
||||||
|
f"Skipping invalid carrier config for '{original_name}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
return CampaignCarrierConfig(by_original_name)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import random
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from game.squadrons import Squadron
|
from game.squadrons import Squadron
|
||||||
@@ -59,7 +60,7 @@ class DefaultSquadronAssigner:
|
|||||||
) -> Optional[SquadronDef]:
|
) -> Optional[SquadronDef]:
|
||||||
for preferred_aircraft in config.aircraft:
|
for preferred_aircraft in config.aircraft:
|
||||||
squadron_def = self.find_preferred_squadron(
|
squadron_def = self.find_preferred_squadron(
|
||||||
preferred_aircraft, config.primary, control_point
|
preferred_aircraft, config.aircraft_type, config.primary, control_point
|
||||||
)
|
)
|
||||||
if squadron_def is not None:
|
if squadron_def is not None:
|
||||||
return squadron_def
|
return squadron_def
|
||||||
@@ -77,7 +78,11 @@ class DefaultSquadronAssigner:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def find_preferred_squadron(
|
def find_preferred_squadron(
|
||||||
self, preferred_aircraft: str, task: FlightType, control_point: ControlPoint
|
self,
|
||||||
|
preferred_aircraft: str,
|
||||||
|
aircraft_type: Optional[str],
|
||||||
|
task: FlightType,
|
||||||
|
control_point: ControlPoint,
|
||||||
) -> Optional[SquadronDef]:
|
) -> Optional[SquadronDef]:
|
||||||
# Attempt to find a squadron with the name in the request.
|
# Attempt to find a squadron with the name in the request.
|
||||||
squadron_def = self.find_squadron_by_name(
|
squadron_def = self.find_squadron_by_name(
|
||||||
@@ -89,7 +94,7 @@ class DefaultSquadronAssigner:
|
|||||||
# If the name didn't match a squadron available to this coalition, try to find
|
# If the name didn't match a squadron available to this coalition, try to find
|
||||||
# an aircraft with the matching name that meets the requirements.
|
# an aircraft with the matching name that meets the requirements.
|
||||||
try:
|
try:
|
||||||
aircraft = AircraftType.named(preferred_aircraft)
|
aircraft = AircraftType.named(aircraft_type or preferred_aircraft)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
"%s is neither a compatible squadron or a known aircraft type, "
|
"%s is neither a compatible squadron or a known aircraft type, "
|
||||||
@@ -101,9 +106,10 @@ class DefaultSquadronAssigner:
|
|||||||
if aircraft not in self.coalition.faction.all_aircrafts:
|
if aircraft not in self.coalition.faction.all_aircrafts:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
lo = self.coalition.faction.liveries_overrides
|
|
||||||
squadron_def = self.find_squadron_for_airframe(aircraft, task, control_point)
|
squadron_def = self.find_squadron_for_airframe(aircraft, task, control_point)
|
||||||
if squadron_def is not None and lo.get(aircraft) is None:
|
if squadron_def is not None and (
|
||||||
|
squadron_def.livery is not None or squadron_def.livery_set is not None
|
||||||
|
):
|
||||||
return squadron_def
|
return squadron_def
|
||||||
|
|
||||||
# No premade squadron available for this aircraft that meets the requirements,
|
# No premade squadron available for this aircraft that meets the requirements,
|
||||||
@@ -124,11 +130,14 @@ class DefaultSquadronAssigner:
|
|||||||
def find_squadron_for_airframe(
|
def find_squadron_for_airframe(
|
||||||
self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint
|
self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint
|
||||||
) -> Optional[SquadronDef]:
|
) -> Optional[SquadronDef]:
|
||||||
|
choices = []
|
||||||
for squadron in self.air_wing.squadron_defs[aircraft]:
|
for squadron in self.air_wing.squadron_defs[aircraft]:
|
||||||
if not squadron.claimed and self.squadron_compatible_with(
|
if not squadron.claimed and self.squadron_compatible_with(
|
||||||
squadron, task, control_point
|
squadron, task, control_point
|
||||||
):
|
):
|
||||||
return squadron
|
choices.append(squadron)
|
||||||
|
if choices:
|
||||||
|
return random.choice(choices)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def find_squadron_by_name(
|
def find_squadron_by_name(
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ from uuid import UUID
|
|||||||
from dcs import Mission
|
from dcs import Mission
|
||||||
from dcs.countries import CombinedJointTaskForcesBlue, CombinedJointTaskForcesRed
|
from dcs.countries import CombinedJointTaskForcesBlue, CombinedJointTaskForcesRed
|
||||||
from dcs.country import Country
|
from dcs.country import Country
|
||||||
from dcs.planes import F_15C, A_10A, AJS37
|
from dcs.planes import F_15C, A_10A, AJS37, C_130
|
||||||
from dcs.ships import HandyWind, LHA_Tarawa, Stennis, USS_Arleigh_Burke_IIa
|
from dcs.ships import HandyWind, LHA_Tarawa, Stennis, USS_Arleigh_Burke_IIa
|
||||||
from dcs.statics import Fortification, Warehouse
|
from dcs.statics import Fortification, Warehouse
|
||||||
from dcs.terrain import Airport
|
from dcs.terrain import Airport
|
||||||
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
|
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
|
||||||
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
||||||
|
|
||||||
|
from game.controlpoint_influenceradius import ControlPointInfluenceRadius, point_in_zone
|
||||||
from game.point_with_heading import PointWithHeading
|
from game.point_with_heading import PointWithHeading
|
||||||
from game.positioned import Positioned
|
from game.positioned import Positioned
|
||||||
from game.profiling import logged_duration
|
from game.profiling import logged_duration
|
||||||
@@ -24,6 +25,8 @@ from game.theater.controlpoint import (
|
|||||||
Airfield,
|
Airfield,
|
||||||
Carrier,
|
Carrier,
|
||||||
ControlPoint,
|
ControlPoint,
|
||||||
|
ControlPointType,
|
||||||
|
Player,
|
||||||
Fob,
|
Fob,
|
||||||
Lha,
|
Lha,
|
||||||
OffMapSpawn,
|
OffMapSpawn,
|
||||||
@@ -43,6 +46,7 @@ class MizCampaignLoader:
|
|||||||
OFF_MAP_UNIT_TYPE = F_15C.id
|
OFF_MAP_UNIT_TYPE = F_15C.id
|
||||||
GROUND_SPAWN_UNIT_TYPE = A_10A.id
|
GROUND_SPAWN_UNIT_TYPE = A_10A.id
|
||||||
GROUND_SPAWN_ROADBASE_UNIT_TYPE = AJS37.id
|
GROUND_SPAWN_ROADBASE_UNIT_TYPE = AJS37.id
|
||||||
|
GROUND_SPAWN_LARGE_UNIT_TYPE = C_130.id
|
||||||
|
|
||||||
CV_UNIT_TYPE = Stennis.id
|
CV_UNIT_TYPE = Stennis.id
|
||||||
LHA_UNIT_TYPE = LHA_Tarawa.id
|
LHA_UNIT_TYPE = LHA_Tarawa.id
|
||||||
@@ -52,11 +56,13 @@ class MizCampaignLoader:
|
|||||||
|
|
||||||
FOB_UNIT_TYPE = Unarmed.SKP_11.id
|
FOB_UNIT_TYPE = Unarmed.SKP_11.id
|
||||||
FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD", "FARP"]
|
FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD", "FARP"]
|
||||||
|
INVISIBLE_FOB_UNIT_TYPE = Unarmed.M_818.id
|
||||||
|
NEUTRAL_FOB_UNIT_TYPE = Unarmed.KrAZ6322.id
|
||||||
|
|
||||||
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
|
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
|
||||||
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
|
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
|
||||||
MISSILE_SITE_UNIT_TYPE = MissilesSS.Scud_B.id
|
MISSILE_SITE_UNIT_TYPE = MissilesSS.Scud_B.id
|
||||||
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.Hy_launcher.id
|
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.hy_launcher.id
|
||||||
|
|
||||||
COMMAND_CENTER_UNIT_TYPE = Fortification._Command_Center.id
|
COMMAND_CENTER_UNIT_TYPE = Fortification._Command_Center.id
|
||||||
CONNECTION_NODE_UNIT_TYPE = Fortification.Comms_tower_M.id
|
CONNECTION_NODE_UNIT_TYPE = Fortification.Comms_tower_M.id
|
||||||
@@ -73,23 +79,25 @@ class MizCampaignLoader:
|
|||||||
MEDIUM_RANGE_SAM_UNIT_TYPES = {
|
MEDIUM_RANGE_SAM_UNIT_TYPES = {
|
||||||
AirDefence.Hawk_ln.id,
|
AirDefence.Hawk_ln.id,
|
||||||
AirDefence.S_75M_Volhov.id,
|
AirDefence.S_75M_Volhov.id,
|
||||||
AirDefence.X_5p73_s_125_ln.id,
|
AirDefence.x_5p73_s_125_ln.id,
|
||||||
|
AirDefence.NASAMS_LN_B.id,
|
||||||
|
AirDefence.NASAMS_LN_C.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
SHORT_RANGE_SAM_UNIT_TYPES = {
|
SHORT_RANGE_SAM_UNIT_TYPES = {
|
||||||
AirDefence.M1097_Avenger.id,
|
AirDefence.M1097_Avenger.id,
|
||||||
AirDefence.Rapier_fsa_launcher.id,
|
AirDefence.rapier_fsa_launcher.id,
|
||||||
AirDefence.X_2S6_Tunguska.id,
|
AirDefence.x_2S6_Tunguska.id,
|
||||||
AirDefence.Strela_1_9P31.id,
|
AirDefence.Strela_1_9P31.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
AAA_UNIT_TYPES = {
|
AAA_UNIT_TYPES = {
|
||||||
AirDefence.Flak18.id,
|
AirDefence.flak18.id,
|
||||||
AirDefence.Vulcan.id,
|
AirDefence.Vulcan.id,
|
||||||
AirDefence.ZSU_23_4_Shilka.id,
|
AirDefence.ZSU_23_4_Shilka.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
EWR_UNIT_TYPE = AirDefence.X_1L13_EWR.id
|
EWR_UNIT_TYPE = AirDefence.x_1L13_EWR.id
|
||||||
|
|
||||||
ARMOR_GROUP_UNIT_TYPE = Armor.M_1_Abrams.id
|
ARMOR_GROUP_UNIT_TYPE = Armor.M_1_Abrams.id
|
||||||
|
|
||||||
@@ -117,9 +125,13 @@ class MizCampaignLoader:
|
|||||||
def control_point_from_airport(
|
def control_point_from_airport(
|
||||||
self, airport: Airport, ctld_zones: List[Tuple[Point, float]]
|
self, airport: Airport, ctld_zones: List[Tuple[Point, float]]
|
||||||
) -> ControlPoint:
|
) -> ControlPoint:
|
||||||
cp = Airfield(
|
if airport.dynamic_spawn:
|
||||||
airport, self.theater, starts_blue=airport.is_blue(), ctld_zones=ctld_zones
|
starting_coalition = Player.NEUTRAL
|
||||||
)
|
elif airport.is_blue():
|
||||||
|
starting_coalition = Player.BLUE
|
||||||
|
else:
|
||||||
|
starting_coalition = Player.RED
|
||||||
|
cp = Airfield(airport, self.theater, starting_coalition, ctld_zones=ctld_zones)
|
||||||
|
|
||||||
# Use the unlimited aircraft option to determine if an airfield should
|
# Use the unlimited aircraft option to determine if an airfield should
|
||||||
# be owned by the player when the campaign is "inverted".
|
# be owned by the player when the campaign is "inverted".
|
||||||
@@ -127,42 +139,54 @@ class MizCampaignLoader:
|
|||||||
|
|
||||||
return cp
|
return cp
|
||||||
|
|
||||||
def country(self, blue: bool) -> Country:
|
def country(self, blue: Player) -> Country:
|
||||||
country = self.mission.country(
|
if blue.is_blue:
|
||||||
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name
|
country = self.mission.country(self.BLUE_COUNTRY.name)
|
||||||
)
|
else:
|
||||||
|
country = self.mission.country(self.RED_COUNTRY.name)
|
||||||
# Should be guaranteed because we initialized them.
|
# Should be guaranteed because we initialized them.
|
||||||
assert country
|
assert country
|
||||||
return country
|
return country
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def blue(self) -> Country:
|
def blue(self) -> Country:
|
||||||
return self.country(blue=True)
|
return self.country(blue=Player.BLUE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def red(self) -> Country:
|
def red(self) -> Country:
|
||||||
return self.country(blue=False)
|
return self.country(blue=Player.RED)
|
||||||
|
|
||||||
def off_map_spawns(self, blue: bool) -> Iterator[PlaneGroup]:
|
def off_map_spawns(self, blue: Player) -> Iterator[PlaneGroup]:
|
||||||
for group in self.country(blue).plane_group:
|
for group in self.country(blue).plane_group:
|
||||||
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
|
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
|
||||||
yield group
|
yield group
|
||||||
|
|
||||||
def carriers(self, blue: bool) -> Iterator[ShipGroup]:
|
def carriers(self, blue: Player) -> Iterator[ShipGroup]:
|
||||||
for group in self.country(blue).ship_group:
|
for group in self.country(blue).ship_group:
|
||||||
if group.units[0].type == self.CV_UNIT_TYPE:
|
if group.units[0].type == self.CV_UNIT_TYPE:
|
||||||
yield group
|
yield group
|
||||||
|
|
||||||
def lhas(self, blue: bool) -> Iterator[ShipGroup]:
|
def lhas(self, blue: Player) -> Iterator[ShipGroup]:
|
||||||
for group in self.country(blue).ship_group:
|
for group in self.country(blue).ship_group:
|
||||||
if group.units[0].type == self.LHA_UNIT_TYPE:
|
if group.units[0].type == self.LHA_UNIT_TYPE:
|
||||||
yield group
|
yield group
|
||||||
|
|
||||||
def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
|
def fobs(self, blue: Player) -> Iterator[VehicleGroup]:
|
||||||
for group in self.country(blue).vehicle_group:
|
for group in self.country(blue).vehicle_group:
|
||||||
if group.units[0].type == self.FOB_UNIT_TYPE:
|
if group.units[0].type == self.FOB_UNIT_TYPE:
|
||||||
yield group
|
yield group
|
||||||
|
|
||||||
|
def invisible_fobs(self, blue: Player) -> Iterator[VehicleGroup]:
|
||||||
|
for group in self.country(blue).vehicle_group:
|
||||||
|
if group.units[0].type == self.INVISIBLE_FOB_UNIT_TYPE:
|
||||||
|
yield group
|
||||||
|
|
||||||
|
@property
|
||||||
|
def neutral_fobs(self) -> Iterator[VehicleGroup]:
|
||||||
|
for group in self.red.vehicle_group:
|
||||||
|
if group.units[0].type == self.NEUTRAL_FOB_UNIT_TYPE:
|
||||||
|
yield group
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ships(self) -> Iterator[ShipGroup]:
|
def ships(self) -> Iterator[ShipGroup]:
|
||||||
for group in self.red.ship_group:
|
for group in self.red.ship_group:
|
||||||
@@ -235,6 +259,12 @@ class MizCampaignLoader:
|
|||||||
if group.units[0].type == self.GROUND_SPAWN_ROADBASE_UNIT_TYPE:
|
if group.units[0].type == self.GROUND_SPAWN_ROADBASE_UNIT_TYPE:
|
||||||
yield group
|
yield group
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ground_spawns_large(self) -> Iterator[PlaneGroup]:
|
||||||
|
for group in itertools.chain(self.blue.plane_group, self.red.plane_group):
|
||||||
|
if group.units[0].type == self.GROUND_SPAWN_LARGE_UNIT_TYPE:
|
||||||
|
yield group
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ground_spawns(self) -> Iterator[PlaneGroup]:
|
def ground_spawns(self) -> Iterator[PlaneGroup]:
|
||||||
for group in itertools.chain(self.blue.plane_group, self.red.plane_group):
|
for group in itertools.chain(self.blue.plane_group, self.red.plane_group):
|
||||||
@@ -263,16 +293,22 @@ class MizCampaignLoader:
|
|||||||
def scenery(self) -> List[SceneryGroup]:
|
def scenery(self) -> List[SceneryGroup]:
|
||||||
return SceneryGroup.from_trigger_zones(self.mission.triggers._zones)
|
return SceneryGroup.from_trigger_zones(self.mission.triggers._zones)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def cp_influence_zones(self) -> List[ControlPointInfluenceRadius]:
|
||||||
|
return ControlPointInfluenceRadius.from_trigger_zones(
|
||||||
|
self.mission.triggers._zones
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def control_points(self) -> dict[UUID, ControlPoint]:
|
def control_points(self) -> dict[UUID, ControlPoint]:
|
||||||
control_points = {}
|
control_points = {}
|
||||||
for airport in self.mission.terrain.airport_list():
|
for airport in self.mission.terrain.airport_list():
|
||||||
if airport.is_blue() or airport.is_red():
|
if airport.is_blue() or airport.is_red() or airport.is_neutral():
|
||||||
ctld_zones = self.get_ctld_zones(airport.name)
|
ctld_zones = self.get_ctld_zones(airport.name)
|
||||||
control_point = self.control_point_from_airport(airport, ctld_zones)
|
control_point = self.control_point_from_airport(airport, ctld_zones)
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
|
|
||||||
for blue in (False, True):
|
for blue in (Player.RED, Player.BLUE):
|
||||||
for group in self.off_map_spawns(blue):
|
for group in self.off_map_spawns(blue):
|
||||||
control_point = OffMapSpawn(
|
control_point = OffMapSpawn(
|
||||||
str(group.name), group.position, self.theater, starts_blue=blue
|
str(group.name), group.position, self.theater, starts_blue=blue
|
||||||
@@ -303,17 +339,47 @@ class MizCampaignLoader:
|
|||||||
control_point.captured_invert = fob.late_activation
|
control_point.captured_invert = fob.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
|
|
||||||
|
for fob in self.invisible_fobs(blue):
|
||||||
|
ctld_zones = self.get_ctld_zones(fob.name)
|
||||||
|
control_point = Fob(
|
||||||
|
str(fob.name),
|
||||||
|
fob.position,
|
||||||
|
self.theater,
|
||||||
|
starts_blue=blue,
|
||||||
|
ctld_zones=ctld_zones,
|
||||||
|
is_invisible=True,
|
||||||
|
)
|
||||||
|
control_point.captured_invert = fob.late_activation
|
||||||
|
control_points[control_point.id] = control_point
|
||||||
|
|
||||||
|
for fob in self.neutral_fobs:
|
||||||
|
ctld_zones = self.get_ctld_zones(fob.name)
|
||||||
|
control_point = Fob(
|
||||||
|
str(fob.name),
|
||||||
|
fob.position,
|
||||||
|
self.theater,
|
||||||
|
starts_blue=Player.NEUTRAL,
|
||||||
|
ctld_zones=ctld_zones,
|
||||||
|
)
|
||||||
|
control_point.captured_invert = fob.late_activation
|
||||||
|
control_points[control_point.id] = control_point
|
||||||
|
|
||||||
|
if self.cp_influence_zones:
|
||||||
|
for cp in control_points.values():
|
||||||
|
for influence_radius in self.cp_influence_zones:
|
||||||
|
if cp.full_name == influence_radius.cp_name:
|
||||||
|
cp.influence_radius = influence_radius
|
||||||
return control_points
|
return control_points
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def front_line_path_groups(self) -> Iterator[VehicleGroup]:
|
def front_line_path_groups(self) -> Iterator[VehicleGroup]:
|
||||||
for group in self.country(blue=True).vehicle_group:
|
for group in self.country(blue=Player.BLUE).vehicle_group:
|
||||||
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
|
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
|
||||||
yield group
|
yield group
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shipping_lane_groups(self) -> Iterator[ShipGroup]:
|
def shipping_lane_groups(self) -> Iterator[ShipGroup]:
|
||||||
for group in self.country(blue=True).ship_group:
|
for group in self.country(blue=Player.BLUE).ship_group:
|
||||||
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE:
|
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE:
|
||||||
yield group
|
yield group
|
||||||
|
|
||||||
@@ -337,7 +403,7 @@ class MizCampaignLoader:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def cp_convoy_spawns(self) -> Iterator[VehicleGroup]:
|
def cp_convoy_spawns(self) -> Iterator[VehicleGroup]:
|
||||||
for group in self.country(blue=True).vehicle_group:
|
for group in self.country(blue=Player.BLUE).vehicle_group:
|
||||||
if group.units[0].type == self.CP_CONVOY_SPAWN_TYPE:
|
if group.units[0].type == self.CP_CONVOY_SPAWN_TYPE:
|
||||||
yield group
|
yield group
|
||||||
|
|
||||||
@@ -462,7 +528,59 @@ class MizCampaignLoader:
|
|||||||
def objective_info(
|
def objective_info(
|
||||||
self, near: Positioned, allow_naval: bool = False
|
self, near: Positioned, allow_naval: bool = False
|
||||||
) -> Tuple[ControlPoint, Distance]:
|
) -> Tuple[ControlPoint, Distance]:
|
||||||
closest = self.theater.closest_control_point(near.position, allow_naval)
|
zones_containing_point = [
|
||||||
|
z
|
||||||
|
for z in self.cp_influence_zones
|
||||||
|
if point_in_zone(z.zone_def, near.position)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Ensure we only consider naval control points if allow_naval is True
|
||||||
|
candidates = [
|
||||||
|
self.theater.control_point_named(z.cp_name) for z in zones_containing_point
|
||||||
|
]
|
||||||
|
if not allow_naval:
|
||||||
|
candidates = [
|
||||||
|
cp
|
||||||
|
for cp in candidates
|
||||||
|
if cp.cptype
|
||||||
|
not in [
|
||||||
|
ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||||
|
ControlPointType.LHA_GROUP,
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
if candidates:
|
||||||
|
closest = min(
|
||||||
|
candidates, key=lambda cp: cp.position.distance_to_point(near.position)
|
||||||
|
)
|
||||||
|
distance = meters(closest.position.distance_to_point(near.position))
|
||||||
|
return closest, distance
|
||||||
|
|
||||||
|
# If no zones contain the point, find the closest control point without an influence radius
|
||||||
|
if not allow_naval:
|
||||||
|
fallback_candidates = [
|
||||||
|
cp
|
||||||
|
for cp in self.theater.controlpoints
|
||||||
|
if cp.cptype
|
||||||
|
not in [
|
||||||
|
ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||||
|
ControlPointType.LHA_GROUP,
|
||||||
|
]
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
fallback_candidates = self.theater.controlpoints
|
||||||
|
|
||||||
|
fallback_candidates = [
|
||||||
|
cp for cp in fallback_candidates if not cp.influence_radius
|
||||||
|
]
|
||||||
|
if not fallback_candidates:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"All control points have an influence zone but no zones contain {near} at {near.position}"
|
||||||
|
)
|
||||||
|
closest = min(
|
||||||
|
fallback_candidates,
|
||||||
|
key=lambda cp: cp.position.distance_to_point(near.position),
|
||||||
|
)
|
||||||
distance = meters(closest.position.distance_to_point(near.position))
|
distance = meters(closest.position.distance_to_point(near.position))
|
||||||
return closest, distance
|
return closest, distance
|
||||||
|
|
||||||
@@ -534,6 +652,10 @@ class MizCampaignLoader:
|
|||||||
closest, distance = self.objective_info(plane_group)
|
closest, distance = self.objective_info(plane_group)
|
||||||
self._add_ground_spawn(closest.ground_spawns_roadbase, plane_group)
|
self._add_ground_spawn(closest.ground_spawns_roadbase, plane_group)
|
||||||
|
|
||||||
|
for plane_group in self.ground_spawns_large:
|
||||||
|
closest, distance = self.objective_info(plane_group)
|
||||||
|
self._add_ground_spawn(closest.ground_spawns_large, plane_group)
|
||||||
|
|
||||||
for plane_group in self.ground_spawns:
|
for plane_group in self.ground_spawns:
|
||||||
closest, distance = self.objective_info(plane_group)
|
closest, distance = self.objective_info(plane_group)
|
||||||
self._add_ground_spawn(closest.ground_spawns, plane_group)
|
self._add_ground_spawn(closest.ground_spawns, plane_group)
|
||||||
@@ -582,9 +704,18 @@ class MizCampaignLoader:
|
|||||||
self.add_preset_locations()
|
self.add_preset_locations()
|
||||||
self.add_supply_routes()
|
self.add_supply_routes()
|
||||||
self.add_shipping_lanes()
|
self.add_shipping_lanes()
|
||||||
|
self.add_rebel_zones()
|
||||||
|
|
||||||
def get_ctld_zones(self, prefix: str) -> List[Tuple[Point, float]]:
|
def get_ctld_zones(self, prefix: str) -> List[Tuple[Point, float]]:
|
||||||
zones = [t for t in self.mission.triggers.zones() if prefix + " CTLD" in t.name]
|
zones = [t for t in self.mission.triggers.zones() if prefix + " CTLD" in t.name]
|
||||||
for z in zones:
|
for z in zones:
|
||||||
self.mission.triggers.zones().remove(z)
|
self.mission.triggers.zones().remove(z)
|
||||||
return [(z.position, z.radius) for z in zones]
|
return [(z.position, z.radius) for z in zones]
|
||||||
|
|
||||||
|
def add_rebel_zones(self) -> None:
|
||||||
|
zones = [
|
||||||
|
t for t in self.mission.triggers.zones() if t.name.startswith("Rebels")
|
||||||
|
]
|
||||||
|
for z in zones:
|
||||||
|
self.mission.triggers.zones().remove(z)
|
||||||
|
self.theater.add_rebel_zones(zones)
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class SquadronDefGenerator:
|
|||||||
def generate_for_task(
|
def generate_for_task(
|
||||||
self, task: FlightType, control_point: ControlPoint
|
self, task: FlightType, control_point: ControlPoint
|
||||||
) -> Optional[SquadronDef]:
|
) -> Optional[SquadronDef]:
|
||||||
|
settings = control_point.coalition.game.settings
|
||||||
|
squadron_random_chance = settings.squadron_random_chance
|
||||||
aircraft_choice: Optional[AircraftType] = None
|
aircraft_choice: Optional[AircraftType] = None
|
||||||
for aircraft in AircraftType.priority_list_for_task(task):
|
for aircraft in AircraftType.priority_list_for_task(task):
|
||||||
if aircraft not in self.faction.all_aircrafts:
|
if aircraft not in self.faction.all_aircrafts:
|
||||||
@@ -30,9 +32,9 @@ class SquadronDefGenerator:
|
|||||||
if not control_point.can_operate(aircraft):
|
if not control_point.can_operate(aircraft):
|
||||||
continue
|
continue
|
||||||
aircraft_choice = aircraft
|
aircraft_choice = aircraft
|
||||||
# 50/50 chance to keep looking for an aircraft that isn't as far up the
|
# squadron_random_chance percent chance to keep looking for an aircraft that isn't as far up the
|
||||||
# priority list to maintain some unit variety.
|
# priority list to maintain some unit variety.
|
||||||
if random.choice([True, False]):
|
if squadron_random_chance >= random.randint(1, 100):
|
||||||
break
|
break
|
||||||
|
|
||||||
if aircraft_choice is None:
|
if aircraft_choice is None:
|
||||||
@@ -47,6 +49,7 @@ class SquadronDefGenerator:
|
|||||||
role="Flying Squadron",
|
role="Flying Squadron",
|
||||||
aircraft=aircraft,
|
aircraft=aircraft,
|
||||||
livery=None,
|
livery=None,
|
||||||
|
livery_set=[],
|
||||||
auto_assignable_mission_types=set(aircraft.iter_task_capabilities()),
|
auto_assignable_mission_types=set(aircraft.iter_task_capabilities()),
|
||||||
radio_presets={},
|
radio_presets={},
|
||||||
operating_bases=OperatingBases.default_for_aircraft(aircraft),
|
operating_bases=OperatingBases.default_for_aircraft(aircraft),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from game.procurement import AircraftProcurementRequest, ProcurementAi
|
|||||||
from game.profiling import MultiEventTracer, logged_duration
|
from game.profiling import MultiEventTracer, logged_duration
|
||||||
from game.squadrons import AirWing
|
from game.squadrons import AirWing
|
||||||
from game.theater.bullseye import Bullseye
|
from game.theater.bullseye import Bullseye
|
||||||
|
from game.theater.player import Player
|
||||||
from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
||||||
from game.threatzones import ThreatZones
|
from game.threatzones import ThreatZones
|
||||||
from game.transfers import PendingTransfers
|
from game.transfers import PendingTransfers
|
||||||
@@ -32,7 +33,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class Coalition:
|
class Coalition:
|
||||||
def __init__(
|
def __init__(
|
||||||
self, game: Game, faction: Faction, budget: float, player: bool
|
self, game: Game, faction: Faction, budget: float, player: Player
|
||||||
) -> None:
|
) -> None:
|
||||||
self.game = game
|
self.game = game
|
||||||
self.player = player
|
self.player = player
|
||||||
@@ -68,9 +69,11 @@ class Coalition:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def coalition_id(self) -> int:
|
def coalition_id(self) -> int:
|
||||||
if self.player:
|
if self.player.is_blue:
|
||||||
return 2
|
return 2
|
||||||
return 1
|
elif self.player.is_red:
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def opponent(self) -> Coalition:
|
def opponent(self) -> Coalition:
|
||||||
@@ -95,12 +98,23 @@ class Coalition:
|
|||||||
state = self.__dict__.copy()
|
state = self.__dict__.copy()
|
||||||
# Avoid persisting any volatile types that can be deterministically
|
# Avoid persisting any volatile types that can be deterministically
|
||||||
# recomputed on load for the sake of save compatibility.
|
# recomputed on load for the sake of save compatibility.
|
||||||
del state["_threat_zone"]
|
|
||||||
del state["_navmesh"]
|
|
||||||
del state["faker"]
|
del state["faker"]
|
||||||
|
# TODO: Figure out why this is needed after adding neutral point support
|
||||||
|
if state["player"] != Player.NEUTRAL:
|
||||||
|
del state["_threat_zone"]
|
||||||
|
del state["_navmesh"]
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
|
# Migration: Convert old boolean player values to Player enum
|
||||||
|
if "player" in state and isinstance(state["player"], bool):
|
||||||
|
from game.theater.player import Player
|
||||||
|
|
||||||
|
if state["player"]:
|
||||||
|
state["player"] = Player.BLUE
|
||||||
|
else:
|
||||||
|
state["player"] = Player.RED
|
||||||
|
|
||||||
self.__dict__.update(state)
|
self.__dict__.update(state)
|
||||||
# Regenerate any state that was not persisted.
|
# Regenerate any state that was not persisted.
|
||||||
self.on_load()
|
self.on_load()
|
||||||
@@ -203,7 +217,7 @@ class Coalition:
|
|||||||
squadron.refund_orders()
|
squadron.refund_orders()
|
||||||
|
|
||||||
def plan_missions(self, now: datetime) -> None:
|
def plan_missions(self, now: datetime) -> None:
|
||||||
color = "Blue" if self.player else "Red"
|
color = "Blue" if self.player.is_blue else "Red"
|
||||||
with MultiEventTracer() as tracer:
|
with MultiEventTracer() as tracer:
|
||||||
with tracer.trace(f"{color} mission planning"):
|
with tracer.trace(f"{color} mission planning"):
|
||||||
with tracer.trace(f"{color} mission identification"):
|
with tracer.trace(f"{color} mission identification"):
|
||||||
@@ -220,7 +234,7 @@ class Coalition:
|
|||||||
# to ground forces and 1400 to aircraft. After that the budget will be spent
|
# to ground forces and 1400 to aircraft. After that the budget will be spent
|
||||||
# proportionally based on how much is already invested.
|
# proportionally based on how much is already invested.
|
||||||
|
|
||||||
if self.player:
|
if self.player.is_blue:
|
||||||
manage_runways = self.game.settings.automate_runway_repair
|
manage_runways = self.game.settings.automate_runway_repair
|
||||||
manage_front_line = self.game.settings.automate_front_line_reinforcements
|
manage_front_line = self.game.settings.automate_front_line_reinforcements
|
||||||
manage_aircraft = self.game.settings.automate_aircraft_reinforcements
|
manage_aircraft = self.game.settings.automate_aircraft_reinforcements
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ from typing import Iterator, TYPE_CHECKING
|
|||||||
|
|
||||||
from game.ato.flighttype import FlightType
|
from game.ato.flighttype import FlightType
|
||||||
from game.ato.traveltime import TotEstimator
|
from game.ato.traveltime import TotEstimator
|
||||||
from game.theater import MissionTarget
|
from game.theater import MissionTarget, NavalControlPoint
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.coalition import Coalition
|
from game.coalition import Coalition
|
||||||
|
from game.ato import Package
|
||||||
|
|
||||||
|
|
||||||
class MissionScheduler:
|
class MissionScheduler:
|
||||||
@@ -40,6 +41,13 @@ class MissionScheduler:
|
|||||||
p for p in self.coalition.ato.packages if p.primary_task not in dca_types
|
p for p in self.coalition.ato.packages if p.primary_task not in dca_types
|
||||||
]
|
]
|
||||||
|
|
||||||
|
previous_aewc_end_time: dict[MissionTarget, datetime] = defaultdict(now.replace)
|
||||||
|
|
||||||
|
max_simultaneous_recovery_tankers = 2 # TODO: make configurable
|
||||||
|
carrier_etas: dict[MissionTarget, list[datetime]] = defaultdict(list)
|
||||||
|
max_carrier_simultaneous_barcaps = 2 # TODO: make configurable
|
||||||
|
carrier_barcaps: dict[MissionTarget, int] = defaultdict(int)
|
||||||
|
|
||||||
start_time = start_time_generator(
|
start_time = start_time_generator(
|
||||||
count=len(non_dca_packages),
|
count=len(non_dca_packages),
|
||||||
earliest=5 * 60,
|
earliest=5 * 60,
|
||||||
@@ -47,6 +55,8 @@ class MissionScheduler:
|
|||||||
margin=5 * 60,
|
margin=5 * 60,
|
||||||
)
|
)
|
||||||
for package in self.coalition.ato.packages:
|
for package in self.coalition.ato.packages:
|
||||||
|
if package.primary_task is FlightType.RECOVERY:
|
||||||
|
continue
|
||||||
tot = TotEstimator(package).earliest_tot(now)
|
tot = TotEstimator(package).earliest_tot(now)
|
||||||
if package.primary_task in dca_types:
|
if package.primary_task in dca_types:
|
||||||
previous_end_time = previous_cap_end_time[package.target]
|
previous_end_time = previous_cap_end_time[package.target]
|
||||||
@@ -58,14 +68,27 @@ class MissionScheduler:
|
|||||||
else:
|
else:
|
||||||
package.time_over_target = previous_end_time
|
package.time_over_target = previous_end_time
|
||||||
|
|
||||||
departure_time = package.mission_departure_time
|
departure_time = self._get_departure_time(package)
|
||||||
# Should be impossible for CAPs
|
|
||||||
if departure_time is None:
|
if departure_time is None:
|
||||||
logging.error(f"Could not determine mission end time for {package}")
|
|
||||||
continue
|
continue
|
||||||
previous_cap_end_time[package.target] = departure_time
|
is_naval_cp = isinstance(package.target, NavalControlPoint)
|
||||||
|
count = carrier_barcaps[package.target]
|
||||||
|
if count >= max_carrier_simultaneous_barcaps - 1 and is_naval_cp:
|
||||||
|
previous_cap_end_time[package.target] = departure_time
|
||||||
|
carrier_barcaps[package.target] = 0
|
||||||
|
elif is_naval_cp:
|
||||||
|
carrier_barcaps[package.target] += 1
|
||||||
|
elif not is_naval_cp:
|
||||||
|
previous_cap_end_time[package.target] = departure_time
|
||||||
elif package.auto_asap:
|
elif package.auto_asap:
|
||||||
package.set_tot_asap(now)
|
package.set_tot_asap(now)
|
||||||
|
elif package.primary_task is FlightType.AEWC:
|
||||||
|
last = previous_aewc_end_time[package.target]
|
||||||
|
package.time_over_target = tot if tot > last else last
|
||||||
|
departure_time = self._get_departure_time(package)
|
||||||
|
if departure_time is None:
|
||||||
|
continue
|
||||||
|
previous_aewc_end_time[package.target] = departure_time
|
||||||
else:
|
else:
|
||||||
# But other packages should be spread out a bit. Note that take
|
# But other packages should be spread out a bit. Note that take
|
||||||
# times are delayed, but all aircraft will become active at
|
# times are delayed, but all aircraft will become active at
|
||||||
@@ -74,3 +97,34 @@ class MissionScheduler:
|
|||||||
# to be present. Runway and air started aircraft will be
|
# to be present. Runway and air started aircraft will be
|
||||||
# delayed until their takeoff time by AirConflictGenerator.
|
# delayed until their takeoff time by AirConflictGenerator.
|
||||||
package.time_over_target = next(start_time) + tot
|
package.time_over_target = next(start_time) + tot
|
||||||
|
for f in package.flights:
|
||||||
|
if f.departure.is_fleet and not f.is_helo:
|
||||||
|
carrier_etas[f.departure].append(
|
||||||
|
f.flight_plan.landing_time - timedelta(minutes=10)
|
||||||
|
)
|
||||||
|
|
||||||
|
# division by 2 is meant to provide some leeway to avoid filtering out too many ETAs
|
||||||
|
duration = self.coalition.game.settings.desired_tanker_on_station_time / 2
|
||||||
|
|
||||||
|
for cp in carrier_etas:
|
||||||
|
filtered: list[datetime] = []
|
||||||
|
for eta in sorted(carrier_etas[cp]):
|
||||||
|
count = len([t for t in filtered if eta < t + duration])
|
||||||
|
if count < max_simultaneous_recovery_tankers:
|
||||||
|
filtered.append(eta)
|
||||||
|
carrier_etas[cp] = filtered
|
||||||
|
for package in [
|
||||||
|
p
|
||||||
|
for p in self.coalition.ato.packages
|
||||||
|
if p.primary_task is FlightType.RECOVERY
|
||||||
|
]:
|
||||||
|
if carrier_etas[package.target]:
|
||||||
|
package.time_over_target = carrier_etas[package.target].pop(0)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_departure_time(package: Package) -> datetime | None:
|
||||||
|
departure_time = package.mission_departure_time
|
||||||
|
# Should be impossible for CAP/AEWC
|
||||||
|
if departure_time is None:
|
||||||
|
logging.error(f"Could not determine mission end time for {package}")
|
||||||
|
return departure_time
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import itertools
|
||||||
import math
|
import math
|
||||||
import operator
|
import operator
|
||||||
from collections.abc import Iterable, Iterator
|
from collections.abc import Iterable, Iterator
|
||||||
@@ -15,6 +16,8 @@ from game.theater import (
|
|||||||
MissionTarget,
|
MissionTarget,
|
||||||
OffMapSpawn,
|
OffMapSpawn,
|
||||||
ParkingType,
|
ParkingType,
|
||||||
|
NavalControlPoint,
|
||||||
|
Player,
|
||||||
)
|
)
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
BuildingGroundObject,
|
BuildingGroundObject,
|
||||||
@@ -34,7 +37,7 @@ MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
|
|||||||
class ObjectiveFinder:
|
class ObjectiveFinder:
|
||||||
"""Identifies potential objectives for the mission planner."""
|
"""Identifies potential objectives for the mission planner."""
|
||||||
|
|
||||||
def __init__(self, game: Game, is_player: bool) -> None:
|
def __init__(self, game: Game, is_player: Player) -> None:
|
||||||
self.game = game
|
self.game = game
|
||||||
self.is_player = is_player
|
self.is_player = is_player
|
||||||
|
|
||||||
@@ -147,10 +150,13 @@ class ObjectiveFinder:
|
|||||||
if isinstance(cp, OffMapSpawn):
|
if isinstance(cp, OffMapSpawn):
|
||||||
# Off-map spawn locations don't need protection.
|
# Off-map spawn locations don't need protection.
|
||||||
continue
|
continue
|
||||||
|
if isinstance(cp, NavalControlPoint):
|
||||||
|
yield cp # always consider CVN/LHA as vulnerable
|
||||||
|
continue
|
||||||
airfields_in_proximity = self.closest_airfields_to(cp)
|
airfields_in_proximity = self.closest_airfields_to(cp)
|
||||||
airbase_threat_range = self.game.settings.airbase_threat_range
|
airbase_threat_range = self.game.settings.airbase_threat_range
|
||||||
if (
|
if (
|
||||||
not self.is_player
|
self.is_player.is_red
|
||||||
and randint(1, 100)
|
and randint(1, 100)
|
||||||
> self.game.settings.opfor_autoplanner_aggressiveness
|
> self.game.settings.opfor_autoplanner_aggressiveness
|
||||||
):
|
):
|
||||||
@@ -167,24 +173,6 @@ class ObjectiveFinder:
|
|||||||
yield cp
|
yield cp
|
||||||
break
|
break
|
||||||
|
|
||||||
def vulnerable_enemy_control_points(self) -> Iterator[ControlPoint]:
|
|
||||||
"""Iterates over enemy CPs that are vulnerable to Air Assault.
|
|
||||||
Vulnerability is defined as any unit being alive in the CP's "blocking_capture" groups.
|
|
||||||
"""
|
|
||||||
for cp in self.enemy_control_points():
|
|
||||||
include = True
|
|
||||||
for tgo in cp.connected_objectives:
|
|
||||||
if tgo.distance_to(cp) > cp.CAPTURE_DISTANCE.meters:
|
|
||||||
continue
|
|
||||||
for u in tgo.units:
|
|
||||||
if u.is_vehicle and u.alive:
|
|
||||||
include = False
|
|
||||||
break
|
|
||||||
if not include:
|
|
||||||
break
|
|
||||||
if include:
|
|
||||||
yield cp
|
|
||||||
|
|
||||||
def oca_targets(self, min_aircraft: int) -> Iterator[ControlPoint]:
|
def oca_targets(self, min_aircraft: int) -> Iterator[ControlPoint]:
|
||||||
parking_type = ParkingType()
|
parking_type = ParkingType()
|
||||||
parking_type.include_rotary_wing = True
|
parking_type.include_rotary_wing = True
|
||||||
@@ -230,7 +218,7 @@ class ObjectiveFinder:
|
|||||||
|
|
||||||
def farthest_friendly_control_point(self) -> ControlPoint:
|
def farthest_friendly_control_point(self) -> ControlPoint:
|
||||||
"""Finds the friendly control point that is farthest from any threats."""
|
"""Finds the friendly control point that is farthest from any threats."""
|
||||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
threat_zones = self.game.threat_zone_for(self.is_player.opponent)
|
||||||
|
|
||||||
farthest = None
|
farthest = None
|
||||||
max_distance = meters(0)
|
max_distance = meters(0)
|
||||||
@@ -248,7 +236,7 @@ class ObjectiveFinder:
|
|||||||
|
|
||||||
def closest_friendly_control_point(self) -> ControlPoint:
|
def closest_friendly_control_point(self) -> ControlPoint:
|
||||||
"""Finds the friendly control point that is closest to any threats."""
|
"""Finds the friendly control point that is closest to any threats."""
|
||||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
threat_zones = self.game.threat_zone_for(self.is_player.opponent)
|
||||||
|
|
||||||
closest = None
|
closest = None
|
||||||
min_distance = meters(math.inf)
|
min_distance = meters(math.inf)
|
||||||
@@ -264,25 +252,54 @@ class ObjectiveFinder:
|
|||||||
raise RuntimeError("Found no friendly control points. You probably lost.")
|
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||||
return closest
|
return closest
|
||||||
|
|
||||||
|
def friendly_naval_control_points(self) -> Iterator[ControlPoint]:
|
||||||
|
return (cp for cp in self.friendly_control_points() if cp.is_fleet)
|
||||||
|
|
||||||
def enemy_control_points(self) -> Iterator[ControlPoint]:
|
def enemy_control_points(self) -> Iterator[ControlPoint]:
|
||||||
"""Iterates over all enemy control points."""
|
"""Iterates over all enemy control points."""
|
||||||
return (
|
return (
|
||||||
c
|
c
|
||||||
for c in self.game.theater.controlpoints
|
for c in self.game.theater.controlpoints
|
||||||
if not c.is_friendly(self.is_player)
|
if not c.is_friendly(self.is_player) and c.captured != Player.NEUTRAL
|
||||||
)
|
)
|
||||||
|
|
||||||
def prioritized_unisolated_points(self) -> list[ControlPoint]:
|
def prioritized_points(self) -> list[ControlPoint]:
|
||||||
prioritized = []
|
prioritized = []
|
||||||
capturable_later = []
|
capturable_later = []
|
||||||
for cp in self.game.theater.control_points_for(not self.is_player):
|
isolated = []
|
||||||
|
for cp in self.game.theater.control_points_for(self.is_player.opponent):
|
||||||
if cp.is_isolated:
|
if cp.is_isolated:
|
||||||
|
isolated.append(cp)
|
||||||
continue
|
continue
|
||||||
if cp.has_active_frontline:
|
if cp.has_active_frontline:
|
||||||
prioritized.append(cp)
|
prioritized.append(cp)
|
||||||
else:
|
else:
|
||||||
capturable_later.append(cp)
|
capturable_later.append(cp)
|
||||||
prioritized.extend(self._targets_by_range(capturable_later))
|
prioritized.extend(self._targets_by_range(capturable_later))
|
||||||
|
prioritized.extend(self._targets_by_range(isolated))
|
||||||
|
return prioritized
|
||||||
|
|
||||||
|
def air_assault_targets(self) -> list[ControlPoint]:
|
||||||
|
"""Returns control points suitable for air assault missions, including neutral bases."""
|
||||||
|
prioritized = []
|
||||||
|
capturable_later = []
|
||||||
|
isolated = []
|
||||||
|
|
||||||
|
combined_control_points = itertools.chain(
|
||||||
|
self.game.theater.control_points_for(self.is_player.opponent),
|
||||||
|
self.game.theater.control_points_for(Player.NEUTRAL),
|
||||||
|
)
|
||||||
|
|
||||||
|
for cp in combined_control_points:
|
||||||
|
if cp.is_isolated:
|
||||||
|
isolated.append(cp)
|
||||||
|
continue
|
||||||
|
if cp.has_active_frontline:
|
||||||
|
prioritized.append(cp)
|
||||||
|
else:
|
||||||
|
capturable_later.append(cp)
|
||||||
|
prioritized.extend(self._targets_by_range(capturable_later))
|
||||||
|
prioritized.extend(self._targets_by_range(isolated))
|
||||||
return prioritized
|
return prioritized
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from typing import Optional, TYPE_CHECKING
|
|||||||
|
|
||||||
from game.theater import ControlPoint, MissionTarget, OffMapSpawn
|
from game.theater import ControlPoint, MissionTarget, OffMapSpawn
|
||||||
from game.utils import nautical_miles
|
from game.utils import nautical_miles
|
||||||
|
from ..ato import FlightType
|
||||||
from ..ato.flight import Flight
|
from ..ato.flight import Flight
|
||||||
from ..ato.package import Package
|
from ..ato.package import Package
|
||||||
from ..ato.starttype import StartType
|
from ..ato.starttype import StartType
|
||||||
@@ -46,10 +47,19 @@ class PackageBuilder:
|
|||||||
caller should return any previously planned flights to the inventory
|
caller should return any previously planned flights to the inventory
|
||||||
using release_planned_aircraft.
|
using release_planned_aircraft.
|
||||||
"""
|
"""
|
||||||
|
target = self.package.target
|
||||||
|
heli = False
|
||||||
pf = self.package.primary_flight
|
pf = self.package.primary_flight
|
||||||
heli = pf.is_helo if pf else False
|
if pf:
|
||||||
|
target = (
|
||||||
|
pf.departure
|
||||||
|
if pf.flight_type
|
||||||
|
in [FlightType.AEWC, FlightType.REFUELING, FlightType.RECOVERY]
|
||||||
|
else target
|
||||||
|
)
|
||||||
|
heli = pf.is_helo
|
||||||
squadron = self.air_wing.best_squadron_for(
|
squadron = self.air_wing.best_squadron_for(
|
||||||
self.package.target,
|
target,
|
||||||
plan.task,
|
plan.task,
|
||||||
plan.num_aircraft,
|
plan.num_aircraft,
|
||||||
heli,
|
heli,
|
||||||
@@ -76,6 +86,16 @@ class PackageBuilder:
|
|||||||
member.assign_tgp_laser_code(
|
member.assign_tgp_laser_code(
|
||||||
self.laser_code_registry.alloc_laser_code()
|
self.laser_code_registry.alloc_laser_code()
|
||||||
)
|
)
|
||||||
|
# If this is a client flight, set the start_type again to match the configured default
|
||||||
|
# https://github.com/dcs-liberation/dcs_liberation/issues/1567
|
||||||
|
if (
|
||||||
|
squadron.location.required_aircraft_start_type is None
|
||||||
|
and flight.roster is not None
|
||||||
|
and flight.roster.player_count > 0
|
||||||
|
):
|
||||||
|
flight.start_type = (
|
||||||
|
squadron.coalition.game.settings.default_start_type_client
|
||||||
|
)
|
||||||
self.package.add_flight(flight)
|
self.package.add_flight(flight)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ class PackageFulfiller:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_player(self) -> bool:
|
def is_player(self) -> bool:
|
||||||
return self.coalition.player
|
if self.coalition.player.is_blue:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ato(self) -> AirTaskingOrder:
|
def ato(self) -> AirTaskingOrder:
|
||||||
@@ -83,12 +85,19 @@ class PackageFulfiller:
|
|||||||
purchase_multiplier: int,
|
purchase_multiplier: int,
|
||||||
ignore_range: bool = False,
|
ignore_range: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
target = mission.location
|
||||||
|
pf = builder.package.primary_flight
|
||||||
|
if (
|
||||||
|
pf
|
||||||
|
and pf.flight_type in [FlightType.AEWC, FlightType.REFUELING]
|
||||||
|
and flight.task is FlightType.ESCORT
|
||||||
|
):
|
||||||
|
target = pf.departure
|
||||||
if not builder.plan_flight(flight, ignore_range):
|
if not builder.plan_flight(flight, ignore_range):
|
||||||
pf = builder.package.primary_flight
|
|
||||||
heli = pf.is_helo if pf else False
|
heli = pf.is_helo if pf else False
|
||||||
missing_types.add(flight.task)
|
missing_types.add(flight.task)
|
||||||
purchase_order = AircraftProcurementRequest(
|
purchase_order = AircraftProcurementRequest(
|
||||||
near=mission.location,
|
near=target,
|
||||||
task_capability=flight.task,
|
task_capability=flight.task,
|
||||||
number=flight.num_aircraft * purchase_multiplier,
|
number=flight.num_aircraft * purchase_multiplier,
|
||||||
heli=heli,
|
heli=heli,
|
||||||
@@ -133,6 +142,21 @@ class PackageFulfiller:
|
|||||||
threats[EscortType.Sead] = True
|
threats[EscortType.Sead] = True
|
||||||
return threats
|
return threats
|
||||||
|
|
||||||
|
def can_plan_escort(self, type: EscortType) -> bool:
|
||||||
|
if type == EscortType.AirToAir:
|
||||||
|
return self.air_wing_can_plan(FlightType.ESCORT)
|
||||||
|
elif type == EscortType.Sead:
|
||||||
|
for task in [
|
||||||
|
FlightType.SEAD,
|
||||||
|
FlightType.SEAD_ESCORT,
|
||||||
|
FlightType.SEAD_SWEEP,
|
||||||
|
]:
|
||||||
|
if self.air_wing_can_plan(task):
|
||||||
|
return True
|
||||||
|
elif type == EscortType.Refuel:
|
||||||
|
return self.air_wing_can_plan(FlightType.REFUELING)
|
||||||
|
return False
|
||||||
|
|
||||||
def plan_mission(
|
def plan_mission(
|
||||||
self,
|
self,
|
||||||
mission: ProposedMission,
|
mission: ProposedMission,
|
||||||
@@ -207,7 +231,9 @@ class PackageFulfiller:
|
|||||||
# This list was generated from the not None set, so this should be
|
# This list was generated from the not None set, so this should be
|
||||||
# impossible.
|
# impossible.
|
||||||
assert escort.escort_type is not None
|
assert escort.escort_type is not None
|
||||||
if needed_escorts[escort.escort_type]:
|
if needed_escorts[escort.escort_type] and self.can_plan_escort(
|
||||||
|
escort.escort_type
|
||||||
|
):
|
||||||
with tracer.trace("Flight planning"):
|
with tracer.trace("Flight planning"):
|
||||||
self.plan_flight(
|
self.plan_flight(
|
||||||
mission, escort, builder, missing_types, purchase_multiplier
|
mission, escort, builder, missing_types, purchase_multiplier
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
from game.commander.tasks.primitive.armedrecon import PlanArmedRecon
|
||||||
from game.commander.tasks.primitive.bai import PlanBai
|
from game.commander.tasks.primitive.bai import PlanBai
|
||||||
from game.commander.theaterstate import TheaterState
|
from game.commander.theaterstate import TheaterState
|
||||||
from game.htn import CompoundTask, Method
|
from game.htn import CompoundTask, Method
|
||||||
@@ -10,3 +11,7 @@ class AttackBattlePositions(CompoundTask[TheaterState]):
|
|||||||
for battle_positions in state.enemy_battle_positions.values():
|
for battle_positions in state.enemy_battle_positions.values():
|
||||||
for battle_position in battle_positions.in_priority_order:
|
for battle_position in battle_positions.in_priority_order:
|
||||||
yield [PlanBai(battle_position)]
|
yield [PlanBai(battle_position)]
|
||||||
|
# Only plan against the 2 most important CPs
|
||||||
|
for cp in state.control_point_priority_queue[:2]:
|
||||||
|
if not cp.is_fleet:
|
||||||
|
yield [PlanArmedRecon(cp)]
|
||||||
|
|||||||
@@ -8,8 +8,4 @@ from game.htn import CompoundTask, Method
|
|||||||
class AttackShips(CompoundTask[TheaterState]):
|
class AttackShips(CompoundTask[TheaterState]):
|
||||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
for ship in state.enemy_ships:
|
for ship in state.enemy_ships:
|
||||||
# Ammo depots are targeted based on the needs of the front line by
|
yield [PlanAntiShip(ship)]
|
||||||
# ReduceEnemyFrontLineCapacity. No reason to target them before that front
|
|
||||||
# line is active.
|
|
||||||
if ship.has_live_radar_sam:
|
|
||||||
yield [PlanAntiShip(ship)]
|
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ from game.commander.tasks.compound.destroyenemygroundunits import (
|
|||||||
from game.commander.tasks.compound.reduceenemyfrontlinecapacity import (
|
from game.commander.tasks.compound.reduceenemyfrontlinecapacity import (
|
||||||
ReduceEnemyFrontLineCapacity,
|
ReduceEnemyFrontLineCapacity,
|
||||||
)
|
)
|
||||||
from game.commander.tasks.primitive.airassault import PlanAirAssault
|
|
||||||
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
|
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
|
||||||
from game.commander.theaterstate import TheaterState
|
from game.commander.theaterstate import TheaterState
|
||||||
from game.htn import CompoundTask, Method
|
from game.htn import CompoundTask, Method
|
||||||
from game.theater import ControlPoint, FrontLine
|
from game.theater import ControlPoint, FrontLine, Player
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -23,17 +22,16 @@ class CaptureBase(CompoundTask[TheaterState]):
|
|||||||
yield [DestroyEnemyGroundUnits(self.front_line)]
|
yield [DestroyEnemyGroundUnits(self.front_line)]
|
||||||
if self.worth_destroying_ammo_depots(state):
|
if self.worth_destroying_ammo_depots(state):
|
||||||
yield [ReduceEnemyFrontLineCapacity(self.enemy_cp(state))]
|
yield [ReduceEnemyFrontLineCapacity(self.enemy_cp(state))]
|
||||||
yield [PlanAirAssault(self.enemy_cp(state))]
|
|
||||||
|
|
||||||
def enemy_cp(self, state: TheaterState) -> ControlPoint:
|
def enemy_cp(self, state: TheaterState) -> ControlPoint:
|
||||||
return self.front_line.control_point_hostile_to(state.context.coalition.player)
|
return self.front_line.control_point_hostile_to(state.context.coalition.player)
|
||||||
|
|
||||||
def units_deployable(self, state: TheaterState, player: bool) -> int:
|
def units_deployable(self, state: TheaterState, player: Player) -> int:
|
||||||
cp = self.front_line.control_point_friendly_to(player)
|
cp = self.front_line.control_point_friendly_to(player)
|
||||||
ammo_depots = list(state.ammo_dumps_at(cp))
|
ammo_depots = list(state.ammo_dumps_at(cp))
|
||||||
return cp.deployable_front_line_units_with(len(ammo_depots))
|
return cp.deployable_front_line_units_with(len(ammo_depots))
|
||||||
|
|
||||||
def unit_cap(self, state: TheaterState, player: bool) -> int:
|
def unit_cap(self, state: TheaterState, player: Player) -> int:
|
||||||
cp = self.front_line.control_point_friendly_to(player)
|
cp = self.front_line.control_point_friendly_to(player)
|
||||||
ammo_depots = list(state.ammo_dumps_at(cp))
|
ammo_depots = list(state.ammo_dumps_at(cp))
|
||||||
return cp.front_line_capacity_with(len(ammo_depots))
|
return cp.front_line_capacity_with(len(ammo_depots))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from collections.abc import Iterator
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from game.commander.tasks.compound.capturebase import CaptureBase
|
from game.commander.tasks.compound.capturebase import CaptureBase
|
||||||
|
from game.commander.tasks.primitive.airassault import PlanAirAssault
|
||||||
from game.commander.theaterstate import TheaterState
|
from game.commander.theaterstate import TheaterState
|
||||||
from game.htn import CompoundTask, Method
|
from game.htn import CompoundTask, Method
|
||||||
|
|
||||||
@@ -11,3 +12,5 @@ class CaptureBases(CompoundTask[TheaterState]):
|
|||||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
for front in state.active_front_lines:
|
for front in state.active_front_lines:
|
||||||
yield [CaptureBase(front)]
|
yield [CaptureBase(front)]
|
||||||
|
for cp in state.vulnerable_control_points:
|
||||||
|
yield [PlanAirAssault(cp)]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from typing import Union
|
|||||||
from game.commander.tasks.primitive.antiship import PlanAntiShip
|
from game.commander.tasks.primitive.antiship import PlanAntiShip
|
||||||
from game.commander.tasks.primitive.dead import PlanDead
|
from game.commander.tasks.primitive.dead import PlanDead
|
||||||
from game.commander.theaterstate import TheaterState
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.data.groups import GroupTask
|
||||||
from game.htn import CompoundTask, Method
|
from game.htn import CompoundTask, Method
|
||||||
from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject
|
from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject
|
||||||
|
|
||||||
@@ -12,12 +13,25 @@ class DegradeIads(CompoundTask[TheaterState]):
|
|||||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
for air_defense in state.threatening_air_defenses:
|
for air_defense in state.threatening_air_defenses:
|
||||||
yield [self.plan_against(air_defense)]
|
yield [self.plan_against(air_defense)]
|
||||||
|
|
||||||
|
prioritized_air_defenses = sorted(
|
||||||
|
[
|
||||||
|
tgo
|
||||||
|
for tgo in state.enemy_air_defenses
|
||||||
|
if tgo.task in [GroupTask.LORAD, GroupTask.MERAD]
|
||||||
|
],
|
||||||
|
key=lambda x: (state.priority_cp.distance_to(x) if state.priority_cp else 0)
|
||||||
|
- x.max_threat_range().meters,
|
||||||
|
)
|
||||||
|
|
||||||
|
for air_defense in prioritized_air_defenses:
|
||||||
|
yield [self.plan_against(air_defense)]
|
||||||
for detector in state.detecting_air_defenses:
|
for detector in state.detecting_air_defenses:
|
||||||
yield [self.plan_against(detector)]
|
yield [self.plan_against(detector)]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def plan_against(
|
def plan_against(
|
||||||
target: Union[IadsGroundObject, NavalGroundObject]
|
target: Union[IadsGroundObject, NavalGroundObject],
|
||||||
) -> Union[PlanDead, PlanAntiShip]:
|
) -> Union[PlanDead, PlanAntiShip]:
|
||||||
if isinstance(target, IadsGroundObject):
|
if isinstance(target, IadsGroundObject):
|
||||||
return PlanDead(target)
|
return PlanDead(target)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from game.commander.tasks.compound.interdictreinforcements import (
|
|||||||
InterdictReinforcements,
|
InterdictReinforcements,
|
||||||
)
|
)
|
||||||
from game.commander.tasks.compound.protectairspace import ProtectAirSpace
|
from game.commander.tasks.compound.protectairspace import ProtectAirSpace
|
||||||
|
from game.commander.tasks.compound.recoverysupport import RecoverySupport
|
||||||
from game.commander.tasks.compound.theatersupport import TheaterSupport
|
from game.commander.tasks.compound.theatersupport import TheaterSupport
|
||||||
from game.commander.theaterstate import TheaterState
|
from game.commander.theaterstate import TheaterState
|
||||||
from game.htn import CompoundTask, Method
|
from game.htn import CompoundTask, Method
|
||||||
@@ -28,9 +29,10 @@ class PlanNextAction(CompoundTask[TheaterState]):
|
|||||||
yield [ProtectAirSpace()]
|
yield [ProtectAirSpace()]
|
||||||
yield [DefendBases()]
|
yield [DefendBases()]
|
||||||
yield [InterdictReinforcements()]
|
yield [InterdictReinforcements()]
|
||||||
yield [DegradeIads()]
|
|
||||||
yield [AttackBattlePositions()]
|
yield [AttackBattlePositions()]
|
||||||
yield [CaptureBases()]
|
yield [CaptureBases()]
|
||||||
yield [AttackAirInfrastructure(self.aircraft_cold_start)]
|
yield [AttackAirInfrastructure(self.aircraft_cold_start)]
|
||||||
yield [AttackBuildings()]
|
yield [AttackBuildings()]
|
||||||
yield [AttackShips()]
|
yield [AttackShips()]
|
||||||
|
yield [DegradeIads()]
|
||||||
|
yield [RecoverySupport()] # for recovery tankers
|
||||||
|
|||||||
16
game/commander/tasks/compound/recoverysupport.py
Normal file
16
game/commander/tasks/compound/recoverysupport.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
from game.commander.tasks.primitive.recovery import PlanRecovery
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
|
||||||
|
|
||||||
|
class RecoverySupport(CompoundTask[TheaterState]):
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
yield [PlanRecoverySupport()]
|
||||||
|
|
||||||
|
|
||||||
|
class PlanRecoverySupport(CompoundTask[TheaterState]):
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
for target in state.recovery_targets:
|
||||||
|
yield [PlanRecovery(target)]
|
||||||
@@ -11,23 +11,23 @@ from game.theater import FrontLine
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.coalition import Coalition
|
from game.coalition import Coalition
|
||||||
|
from game.theater.player import Player
|
||||||
|
|
||||||
|
|
||||||
class FrontLineStanceTask(TheaterCommanderTask, ABC):
|
class FrontLineStanceTask(TheaterCommanderTask, ABC):
|
||||||
def __init__(self, front_line: FrontLine, player: bool) -> None:
|
def __init__(self, front_line: FrontLine, player: Player) -> None:
|
||||||
self.front_line = front_line
|
self.front_line = front_line
|
||||||
self.friendly_cp = self.front_line.control_point_friendly_to(player)
|
self.friendly_cp = self.front_line.control_point_friendly_to(player)
|
||||||
self.enemy_cp = self.front_line.control_point_hostile_to(player)
|
self.enemy_cp = self.front_line.control_point_hostile_to(player)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def stance(self) -> CombatStance:
|
def stance(self) -> CombatStance: ...
|
||||||
...
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def management_allowed(state: TheaterState) -> bool:
|
def management_allowed(state: TheaterState) -> bool:
|
||||||
return (
|
return (
|
||||||
not state.context.coalition.player
|
not state.context.coalition.player.is_blue
|
||||||
or state.context.settings.automate_front_line_stance
|
or state.context.settings.automate_front_line_stance
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,8 +49,7 @@ class FrontLineStanceTask(TheaterCommanderTask, ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def have_sufficient_front_line_advantage(self) -> bool:
|
def have_sufficient_front_line_advantage(self) -> bool: ...
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ground_force_balance(self) -> float:
|
def ground_force_balance(self) -> float:
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ from game.commander.missionproposals import EscortType, ProposedFlight, Proposed
|
|||||||
from game.commander.packagefulfiller import PackageFulfiller
|
from game.commander.packagefulfiller import PackageFulfiller
|
||||||
from game.commander.tasks.theatercommandertask import TheaterCommanderTask
|
from game.commander.tasks.theatercommandertask import TheaterCommanderTask
|
||||||
from game.commander.theaterstate import TheaterState
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.data.groups import GroupTask
|
||||||
from game.settings import AutoAtoBehavior
|
from game.settings import AutoAtoBehavior
|
||||||
from game.theater import MissionTarget
|
from game.theater import MissionTarget, ControlPoint
|
||||||
from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject
|
from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject
|
||||||
from game.utils import Distance, meters
|
from game.utils import Distance, meters
|
||||||
|
|
||||||
@@ -44,20 +45,28 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
|||||||
|
|
||||||
def preconditions_met(self, state: TheaterState) -> bool:
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
if (
|
if (
|
||||||
state.context.coalition.player
|
state.context.coalition.player.is_blue
|
||||||
and state.context.settings.auto_ato_behavior is AutoAtoBehavior.Disabled
|
and state.context.settings.auto_ato_behavior is AutoAtoBehavior.Disabled
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
return self.fulfill_mission(state)
|
return self.fulfill_mission(state)
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
seen: set[ControlPoint] = set()
|
||||||
|
if not self.package:
|
||||||
|
return
|
||||||
|
for f in self.package.flights:
|
||||||
|
if f.departure.is_fleet and not f.is_helo and f.departure not in seen:
|
||||||
|
state.recovery_targets[f.departure] += f.count
|
||||||
|
seen.add(f.departure)
|
||||||
|
|
||||||
def execute(self, coalition: Coalition) -> None:
|
def execute(self, coalition: Coalition) -> None:
|
||||||
if self.package is None:
|
if self.package is None:
|
||||||
raise RuntimeError("Attempted to execute failed package planning task")
|
raise RuntimeError("Attempted to execute failed package planning task")
|
||||||
coalition.ato.add_package(self.package)
|
coalition.ato.add_package(self.package)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def propose_flights(self) -> None:
|
def propose_flights(self) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
def propose_flight(
|
def propose_flight(
|
||||||
self,
|
self,
|
||||||
@@ -93,7 +102,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
def fulfill_mission(self, state: TheaterState) -> bool:
|
def fulfill_mission(self, state: TheaterState) -> bool:
|
||||||
color = "blue" if state.context.coalition.player else "red"
|
color = "blue" if state.context.coalition.player.is_blue else "red"
|
||||||
self.propose_flights()
|
self.propose_flights()
|
||||||
fulfiller = PackageFulfiller(
|
fulfiller = PackageFulfiller(
|
||||||
state.context.coalition,
|
state.context.coalition,
|
||||||
@@ -102,8 +111,14 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
|||||||
state.context.settings,
|
state.context.settings,
|
||||||
)
|
)
|
||||||
with state.context.tracer.trace(f"{color} {self.flights[0].task} planning"):
|
with state.context.tracer.trace(f"{color} {self.flights[0].task} planning"):
|
||||||
|
asap = False
|
||||||
|
if (
|
||||||
|
not state.context.coalition.ato.has_awacs_package
|
||||||
|
and FlightType.AEWC in [f.task for f in self.flights]
|
||||||
|
):
|
||||||
|
asap = True
|
||||||
self.package = fulfiller.plan_mission(
|
self.package = fulfiller.plan_mission(
|
||||||
ProposedMission(self.target, self.flights),
|
ProposedMission(self.target, self.flights, asap=asap),
|
||||||
self.purchase_multiplier,
|
self.purchase_multiplier,
|
||||||
state.context.now,
|
state.context.now,
|
||||||
state.context.tracer,
|
state.context.tracer,
|
||||||
@@ -121,9 +136,9 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
|||||||
target_ranges: list[
|
target_ranges: list[
|
||||||
tuple[Union[IadsGroundObject, NavalGroundObject], Distance]
|
tuple[Union[IadsGroundObject, NavalGroundObject], Distance]
|
||||||
] = []
|
] = []
|
||||||
all_iads: Iterator[
|
all_iads: Iterator[Union[IadsGroundObject, NavalGroundObject]] = (
|
||||||
Union[IadsGroundObject, NavalGroundObject]
|
itertools.chain(state.enemy_air_defenses, state.enemy_ships)
|
||||||
] = itertools.chain(state.enemy_air_defenses, state.enemy_ships)
|
)
|
||||||
for target in all_iads:
|
for target in all_iads:
|
||||||
distance = meters(target.distance_to(self.target))
|
distance = meters(target.distance_to(self.target))
|
||||||
if range_type is RangeType.Detection:
|
if range_type is RangeType.Detection:
|
||||||
@@ -150,6 +165,16 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
|||||||
for target, _range in target_ranges:
|
for target, _range in target_ranges:
|
||||||
yield target
|
yield target
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def corrective_factor_for_type(
|
||||||
|
target: IadsGroundObject | NavalGroundObject,
|
||||||
|
) -> float:
|
||||||
|
return (
|
||||||
|
1.0
|
||||||
|
if target.task in [GroupTask.LORAD, GroupTask.MERAD]
|
||||||
|
else 0.5 if target.task == GroupTask.AAA else 0.9
|
||||||
|
)
|
||||||
|
|
||||||
def iter_detecting_iads(
|
def iter_detecting_iads(
|
||||||
self, state: TheaterState
|
self, state: TheaterState
|
||||||
) -> Iterator[Union[IadsGroundObject, NavalGroundObject]]:
|
) -> Iterator[Union[IadsGroundObject, NavalGroundObject]]:
|
||||||
@@ -173,19 +198,31 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
|||||||
|
|
||||||
if not ignore_iads:
|
if not ignore_iads:
|
||||||
for iads_threat in self.iter_iads_threats(state):
|
for iads_threat in self.iter_iads_threats(state):
|
||||||
# Only consider blue faction flights threatened.
|
weighted = self._get_weighted_threat_range(iads_threat, state)
|
||||||
# Red might still plan missions into hostile territory,
|
if weighted < meters(0):
|
||||||
# depending on the aggressiveness setting.
|
|
||||||
if (
|
|
||||||
state.context.coalition.player
|
|
||||||
or random.randint(1, 100)
|
|
||||||
> state.context.coalition.game.settings.opfor_autoplanner_aggressiveness
|
|
||||||
):
|
|
||||||
threatened = True
|
threatened = True
|
||||||
if iads_threat not in state.threatening_air_defenses:
|
if iads_threat not in state.threatening_air_defenses:
|
||||||
state.threatening_air_defenses.append(iads_threat)
|
state.threatening_air_defenses.append(iads_threat)
|
||||||
return not threatened
|
return not threatened
|
||||||
|
|
||||||
|
def _get_weighted_threat_range(
|
||||||
|
self,
|
||||||
|
iads_threat: Union[IadsGroundObject | NavalGroundObject],
|
||||||
|
state: TheaterState,
|
||||||
|
) -> Distance:
|
||||||
|
distance = meters(iads_threat.distance_to(self.target))
|
||||||
|
settings = state.context.coalition.game.settings
|
||||||
|
margin = 100 - (
|
||||||
|
settings.ownfor_autoplanner_aggressiveness
|
||||||
|
if state.context.coalition.player.is_blue
|
||||||
|
else settings.opfor_autoplanner_aggressiveness
|
||||||
|
)
|
||||||
|
threat_range = iads_threat.max_threat_range() * (margin / 100)
|
||||||
|
corrective_factor = self.corrective_factor_for_type(iads_threat)
|
||||||
|
threat_range *= corrective_factor
|
||||||
|
distance_to_threat = distance - threat_range
|
||||||
|
return distance_to_threat
|
||||||
|
|
||||||
def get_flight_size(self) -> int:
|
def get_flight_size(self) -> int:
|
||||||
settings = self.target.coalition.game.settings
|
settings = self.target.coalition.game.settings
|
||||||
weights = [
|
weights = [
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from game.theater import MissionTarget
|
|||||||
class PlanAewc(PackagePlanningTask[MissionTarget]):
|
class PlanAewc(PackagePlanningTask[MissionTarget]):
|
||||||
def preconditions_met(self, state: TheaterState) -> bool:
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
if (
|
if (
|
||||||
state.context.coalition.player
|
state.context.coalition.player.is_blue
|
||||||
and not state.context.settings.auto_ato_behavior_awacs
|
and not state.context.settings.auto_ato_behavior_awacs
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
@@ -22,9 +22,11 @@ class PlanAewc(PackagePlanningTask[MissionTarget]):
|
|||||||
|
|
||||||
def apply_effects(self, state: TheaterState) -> None:
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
state.aewc_targets.remove(self.target)
|
state.aewc_targets.remove(self.target)
|
||||||
|
super().apply_effects(state)
|
||||||
|
|
||||||
def propose_flights(self) -> None:
|
def propose_flights(self) -> None:
|
||||||
self.propose_flight(FlightType.AEWC, 1)
|
self.propose_flight(FlightType.AEWC, 1)
|
||||||
|
self.propose_flight(FlightType.ESCORT, 2)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def asap(self) -> bool:
|
def asap(self) -> bool:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from game.theater import ControlPoint
|
|||||||
@dataclass
|
@dataclass
|
||||||
class PlanAirAssault(PackagePlanningTask[ControlPoint]):
|
class PlanAirAssault(PackagePlanningTask[ControlPoint]):
|
||||||
def preconditions_met(self, state: TheaterState) -> bool:
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
if self.target not in state.vulnerable_control_points:
|
if self.target not in state.vulnerable_control_points or self.target.is_fleet:
|
||||||
return False
|
return False
|
||||||
if not self.target_area_preconditions_met(state):
|
if not self.target_area_preconditions_met(state):
|
||||||
return False
|
return False
|
||||||
@@ -19,8 +19,8 @@ class PlanAirAssault(PackagePlanningTask[ControlPoint]):
|
|||||||
|
|
||||||
def apply_effects(self, state: TheaterState) -> None:
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
state.vulnerable_control_points.remove(self.target)
|
state.vulnerable_control_points.remove(self.target)
|
||||||
|
super().apply_effects(state)
|
||||||
|
|
||||||
def propose_flights(self) -> None:
|
def propose_flights(self) -> None:
|
||||||
size = self.get_flight_size()
|
self.propose_flight(FlightType.AIR_ASSAULT, self.get_flight_size())
|
||||||
self.propose_flight(FlightType.AIR_ASSAULT, size)
|
|
||||||
self.propose_common_escorts()
|
self.propose_common_escorts()
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from random import randint
|
|
||||||
|
|
||||||
from game.ato.flighttype import FlightType
|
from game.ato.flighttype import FlightType
|
||||||
from game.commander.missionproposals import EscortType
|
from game.commander.missionproposals import EscortType
|
||||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
from game.commander.theaterstate import TheaterState
|
from game.commander.theaterstate import TheaterState
|
||||||
from game.theater.theatergroundobject import NavalGroundObject
|
from game.theater.theatergroundobject import NavalGroundObject
|
||||||
|
from game.utils import meters
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
|
class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
|
||||||
def preconditions_met(self, state: TheaterState) -> bool:
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
if self.target not in state.threatening_air_defenses:
|
if (
|
||||||
|
self.target not in state.threatening_air_defenses
|
||||||
|
and not self.target.is_naval_control_point
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
if not self.target_area_preconditions_met(state, ignore_iads=True):
|
if not self.target_area_preconditions_met(state, ignore_iads=True):
|
||||||
return False
|
return False
|
||||||
@@ -21,8 +24,15 @@ class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
|
|||||||
|
|
||||||
def apply_effects(self, state: TheaterState) -> None:
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
state.eliminate_ship(self.target)
|
state.eliminate_ship(self.target)
|
||||||
|
super().apply_effects(state)
|
||||||
|
|
||||||
def propose_flights(self) -> None:
|
def propose_flights(self) -> None:
|
||||||
size = self.get_flight_size()
|
size = self.get_flight_size()
|
||||||
|
if self.target.max_detection_range() > meters(0):
|
||||||
|
size = 4 # attempt to saturate ship's air-defences
|
||||||
|
self.propose_flight(FlightType.ANTISHIP, size)
|
||||||
|
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)
|
||||||
self.propose_flight(FlightType.ANTISHIP, size)
|
self.propose_flight(FlightType.ANTISHIP, size)
|
||||||
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)
|
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)
|
||||||
|
if self.target.max_detection_range() > meters(0):
|
||||||
|
self.propose_flight(FlightType.SEAD, 2, EscortType.Sead)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class PlanAntiShipping(PackagePlanningTask[CargoShip]):
|
|||||||
|
|
||||||
def apply_effects(self, state: TheaterState) -> None:
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
state.enemy_shipping.remove(self.target)
|
state.enemy_shipping.remove(self.target)
|
||||||
|
super().apply_effects(state)
|
||||||
|
|
||||||
def propose_flights(self) -> None:
|
def propose_flights(self) -> None:
|
||||||
size = self.get_flight_size()
|
size = self.get_flight_size()
|
||||||
|
|||||||
26
game/commander/tasks/primitive/armedrecon.py
Normal file
26
game/commander/tasks/primitive/armedrecon.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.ato.flighttype import FlightType
|
||||||
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.theater import ControlPoint
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanArmedRecon(PackagePlanningTask[ControlPoint]):
|
||||||
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
|
if self.target not in state.control_point_priority_queue:
|
||||||
|
return False
|
||||||
|
if not self.target_area_preconditions_met(state):
|
||||||
|
return False
|
||||||
|
return super().preconditions_met(state)
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
state.control_point_priority_queue.remove(self.target)
|
||||||
|
super().apply_effects(state)
|
||||||
|
|
||||||
|
def propose_flights(self) -> None:
|
||||||
|
self.propose_flight(FlightType.ARMED_RECON, self.get_flight_size())
|
||||||
|
self.propose_common_escorts()
|
||||||
@@ -19,6 +19,7 @@ class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]):
|
|||||||
|
|
||||||
def apply_effects(self, state: TheaterState) -> None:
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
state.eliminate_battle_position(self.target)
|
state.eliminate_battle_position(self.target)
|
||||||
|
super().apply_effects(state)
|
||||||
|
|
||||||
def propose_flights(self) -> None:
|
def propose_flights(self) -> None:
|
||||||
tgt_count = self.target.alive_unit_count
|
tgt_count = self.target.alive_unit_count
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import random
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from random import randint
|
|
||||||
|
|
||||||
from game.ato.flighttype import FlightType
|
from game.ato.flighttype import FlightType
|
||||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
@@ -21,6 +19,7 @@ class PlanBarcap(PackagePlanningTask[ControlPoint]):
|
|||||||
|
|
||||||
def apply_effects(self, state: TheaterState) -> None:
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
state.barcaps_needed[self.target] -= 1
|
state.barcaps_needed[self.target] -= 1
|
||||||
|
super().apply_effects(state)
|
||||||
|
|
||||||
def propose_flights(self) -> None:
|
def propose_flights(self) -> None:
|
||||||
size = self.get_flight_size()
|
size = self.get_flight_size()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from game.ato.flighttype import FlightType
|
|||||||
from game.commander.missionproposals import EscortType
|
from game.commander.missionproposals import EscortType
|
||||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
from game.commander.theaterstate import TheaterState
|
from game.commander.theaterstate import TheaterState
|
||||||
from game.theater import FrontLine
|
from game.theater import FrontLine, Player
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -19,18 +19,18 @@ class PlanCas(PackagePlanningTask[FrontLine]):
|
|||||||
# An exception is made for turn zero since that's not being truly planned, but
|
# An exception is made for turn zero since that's not being truly planned, but
|
||||||
# just to determine what missions should be planned on turn 1 (when there *will*
|
# just to determine what missions should be planned on turn 1 (when there *will*
|
||||||
# be ground units) and what aircraft should be ordered.
|
# be ground units) and what aircraft should be ordered.
|
||||||
enemy_cp = self.target.control_point_friendly_to(
|
player = state.context.coalition.player.opponent
|
||||||
player=not state.context.coalition.player
|
enemy_cp = self.target.control_point_friendly_to(player)
|
||||||
)
|
|
||||||
if enemy_cp.deployable_front_line_units == 0 and state.context.turn > 0:
|
if enemy_cp.deployable_front_line_units == 0 and state.context.turn > 0:
|
||||||
return False
|
return False
|
||||||
return super().preconditions_met(state)
|
return super().preconditions_met(state)
|
||||||
|
|
||||||
def apply_effects(self, state: TheaterState) -> None:
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
state.vulnerable_front_lines.remove(self.target)
|
state.vulnerable_front_lines.remove(self.target)
|
||||||
|
super().apply_effects(state)
|
||||||
|
|
||||||
def propose_flights(self) -> None:
|
def propose_flights(self) -> None:
|
||||||
size = self.get_flight_size()
|
size = self.get_flight_size()
|
||||||
self.propose_flight(FlightType.CAS, size)
|
self.propose_flight(FlightType.CAS, size)
|
||||||
self.propose_flight(FlightType.TARCAP, 2, EscortType.AirToAir)
|
self.propose_flight(FlightType.TARCAP, 2, EscortType.AirToAir)
|
||||||
self.propose_flight(FlightType.SEAD_SWEEP, 2, EscortType.Sead)
|
self.propose_flight(FlightType.SEAD_SWEEP, 2)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user