mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Compare commits
1062 Commits
f16i-mod-s
...
v1.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9832b5d82 | ||
|
|
147136c41a | ||
|
|
6204dd4e72 | ||
|
|
19daa1737e | ||
|
|
0b40f872bf | ||
|
|
733e31c35a | ||
|
|
fc9a1ad145 | ||
|
|
94801f859a | ||
|
|
3cd1b2f80a | ||
|
|
a9287809a3 | ||
|
|
ced97dff1e | ||
|
|
c418d70d96 | ||
|
|
1085cbbd9d | ||
|
|
c39d58f60b | ||
|
|
f6985d9f70 | ||
|
|
eb6afbdf02 | ||
|
|
15218d0241 | ||
|
|
005bbcb812 | ||
|
|
dc3e7e790b | ||
|
|
9303e1cb9e | ||
|
|
7158a5e60d | ||
|
|
bcf8ee9d42 | ||
|
|
7a3ca43514 | ||
|
|
707c0793a3 | ||
|
|
0591f74a11 | ||
|
|
d484e133ce | ||
|
|
d2004e5b11 | ||
|
|
2fdc9bbb2d | ||
|
|
61b2d7c68e | ||
|
|
aab8454fe6 | ||
|
|
9f204d0334 | ||
|
|
b4ba200f95 | ||
|
|
32f8d8d0b7 | ||
|
|
9f7c9eec14 | ||
|
|
697aef2aef | ||
|
|
95580a6cef | ||
|
|
9e1a642eb2 | ||
|
|
93c5cb767e | ||
|
|
0f23a526eb | ||
|
|
7fb19649e1 | ||
|
|
65b2db0ece | ||
|
|
4416463ec8 | ||
|
|
c887a35d0b | ||
|
|
9cb7617352 | ||
|
|
e36e213b76 | ||
|
|
b737f4e00c | ||
|
|
d3d4acd806 | ||
|
|
0d1c292b23 | ||
|
|
7e620acb4f | ||
|
|
aadff131a3 | ||
|
|
68a09ea81b | ||
|
|
f1920d3bad | ||
|
|
e7796be586 | ||
|
|
37fc571fa1 | ||
|
|
a7ee431768 | ||
|
|
f7b34d2d79 | ||
|
|
e5c44a1f16 | ||
|
|
e4b67ffb8a | ||
|
|
f54993f579 | ||
|
|
b9230b6fb6 | ||
|
|
dbd9776aff | ||
|
|
e7e43a95fb | ||
|
|
96b1f7cc3a | ||
|
|
e5d199f20b | ||
|
|
fc467b62e0 | ||
|
|
84b61b271b | ||
|
|
6571db3001 | ||
|
|
3d98aaaae0 | ||
|
|
581df54fe2 | ||
|
|
e362b15c02 | ||
|
|
738b0dffc5 | ||
|
|
56899985cd | ||
|
|
781d611f17 | ||
|
|
d901df4fa0 | ||
|
|
40a733e147 | ||
|
|
6e4d1e9e5c | ||
|
|
47531cd9fa | ||
|
|
1395dcea71 | ||
|
|
fd6d81e3ad | ||
|
|
4ba5fa4766 | ||
|
|
ca0a100d9a | ||
|
|
08c007b7bf | ||
|
|
e0aeabb63a | ||
|
|
d9479be12e | ||
|
|
3f5b93a5c1 | ||
|
|
05163d41a0 | ||
|
|
5dfba64ac3 | ||
|
|
76a077d38f | ||
|
|
caad080b01 | ||
|
|
35f6cd8f17 | ||
|
|
89aa0985bc | ||
|
|
747ccae023 | ||
|
|
06242a433c | ||
|
|
14a0310d25 | ||
|
|
80bf5149fc | ||
|
|
80d1ae0950 | ||
|
|
cb68ff0df3 | ||
|
|
63d05ea696 | ||
|
|
7c05e94619 | ||
|
|
f4987e05e4 | ||
|
|
4c13704630 | ||
|
|
3e373b706f | ||
|
|
5aefa4ae55 | ||
|
|
7f92db75df | ||
|
|
7a94db5404 | ||
|
|
a30bfd2a45 | ||
|
|
33cfcd211a | ||
|
|
0edcd31367 | ||
|
|
ca98183e94 | ||
|
|
8ff6e5688a | ||
|
|
dad5e7c146 | ||
|
|
8d56e2d3bb | ||
|
|
a2494e467e | ||
|
|
8ae63796cf | ||
|
|
6a002a7d43 | ||
|
|
0b060d3110 | ||
|
|
bc26eb3f5e | ||
|
|
d6026681ee | ||
|
|
9ab8e15dd1 | ||
|
|
febc901518 | ||
|
|
26066ef8f4 | ||
|
|
f548b27ac5 | ||
|
|
181f8583d5 | ||
|
|
e6e557c18a | ||
|
|
a6a872eebb | ||
|
|
d07cb46741 | ||
|
|
df3bd146da | ||
|
|
93ec459460 | ||
|
|
6e96b4fbb0 | ||
|
|
79b1d949ce | ||
|
|
5ca12373d8 | ||
|
|
7e0125725c | ||
|
|
5eb25e02a4 | ||
|
|
435bac3847 | ||
|
|
6e879b297e | ||
|
|
bf4f882214 | ||
|
|
d0dec86a6d | ||
|
|
96c7e20488 | ||
|
|
35408f7a53 | ||
|
|
62df761d9c | ||
|
|
9ff9fa5b69 | ||
|
|
d6f586bbe3 | ||
|
|
e63520265c | ||
|
|
4d4f4d1930 | ||
|
|
387f72ace2 | ||
|
|
93abee2e35 | ||
|
|
94580bad88 | ||
|
|
b35e0c26d6 | ||
|
|
7d95e3e2fe | ||
|
|
9c40506728 | ||
|
|
60859d39c1 | ||
|
|
8740df65c4 | ||
|
|
26b15c66b8 | ||
|
|
04ac3d51b3 | ||
|
|
1c609e1b70 | ||
|
|
5de863fbb8 | ||
|
|
11d8f139f8 | ||
|
|
65947129f0 | ||
|
|
d85d73a6d0 | ||
|
|
28e3c3d817 | ||
|
|
9907432a0a | ||
|
|
ade81f4548 | ||
|
|
aaf23d52bc | ||
|
|
b5dd2767a7 | ||
|
|
5043a1b8ac | ||
|
|
cade8f4a19 | ||
|
|
82e2f1c2b5 | ||
|
|
6cfa52740e | ||
|
|
26f83cf269 | ||
|
|
4cbadb1d81 | ||
|
|
f950230fb5 | ||
|
|
6384f2feb2 | ||
|
|
f648add735 | ||
|
|
470cdd64d4 | ||
|
|
17ee2385f3 | ||
|
|
25ee1dfc45 | ||
|
|
859af22d2c | ||
|
|
da1a820d45 | ||
|
|
bc861380f0 | ||
|
|
e3dcf8e547 | ||
|
|
9805c482c2 | ||
|
|
7d314f8ee4 | ||
|
|
10aee31c7d | ||
|
|
6f1ee9561b | ||
|
|
8f531d247d | ||
|
|
f5bee7cc5d | ||
|
|
d8ce0a6c24 | ||
|
|
be634ab961 | ||
|
|
b0047784dc | ||
|
|
d6ba45a42b | ||
|
|
291822b5c8 | ||
|
|
d3a56fa893 | ||
|
|
7d347eee94 | ||
|
|
bcdf2ad887 | ||
|
|
22900407dd | ||
|
|
84c9098dba | ||
|
|
63e2c30e44 | ||
|
|
02ae5d9ad7 | ||
|
|
91fb0cea58 | ||
|
|
3c921198af | ||
|
|
7a3c9a707f | ||
|
|
3b319ca481 | ||
|
|
7adbe71475 | ||
|
|
06f51573e1 | ||
|
|
b2c38f454f | ||
|
|
c9ed0352b7 | ||
|
|
b8ef4fc726 | ||
|
|
394627e8d5 | ||
|
|
f513e27dc6 | ||
|
|
fe6ff6a1ae | ||
|
|
88a5179653 | ||
|
|
a357097c49 | ||
|
|
c4be00d11b | ||
|
|
93b83f2bc8 | ||
|
|
a2e98f485c | ||
|
|
e04f23bf15 | ||
|
|
f11689f4cb | ||
|
|
ea726bbf06 | ||
|
|
59673e7911 | ||
|
|
625c4f2dfb | ||
|
|
6a47e84699 | ||
|
|
14d3e743d4 | ||
|
|
dcaee350cf | ||
|
|
d205e66fe4 | ||
|
|
62a40db9ea | ||
|
|
75498fe061 | ||
|
|
5ca344e87b | ||
|
|
03d8448def | ||
|
|
a2630fc75f | ||
|
|
c50dcaaafb | ||
|
|
3499ba5ffa | ||
|
|
f5f11ff3ac | ||
|
|
0ec375ad89 | ||
|
|
ee3bdf9fd7 | ||
|
|
1760532168 | ||
|
|
97ee6ba19f | ||
|
|
d96f7a267c | ||
|
|
98f92f9ab2 | ||
|
|
8b04dd878d | ||
|
|
643dafd2c8 | ||
|
|
bf879a6141 | ||
|
|
fa1caa86d6 | ||
|
|
181ac27299 | ||
|
|
313d8f5065 | ||
|
|
ee1fa78ecd | ||
|
|
60172d0e52 | ||
|
|
e514cb1397 | ||
|
|
c640a53190 | ||
|
|
6ef3bd5095 | ||
|
|
47add7848f | ||
|
|
0b7acbc94e | ||
|
|
04974c3c0b | ||
|
|
014ae5faf5 | ||
|
|
e25aac774f | ||
|
|
c11b4149a0 | ||
|
|
091d523516 | ||
|
|
2ab456b9c1 | ||
|
|
bb36944a8a | ||
|
|
62e43f1169 | ||
|
|
dc22e0a577 | ||
|
|
51f578b9e3 | ||
|
|
2a8f34ea25 | ||
|
|
26a71f4f0e | ||
|
|
a366aebbb2 | ||
|
|
1ce8d9c2b7 | ||
|
|
17e539e524 | ||
|
|
177f357492 | ||
|
|
723e191f10 | ||
|
|
cb6bffe3ec | ||
|
|
1ebc289fb7 | ||
|
|
d271428f9d | ||
|
|
54777a9045 | ||
|
|
226a171550 | ||
|
|
227134d9c1 | ||
|
|
b3f5afe36b | ||
|
|
2c6c2e5fbd | ||
|
|
d4eefd0b55 | ||
|
|
110866cd34 | ||
|
|
279572ae09 | ||
|
|
485229b92f | ||
|
|
8e670e1a3c | ||
|
|
bce6a170b8 | ||
|
|
a0fdfa11e2 | ||
|
|
7303c8fa20 | ||
|
|
855fa1347b | ||
|
|
5758436dbf | ||
|
|
a343bdef23 | ||
|
|
d3b0ae30eb | ||
|
|
2c7e877086 | ||
|
|
12cdb8646c | ||
|
|
f89ac52bf3 | ||
|
|
5e858731d2 | ||
|
|
30f6c14030 | ||
|
|
78e07fec83 | ||
|
|
1cf871c150 | ||
|
|
8e821c50e5 | ||
|
|
3024bd6479 | ||
|
|
105d1abba8 | ||
|
|
3b7077e593 | ||
|
|
d581af3b8a | ||
|
|
e8929467e0 | ||
|
|
234dc52c7e | ||
|
|
ea74471307 | ||
|
|
b870198281 | ||
|
|
389037e6bf | ||
|
|
167d048232 | ||
|
|
59871ac36c | ||
|
|
bd3205a442 | ||
|
|
00a2d77543 | ||
|
|
1b000e83df | ||
|
|
3787ec3039 | ||
|
|
e341599a11 | ||
|
|
e50d49d7ba | ||
|
|
0cb3702eb9 | ||
|
|
09749bc088 | ||
|
|
0b152c201d | ||
|
|
fe470ad9c6 | ||
|
|
d22bfd0804 | ||
|
|
1ba3f2fbb4 | ||
|
|
688b7f4469 | ||
|
|
a8a37fa3e7 | ||
|
|
b7bc9efb52 | ||
|
|
74cd3a2627 | ||
|
|
e4cb0f7c28 | ||
|
|
66d741d0b3 | ||
|
|
819bd92d9a | ||
|
|
16b7ff2bb4 | ||
|
|
19b9b8b0ea | ||
|
|
f5cb293e44 | ||
|
|
f22ad17651 | ||
|
|
9b9905c5a3 | ||
|
|
8a6b7b172c | ||
|
|
4901443b7a | ||
|
|
789806637c | ||
|
|
270301958a | ||
|
|
1157d264ee | ||
|
|
87399c9ada | ||
|
|
cc4db1deff | ||
|
|
77b88de40e | ||
|
|
7baba78bca | ||
|
|
95f169bb58 | ||
|
|
7a9a8e1b4b | ||
|
|
046dccc4d5 | ||
|
|
15b0c26c5d | ||
|
|
7200c687af | ||
|
|
9b11eaa9bc | ||
|
|
d6504c3a09 | ||
|
|
15cb1839b0 | ||
|
|
633615c90d | ||
|
|
115bd8b22a | ||
|
|
c51fa5c1df | ||
|
|
a5104dd9ec | ||
|
|
901c867d7f | ||
|
|
5c06700873 | ||
|
|
0bc645edeb | ||
|
|
1e5cb94feb | ||
|
|
53122ff149 | ||
|
|
da75f41b01 | ||
|
|
63e65744f1 | ||
|
|
b8cce988f8 | ||
|
|
85bbda4234 | ||
|
|
3f96334065 | ||
|
|
79ce31c920 | ||
|
|
0e5df2fd21 | ||
|
|
57319bd78b | ||
|
|
5bfab913fb | ||
|
|
0027c64e57 | ||
|
|
bc38563618 | ||
|
|
493e69eb40 | ||
|
|
15be9e3ce1 | ||
|
|
64f6a10ced | ||
|
|
2cfd56c8da | ||
|
|
60d11ffe2a | ||
|
|
01f2d37fb8 | ||
|
|
8e6464eb90 | ||
|
|
2e6b98e9de | ||
|
|
d095732a4e | ||
|
|
b83f3c41d7 | ||
|
|
e497c7dc6c | ||
|
|
1a52154b5a | ||
|
|
6072b0181d | ||
|
|
e3f9f611eb | ||
|
|
34f7b2c53d | ||
|
|
8472897e5f | ||
|
|
7c5f92b3c5 | ||
|
|
5d89ba769d | ||
|
|
bdcbf81443 | ||
|
|
e5afa27b4a | ||
|
|
51646639ff | ||
|
|
428833ce38 | ||
|
|
4cb4058ded | ||
|
|
20d35762a6 | ||
|
|
c0e8e240a1 | ||
|
|
850cb415c7 | ||
|
|
3e42484be3 | ||
|
|
adaf049eb9 | ||
|
|
bed1417c54 | ||
|
|
98a10b59b9 | ||
|
|
f5174281c7 | ||
|
|
7ec4073ee2 | ||
|
|
80f81e3775 | ||
|
|
d8c34a7057 | ||
|
|
595a5ac111 | ||
|
|
024407e1e2 | ||
|
|
9d63bcbf69 | ||
|
|
092ef88cab | ||
|
|
16b3d76684 | ||
|
|
43b6cb8bb1 | ||
|
|
009debd4e8 | ||
|
|
6c87421c26 | ||
|
|
21eff6cb87 | ||
|
|
00bcec498c | ||
|
|
baac660f1d | ||
|
|
63898ead87 | ||
|
|
18f95acf87 | ||
|
|
cb136b8d9d | ||
|
|
4ecc44da17 | ||
|
|
8c8fc7cc26 | ||
|
|
17211e1597 | ||
|
|
f76c8a80b5 | ||
|
|
7a6ca986db | ||
|
|
8f35546d84 | ||
|
|
96828ef3db | ||
|
|
9dd76bdd37 | ||
|
|
2b5f6f7998 | ||
|
|
d6e7451f8a | ||
|
|
06d98db55b | ||
|
|
533057a0cc | ||
|
|
e86fc60b41 | ||
|
|
737df98988 | ||
|
|
6734b80684 | ||
|
|
af3a80a294 | ||
|
|
8face817e2 | ||
|
|
da10cc091c | ||
|
|
2ec187bc9c | ||
|
|
785ac6eacc | ||
|
|
be31a97c71 | ||
|
|
aecb171b8f | ||
|
|
aafc832e24 | ||
|
|
6c210c9d15 | ||
|
|
031feeed6f | ||
|
|
6ced738256 | ||
|
|
db038ecdea | ||
|
|
e2d9a794b8 | ||
|
|
ae17f195fe | ||
|
|
46f98a0684 | ||
|
|
9c6b070dbd | ||
|
|
569efdbb12 | ||
|
|
9086262f2b | ||
|
|
861ed584df | ||
|
|
a5083e3e1f | ||
|
|
6e37cadb84 | ||
|
|
4b4ec8d9ad | ||
|
|
ef0bc31b8c | ||
|
|
f6f1d69048 | ||
|
|
3b745c43eb | ||
|
|
aaaa9847b1 | ||
|
|
9e42f7f255 | ||
|
|
03a3fd37ef | ||
|
|
e5bb341f5b | ||
|
|
57a0231699 | ||
|
|
bf5807d9c1 | ||
|
|
5368c06650 | ||
|
|
ab66846967 | ||
|
|
a2b7477404 | ||
|
|
3bc4203954 | ||
|
|
51fc0215d6 | ||
|
|
768f2dbc57 | ||
|
|
928a679e7d | ||
|
|
430fcfee20 | ||
|
|
50975d5eb6 | ||
|
|
918716044c | ||
|
|
c7a214201c | ||
|
|
cd563d837b | ||
|
|
32df683720 | ||
|
|
e15002dff5 | ||
|
|
fb2598a671 | ||
|
|
208c517882 | ||
|
|
f970801243 | ||
|
|
e273e93012 | ||
|
|
5a71806651 | ||
|
|
430d33c144 | ||
|
|
fa63beeb52 | ||
|
|
e952e0b536 | ||
|
|
19a6eaeaa3 | ||
|
|
5818367f2e | ||
|
|
d3069bdaad | ||
|
|
471654aa5e | ||
|
|
56e712f948 | ||
|
|
74bc3b004b | ||
|
|
6f1333d26a | ||
|
|
42a0545630 | ||
|
|
1c5f872b3c | ||
|
|
e4e7155f02 | ||
|
|
c924130356 | ||
|
|
095947333f | ||
|
|
6356d0b3f5 | ||
|
|
df5b5839e2 | ||
|
|
3abb9375fb | ||
|
|
322b8245f7 | ||
|
|
2286d155c4 | ||
|
|
7c1b642aa8 | ||
|
|
198f93fe4a | ||
|
|
ea25e2ceb8 | ||
|
|
453343849f | ||
|
|
cee9122680 | ||
|
|
ce3e358f8c | ||
|
|
018a4b4718 | ||
|
|
e677a1e910 | ||
|
|
b2fbcdd86a | ||
|
|
1474dfd200 | ||
|
|
3edb1b6c10 | ||
|
|
0de4b01917 | ||
|
|
04876c8c66 | ||
|
|
8c29071d04 | ||
|
|
91ee04bf2a | ||
|
|
666f2908b7 | ||
|
|
45b8757593 | ||
|
|
057ea58d8e | ||
|
|
ad227965ea | ||
|
|
2048910c78 | ||
|
|
245bab57c2 | ||
|
|
ef63fe5b5c | ||
|
|
e2a14f861e | ||
|
|
2468b1c6d4 | ||
|
|
b62b646e3b | ||
|
|
5ab97561cc | ||
|
|
b45139bf02 | ||
|
|
88db6eb5f0 | ||
|
|
3b40aef70c | ||
|
|
389d230ea3 | ||
|
|
765e6f8dbf | ||
|
|
0e307683b6 | ||
|
|
5065f4e505 | ||
|
|
a31017e05a | ||
|
|
59f04238cb | ||
|
|
c4c23a6692 | ||
|
|
e4fbaf8480 | ||
|
|
143cb61734 | ||
|
|
d3e727f205 | ||
|
|
7e9380162c | ||
|
|
e9760e4a7d | ||
|
|
0d070d7652 | ||
|
|
57c2850c87 | ||
|
|
ce9c3074ff | ||
|
|
4cfc11bc79 | ||
|
|
27fcef7517 | ||
|
|
e980e7a5f8 | ||
|
|
3b5af93e2d | ||
|
|
a3d43e1815 | ||
|
|
f9af1ed184 | ||
|
|
abe6691206 | ||
|
|
9c1be534c7 | ||
|
|
055519411b | ||
|
|
9b26c2ddca | ||
|
|
aad908a716 | ||
|
|
c69f536639 | ||
|
|
fbdc3e896e | ||
|
|
f7f7925260 | ||
|
|
a97fd69828 | ||
|
|
8a57104b0f | ||
|
|
d88b5da382 | ||
|
|
c69b797ff3 | ||
|
|
e3c9806afb | ||
|
|
fa27ad96c7 | ||
|
|
9d60418aef | ||
|
|
bf277c6c06 | ||
|
|
b398b72f94 | ||
|
|
f2bdc3e0ee | ||
|
|
813d68bf31 | ||
|
|
883d08209d | ||
|
|
2aeee4ced6 | ||
|
|
1b39d21750 | ||
|
|
6d7d7a4439 | ||
|
|
6452f879bf | ||
|
|
0d8064c582 | ||
|
|
83e63487e5 | ||
|
|
4f00ec9546 | ||
|
|
402e4a54d5 | ||
|
|
d8e4fa6f80 | ||
|
|
be9f83e221 | ||
|
|
cc0d1d5ab6 | ||
|
|
df922e9ca7 | ||
|
|
5600257442 | ||
|
|
e9a3bd0640 | ||
|
|
6cb67edfcb | ||
|
|
7e53c36d2c | ||
|
|
a54f836cde | ||
|
|
aa64f28a4b | ||
|
|
2251018de2 | ||
|
|
c559d6e2be | ||
|
|
529eea0c01 | ||
|
|
0acf970443 | ||
|
|
7a57bd3ee0 | ||
|
|
5d127744ff | ||
|
|
6e15599691 | ||
|
|
e018ef7f11 | ||
|
|
b318bc4941 | ||
|
|
962c64d065 | ||
|
|
bbf8e69659 | ||
|
|
2ed85792b9 | ||
|
|
db7bcaa45c | ||
|
|
2225d6ef8e | ||
|
|
703da303ee | ||
|
|
54a471fe82 | ||
|
|
f8f37e8986 | ||
|
|
027dd88feb | ||
|
|
118005528c | ||
|
|
bcd81bae7c | ||
|
|
fa70ba5d29 | ||
|
|
0c93d281bd | ||
|
|
3e7968adce | ||
|
|
e5a4e54544 | ||
|
|
1540b2cf31 | ||
|
|
198ff7d8a3 | ||
|
|
8363a7e8fa | ||
|
|
33ee4b1d1a | ||
|
|
7582040d41 | ||
|
|
b8cb1a6273 | ||
|
|
b958e9b095 | ||
|
|
35607515f6 | ||
|
|
28deb36f2c | ||
|
|
7e6a6cc61a | ||
|
|
78990b8953 | ||
|
|
4898cdb614 | ||
|
|
0923add514 | ||
|
|
d5e019b078 | ||
|
|
b81030849d | ||
|
|
0cbf859c94 | ||
|
|
e224990c85 | ||
|
|
ca78baa008 | ||
|
|
72eb6622ac | ||
|
|
f9e15951f2 | ||
|
|
a46e32cdf1 | ||
|
|
2b89307897 | ||
|
|
ffcdecad30 | ||
|
|
167a870f6a | ||
|
|
95083929ce | ||
|
|
50378e624a | ||
|
|
93b3157c35 | ||
|
|
5eee136189 | ||
|
|
1a5e31c01a | ||
|
|
1b8df76682 | ||
|
|
70fb387268 | ||
|
|
27686f6d78 | ||
|
|
e7f20e7d10 | ||
|
|
dbc6a25791 | ||
|
|
d7d803d4f9 | ||
|
|
461c239cc8 | ||
|
|
05fb5872d0 | ||
|
|
f72f3316f4 | ||
|
|
5909ce372b | ||
|
|
386844378e | ||
|
|
a9e48770b7 | ||
|
|
99b56f1ab7 | ||
|
|
8cc7f66169 | ||
|
|
733645e370 | ||
|
|
a20fc50aed | ||
|
|
978e3e4de2 | ||
|
|
da109146c9 | ||
|
|
cca45d3729 | ||
|
|
b14059e134 | ||
|
|
cb6a7d4a92 | ||
|
|
51d823a0b2 | ||
|
|
5cfc9aa518 | ||
|
|
0dea18f582 | ||
|
|
53df67e641 | ||
|
|
2448ffe337 | ||
|
|
9ace5bab94 | ||
|
|
ccf2d11034 | ||
|
|
190b6c6e7a | ||
|
|
573ccb0bc6 | ||
|
|
910fd27c14 | ||
|
|
5f65298728 | ||
|
|
4ca150198e | ||
|
|
71f6186a88 | ||
|
|
e3fce5fddc | ||
|
|
958252515e | ||
|
|
0e139b8640 | ||
|
|
8ed843a9cf | ||
|
|
080d346517 | ||
|
|
73e0cbe182 | ||
|
|
b750099b16 | ||
|
|
f00a3fb1d2 | ||
|
|
1b1090d78f | ||
|
|
34645560bf | ||
|
|
785beffee0 | ||
|
|
f9e9c2793a | ||
|
|
83916b9fb0 | ||
|
|
ac5c13663b | ||
|
|
e92d6d0c1c | ||
|
|
b44249317a | ||
|
|
e8e0452f32 | ||
|
|
0608089eb0 | ||
|
|
b3e21498fd | ||
|
|
9c407a6204 | ||
|
|
9ec879cea0 | ||
|
|
5c07a2556e | ||
|
|
f719e1cfe7 | ||
|
|
e95a6bf759 | ||
|
|
7bbf5d59e4 | ||
|
|
790d08fa13 | ||
|
|
0a4706e6f4 | ||
|
|
af833f6682 | ||
|
|
6de4df23b2 | ||
|
|
2c9dcd430f | ||
|
|
71c2eb3567 | ||
|
|
c02351fde3 | ||
|
|
968a360fde | ||
|
|
03c48376c4 | ||
|
|
5e2625ad21 | ||
|
|
711d102425 | ||
|
|
55f1f5c548 | ||
|
|
b8e2ae9bb7 | ||
|
|
6ed5283454 | ||
|
|
231a857813 | ||
|
|
40f4b1c926 | ||
|
|
3ce0473c86 | ||
|
|
0b6575ca97 | ||
|
|
9ebbe11d83 | ||
|
|
016a9b5762 | ||
|
|
ddd6063f25 | ||
|
|
81554c510e | ||
|
|
1d1df15d5d | ||
|
|
8e12f4e790 | ||
|
|
179850cdb6 | ||
|
|
bf14c8e32d | ||
|
|
f965c9ffa7 | ||
|
|
0ff60283c2 | ||
|
|
06094c54b3 | ||
|
|
065f90ea98 | ||
|
|
c44fe4c89e | ||
|
|
1964f30bfc | ||
|
|
07bd76cd61 | ||
|
|
32b0a3bb12 | ||
|
|
4b8bfae7c9 | ||
|
|
0697a0dd5b | ||
|
|
4cf7864916 | ||
|
|
52fb153790 | ||
|
|
49d965ae7b | ||
|
|
55b76de3f4 | ||
|
|
29f3de3587 | ||
|
|
df4a071a0f | ||
|
|
cb4cf4c3ac | ||
|
|
cd579528b5 | ||
|
|
b36e7ea750 | ||
|
|
2ce63abf04 | ||
|
|
199467f31b | ||
|
|
b4b19d3ad5 | ||
|
|
31cfc333c7 | ||
|
|
d18b89484a | ||
|
|
6be517ad3e | ||
|
|
81e6e682bb | ||
|
|
907acefbc8 | ||
|
|
449a5bf0fc | ||
|
|
350caefbf8 | ||
|
|
fad48ebaed | ||
|
|
bf23ee4e8f | ||
|
|
6517dc0de8 | ||
|
|
96cb39c334 | ||
|
|
845888bc75 | ||
|
|
1582b3ee43 | ||
|
|
c0406e7c3c | ||
|
|
2746636119 | ||
|
|
c4338c1a6f | ||
|
|
e10112be3b | ||
|
|
5b7ff8bdd6 | ||
|
|
efd2c40cfc | ||
|
|
7eb652b970 | ||
|
|
8973100f95 | ||
|
|
13c1f429b1 | ||
|
|
3f509a876e | ||
|
|
5916ed43d2 | ||
|
|
5c4ab2578d | ||
|
|
8692c3970d | ||
|
|
8a95be7473 | ||
|
|
5db53aa0b1 | ||
|
|
7a0ccd0076 | ||
|
|
a04196b5be | ||
|
|
ce7bd9def7 | ||
|
|
d7fda2d598 | ||
|
|
19c60876d1 | ||
|
|
67e609f800 | ||
|
|
7c3d2367f2 | ||
|
|
83eea8974b | ||
|
|
a1fc92bd00 | ||
|
|
3060645a26 | ||
|
|
0c6c578e35 | ||
|
|
e08f134c32 | ||
|
|
23038671b3 | ||
|
|
ae24b5e1ea | ||
|
|
918903f831 | ||
|
|
f3241c286c | ||
|
|
e682176040 | ||
|
|
8bea0c5cf8 | ||
|
|
497fd56efc | ||
|
|
100f4b66fd | ||
|
|
e2797fd796 | ||
|
|
e827d6d1d0 | ||
|
|
b9609a00f6 | ||
|
|
aaa06a5e88 | ||
|
|
26851520c8 | ||
|
|
cf0b5dc931 | ||
|
|
b2592b506f | ||
|
|
e84551b301 | ||
|
|
cebcee8d32 | ||
|
|
d75c6d7e9a | ||
|
|
eed514a432 | ||
|
|
218e783bb1 | ||
|
|
e1e1689d23 | ||
|
|
5c7a87dbfd | ||
|
|
5e7f5a68c2 | ||
|
|
5b19e0fdcd | ||
|
|
648fad33d8 | ||
|
|
843bb30b99 | ||
|
|
0f093d5f54 | ||
|
|
3a09a15970 | ||
|
|
83acf8b93d | ||
|
|
1793703e47 | ||
|
|
6713ee155c | ||
|
|
17f8830fbf | ||
|
|
24d995828f | ||
|
|
ae379bd8f5 | ||
|
|
f2cebc82cc | ||
|
|
2fa2e16b21 | ||
|
|
f670ccd8a6 | ||
|
|
9ffaec6d16 | ||
|
|
60de35ddf3 | ||
|
|
41faf794cc | ||
|
|
be37b4b3e7 | ||
|
|
3598b07185 | ||
|
|
602f88dab9 | ||
|
|
a4f5c290b5 | ||
|
|
8e42df11f3 | ||
|
|
8c6daa0976 | ||
|
|
bb629a7be2 | ||
|
|
80a7d7da2d | ||
|
|
c45795434a | ||
|
|
04971f3f65 | ||
|
|
6bf5356d00 | ||
|
|
0cd7db633b | ||
|
|
d9967bda8f | ||
|
|
34b9d8c334 | ||
|
|
12ff6679e4 | ||
|
|
4f9a250a9e | ||
|
|
c622cacadc | ||
|
|
786fb4e1c0 | ||
|
|
75bc521fa5 | ||
|
|
71296f9180 | ||
|
|
86672d882e | ||
|
|
3fc82af3ac | ||
|
|
22835e7535 | ||
|
|
f979a0c23d | ||
|
|
5f1f2b34b7 | ||
|
|
9ab2fc1714 | ||
|
|
b25f484ef3 | ||
|
|
dd0b565de9 | ||
|
|
29d88fd61d | ||
|
|
7625725fd8 | ||
|
|
cef25bd2f3 | ||
|
|
1e9cee1003 | ||
|
|
15bcc2d947 | ||
|
|
763dbebf13 | ||
|
|
667addf25b | ||
|
|
1033923f68 | ||
|
|
d410f9d3c4 | ||
|
|
a1cc3412e8 | ||
|
|
a98c9ffe8d | ||
|
|
5d506ca97e | ||
|
|
51c0bfa970 | ||
|
|
5c8e783980 | ||
|
|
b7d95819c1 | ||
|
|
bb0bf66eac | ||
|
|
be7008722b | ||
|
|
5535282894 | ||
|
|
2fb5f2b738 | ||
|
|
e87621576d | ||
|
|
531625ff08 | ||
|
|
0d57f710aa | ||
|
|
0da431fe33 | ||
|
|
88f984b0a8 | ||
|
|
ddb9d6968b | ||
|
|
3313535b27 | ||
|
|
cccc6e5a44 | ||
|
|
8b802a9cbc | ||
|
|
0a3a56d4bc | ||
|
|
7bae6cd562 | ||
|
|
e8151678dd | ||
|
|
9dd6022907 | ||
|
|
48e0d579cf | ||
|
|
cc9f61005b | ||
|
|
48ba8ff255 | ||
|
|
3f570cce5e | ||
|
|
01ee24c8cf | ||
|
|
1deff6fa49 | ||
|
|
4404d31c7b | ||
|
|
989562b90f | ||
|
|
bd63e4484d | ||
|
|
df642fe7a1 | ||
|
|
cb8f2619f2 | ||
|
|
2aed461b3a | ||
|
|
f4d75a2c6f | ||
|
|
e36d51a7c2 | ||
|
|
7267b70066 | ||
|
|
62d1a89269 | ||
|
|
6d0ac603f2 | ||
|
|
37f2e5bf3a | ||
|
|
9ddaf50ad4 | ||
|
|
42f488df0b | ||
|
|
d2a7de2775 | ||
|
|
ee1fc53dd3 | ||
|
|
192741af36 | ||
|
|
cd4ace4ad5 | ||
|
|
39426dde45 | ||
|
|
bd4eb3e105 | ||
|
|
c37c56c879 | ||
|
|
d26fc84316 | ||
|
|
33586aaf4f | ||
|
|
650b06d7c8 | ||
|
|
09807b25a8 | ||
|
|
13d42c3f83 | ||
|
|
6966b14c6b | ||
|
|
00ab0c4be2 | ||
|
|
5c06e74659 | ||
|
|
b0a0050725 | ||
|
|
c5a7fbd15a | ||
|
|
fa093ea237 | ||
|
|
782759bd48 | ||
|
|
653e507e89 | ||
|
|
4f9c7a7370 | ||
|
|
c114256b82 | ||
|
|
cdd72820b4 | ||
|
|
4b8e7e972e | ||
|
|
68b04b0e46 | ||
|
|
d00b7a176e | ||
|
|
3c3abd484f | ||
|
|
9d5eece5a5 | ||
|
|
f08df08c7e | ||
|
|
44e5bc646d | ||
|
|
74eb8f38f9 | ||
|
|
5ff8fb170d | ||
|
|
39a5b486d3 | ||
|
|
6664a139d8 | ||
|
|
d5de5b3a78 | ||
|
|
c3024e38a6 | ||
|
|
cfd05400d2 | ||
|
|
64992a2949 | ||
|
|
8ea7656362 | ||
|
|
cd760c45a8 | ||
|
|
15f6c8dd85 | ||
|
|
7d2dd0bcbe | ||
|
|
608525d5b4 | ||
|
|
49d4d508d4 | ||
|
|
0e286ab7b5 | ||
|
|
4e27591afa | ||
|
|
01c5f3e2a9 | ||
|
|
436d2f87da | ||
|
|
2e2a397312 | ||
|
|
54a5461ecb | ||
|
|
49ddd5ce96 | ||
|
|
e9c1e7b770 | ||
|
|
c298e5bc24 | ||
|
|
48e5e7ca95 | ||
|
|
55fb5befb9 | ||
|
|
22c7e21a66 | ||
|
|
a88f48f3ce | ||
|
|
6c673697f0 | ||
|
|
4af7255917 | ||
|
|
dfa33724e9 | ||
|
|
568fd35ddc | ||
|
|
bc674edb3a | ||
|
|
f5c7935993 | ||
|
|
0c30ce3bb3 | ||
|
|
e38a6cd907 | ||
|
|
dd98a1d078 | ||
|
|
c9611cca99 | ||
|
|
8a17184ccc | ||
|
|
e5b193f21a | ||
|
|
b9c62bc44a | ||
|
|
243ded448d | ||
|
|
ae561e5564 | ||
|
|
5ffb8cbd96 | ||
|
|
07d0e555e5 | ||
|
|
e5c80ef3af | ||
|
|
11cc62d9a9 | ||
|
|
8c9f07643f | ||
|
|
85607b97d5 | ||
|
|
0bc67adea8 | ||
|
|
1cfab07b8b | ||
|
|
499e63e2c3 | ||
|
|
bc8624d605 | ||
|
|
11a3c49464 | ||
|
|
23becb49b2 | ||
|
|
72b594934e | ||
|
|
d535d312a5 | ||
|
|
4b0215c16f | ||
|
|
8292da414d | ||
|
|
b2cce52b34 | ||
|
|
6ec248dfbf | ||
|
|
634d48e3e7 | ||
|
|
391efb1b2b | ||
|
|
07e64cfa02 | ||
|
|
594a8dd3cc | ||
|
|
f5ef509af6 | ||
|
|
e541111a27 | ||
|
|
c0c310862a | ||
|
|
65f09c46ca | ||
|
|
f199dd5b9f | ||
|
|
56e53219c2 | ||
|
|
cee00d8315 | ||
|
|
cbc124eff6 | ||
|
|
219b52fc12 | ||
|
|
98e8fc88bd | ||
|
|
9a73a10a09 | ||
|
|
6ff3a34c14 | ||
|
|
0499ded1b3 | ||
|
|
9fa04702af | ||
|
|
89c4cc9d79 | ||
|
|
b49562f4bc | ||
|
|
5f15ddc52c | ||
|
|
5bde9804d6 | ||
|
|
547f1aed75 | ||
|
|
5aed3fbb87 | ||
|
|
13cb4d321a | ||
|
|
004a99e080 | ||
|
|
2aee878a40 | ||
|
|
ca042631b7 | ||
|
|
7e51061582 | ||
|
|
e71ff17a23 | ||
|
|
2270300177 | ||
|
|
3f79fa5b9d | ||
|
|
322a6a700a | ||
|
|
ba66c18f28 | ||
|
|
5f10154000 | ||
|
|
66290fe1ce | ||
|
|
0c3340b543 | ||
|
|
c7ba4e8db6 | ||
|
|
65ec41c6ce | ||
|
|
395969a371 | ||
|
|
f2f818acb3 | ||
|
|
5668c37127 | ||
|
|
5f67f02cde | ||
|
|
f0fcd7155c | ||
|
|
f96348cf59 | ||
|
|
f7f4803fdc | ||
|
|
810c202f1b | ||
|
|
2d770ae05e | ||
|
|
5dfeb60a13 | ||
|
|
5896bdccc1 | ||
|
|
206c046382 | ||
|
|
3ce66894f0 | ||
|
|
cfcdcd0838 | ||
|
|
474980060e | ||
|
|
e816b4e58b | ||
|
|
3f4460ad6f | ||
|
|
92e2d45f0d | ||
|
|
7e7c920816 | ||
|
|
6606f0f58b | ||
|
|
b64a0f17eb | ||
|
|
7a5e878733 | ||
|
|
5edec0c0cb | ||
|
|
8912b1de62 |
8
.coveragerc
Normal file
8
.coveragerc
Normal file
@@ -0,0 +1,8 @@
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
|
||||
[run]
|
||||
branch = True
|
||||
source = game,pydcs_extensions,qt_ui,resources/tools
|
||||
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -3,7 +3,7 @@ name: Bug report
|
||||
description: >
|
||||
Use for any bug that happens after campaign generation. If the New Game wizard
|
||||
failed, use the "New Game wizard failed" template instead.
|
||||
labels: bug
|
||||
labels: ["Bug", "Priority Medium"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -31,7 +31,7 @@ body:
|
||||
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.
|
||||
options:
|
||||
- 1.0.0
|
||||
- v1.2.1
|
||||
- Test build
|
||||
- Development build
|
||||
- type: input
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/campaign_update.md
vendored
8
.github/ISSUE_TEMPLATE/campaign_update.md
vendored
@@ -2,19 +2,19 @@
|
||||
name: Campaign update submission
|
||||
about: Submit an update to a campaign you maintain.
|
||||
title: 'Update for <campaign name>'
|
||||
labels: campaign-update-submission
|
||||
labels: Campaign Update Submission
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
This form should only be used for submitted updated miz/json files for campaigns
|
||||
distributed with Liberation. If you are _requesting_ an update to a campaign, see
|
||||
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance. If the
|
||||
distributed with Retribution. If you are _requesting_ an update to a campaign, see
|
||||
https://github.com/dcs-retribution/dcs-retribution/wiki/Campaign-maintenance. If the
|
||||
campaign has an owner, it will be updated before release. If it does not, you can
|
||||
volunteer to own it.
|
||||
|
||||
If you are not the owner of the campaign listed on
|
||||
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance, please start
|
||||
https://github.com/dcs-retribution/dcs-retribution/wiki/Campaign-maintenance, please start
|
||||
there.
|
||||
|
||||
Otherwise, delete everything above the line below and fill out the following form. Note:
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/code_architecture.yml
vendored
Normal file
17
.github/ISSUE_TEMPLATE/code_architecture.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Code Architecture
|
||||
description: >
|
||||
Suggest a refactor, or other issues related to the codebase.
|
||||
labels: ["Refactor", "Python", "Priority Low"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Before filing, please search the issues to see if the problem has already been reported.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe your proposal.
|
||||
description: >
|
||||
Indicate where the issue lies and briefly explain how you plan to tackle it.
|
||||
validations:
|
||||
required: true
|
||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,12 +1,12 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: FAQ
|
||||
url: https://discord.gg/PXHA6AXw
|
||||
url: https://discord.gg/b4x34Bg4We
|
||||
about: Check to see if your issue is in the FAQ.
|
||||
- name: Manual
|
||||
url: https://github.com/dcs-liberation/dcs_liberation/wiki/
|
||||
url: https://github.com/dcs-retribution/dcs-retribution/wiki/
|
||||
- name: Feature blocking DCS AI bugs
|
||||
url: https://github.com/dcs-liberation/dcs_liberation#dcs-bugs
|
||||
url: https://github.com/dcs-retribution/dcs-retribution#dcs-bugs
|
||||
about: >
|
||||
A list of known DCS bugs that prevent us from improving AI behavior. Check
|
||||
the list before filing AI bugs here to see if it's something we know about
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Before filing, please search the issue tracker to see if this feature has already been requested.
|
||||
|
||||
If requesting a DCS AI feature, check If reporting a DCS AI bug, check https://github.com/dcs-liberation/dcs_liberation#dcs-bugs.
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
31
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
31
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Feature Request
|
||||
description: >
|
||||
Suggest an idea for this project.
|
||||
labels: ["Enhancement", "Priority Medium"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Before filing, please search the issue tracker to see if this feature has already been requested.
|
||||
If requesting a DCS AI feature, check If reporting a DCS AI bug,
|
||||
check https://github.com/dcs-retribution/dcs-retribution#dcs-bugs.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: >
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: >
|
||||
A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: >
|
||||
Add any other context or screenshots about the feature request here.
|
||||
4
.github/ISSUE_TEMPLATE/mod_support.md
vendored
4
.github/ISSUE_TEMPLATE/mod_support.md
vendored
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Mod support request
|
||||
about: Request Liberation support for new mods, or updates to existing mods
|
||||
about: Request Retribution support for new mods, or updates to existing mods
|
||||
title: Add/update <mod name>
|
||||
labels: mod support
|
||||
labels: Mod Request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
2
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
@@ -3,7 +3,7 @@ name: New Game wizard failed
|
||||
description: >
|
||||
Use for bugs that prevent the "New Game" wizard from completing successfully.
|
||||
If the wizard completes without issue, use the normal bug report template.
|
||||
labels: bug
|
||||
labels: ["Bug", "Priority High"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -19,5 +19,3 @@ runs:
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
python -m pip install -r requirements.txt
|
||||
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
|
||||
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force
|
||||
|
||||
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -45,9 +45,19 @@ jobs:
|
||||
with:
|
||||
webhook_url: ${{ secrets.discord_webhook_betas }}
|
||||
title: "Build #${{ github.run_number }} available!"
|
||||
message: "**Branch:** ${{ github.ref_name }}\n**Download link:\n**
|
||||
https://github.com/dcs-retribution/dcs-retribution/actions/runs/${{ github.run_id }}"
|
||||
message: "**Branch:** ${{ github.ref_name }}\n**Download link:\n** https://github.com/dcs-retribution/dcs-retribution/actions/runs/${{ github.run_id }}"
|
||||
include_image: true
|
||||
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
||||
username: GitHub Build Notifier
|
||||
if: startsWith(github.ref_name, 'test')
|
||||
if: startsWith(github.ref_name, 'test')
|
||||
|
||||
- name: Send status to Discord
|
||||
uses: stegzilla/discord-notify@v2
|
||||
with:
|
||||
webhook_url: ${{ secrets.discord_webhook_alphas }}
|
||||
title: "Build #${{ github.run_number }} available!"
|
||||
message: "**Branch:** ${{ github.ref_name }}\n**Download link:\n** https://github.com/dcs-retribution/dcs-retribution/actions/runs/${{ github.run_id }}"
|
||||
include_image: true
|
||||
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
||||
username: GitHub Build Notifier
|
||||
if: github.ref_name == 'dev'
|
||||
|
||||
1
.github/workflows/lint.yml
vendored
1
.github/workflows/lint.yml
vendored
@@ -13,6 +13,7 @@ jobs:
|
||||
with:
|
||||
src: "."
|
||||
options: "--check"
|
||||
version: "23.9.1"
|
||||
|
||||
mypy:
|
||||
name: Type checking
|
||||
|
||||
22
.github/workflows/test.yml
vendored
22
.github/workflows/test.yml
vendored
@@ -15,4 +15,24 @@ jobs:
|
||||
- name: run tests
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
pytest tests
|
||||
pytest --cov --cov-report=xml tests
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
ts-tests:
|
||||
name: Typescript tests
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up JS environment
|
||||
uses: ./.github/actions/setup-liberation-js
|
||||
|
||||
- name: run tests
|
||||
run: |
|
||||
cd client
|
||||
npm test -- --coverage
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,11 +1,15 @@
|
||||
*.pyc
|
||||
__pycache__
|
||||
build/**
|
||||
# Sphinx
|
||||
docs/_build
|
||||
resources/payloads/*.lua
|
||||
venv
|
||||
.DS_Store
|
||||
.vscode/settings.json
|
||||
dist/**
|
||||
/.coverage
|
||||
/coverage.xml
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
.env
|
||||
|
||||
13
.readthedocs.yaml
Normal file
13
.readthedocs.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
version: 2
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
@@ -11,7 +11,7 @@ Note that you may need to remove the filter for open bugs if it's something we'v
|
||||
|
||||
## Making content for Liberation
|
||||
|
||||
You can create new campaigns : See [campaign creation wiki](https://github.com/dcs-liberation/dcs_liberation/wiki/Custom-Campaigns).
|
||||
You can create new campaigns : See [campaign creation wiki](https://github.com/dcs-retribution/dcs-retribution/wiki/Custom-Campaigns).
|
||||
You can also improve existing campaigns.
|
||||
|
||||
You can then submit new campaigns on the "campaigns" channel on Discord, or by making a pull request if you are comfortable with git.
|
||||
|
||||
52
README.md
52
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
(Github Readme Banner and Splash screen Artwork by Andriy Dankovych, CC BY-SA 4.0)
|
||||
|
||||
[](https://patreon.com/khopa)
|
||||
[](https://patreon.com/dcsliberation)
|
||||
|
||||
[](https://github.com/dcs-retribution/dcs-retribution/releases)
|
||||
|
||||
@@ -12,42 +12,48 @@
|
||||
[](https://github.com/dcs-retribution/dcs-retribution/issues)
|
||||

|
||||
|
||||
## About DCS Retribution
|
||||
DCS Retribution was forked from [DCS Liberation](https://github.com/dcs-retribution/dcs-retribution),
|
||||
## About DCS Retribution
|
||||
(Last update: 2023-05-20)
|
||||
|
||||
DCS Retribution was forked from [DCS Liberation](https://github.com/dcs-liberation/dcs_liberation),
|
||||
which is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign.
|
||||
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
|
||||
|
||||

|
||||
|
||||
DCS Retribution will rely heavily on Liberation in the near future,
|
||||
and possibly in a more distant future as well since Retribution will attempt
|
||||
to stay backwards compatible for as long as possible. Therefore,
|
||||
DCS Retribution is still relying on Liberation for certain updates,
|
||||
and will possibly stay that way in a more distant future. Therefore,
|
||||
the patreon link will keep pointing to [Khopa's](https://github.com/Khopa) account until further notice.
|
||||
However, we are no longer backwards compatible with Liberation, and will no longer attempt to do so.
|
||||
|
||||
Currently, Retribution supports the following new features:
|
||||
* A livery selector in the air wing configuration dialog
|
||||
* Support for campaigns with quad-point triggers
|
||||
* Support for the F-15D 'Baz' mod
|
||||
* Support for the Su-30 mod
|
||||
* Elementary end-game mechanics
|
||||
* Variation in flight-size for BAI/ANTISHIP/DEAD/STRIKE main flight (randomize 2/3/4-ships)
|
||||
* More performance options
|
||||
* Various bug-fixes (including a fix for the zombie carrier)
|
||||
* And more... (See [changelog](https://github.com/dcs-retribution/dcs-retribution/blob/main/changelog.md))
|
||||
Instead, we are focussing on keeping our campaigns forward compatible so that you as a user
|
||||
can continue on whatever campaign you had going on.
|
||||
Please keep in mind that once you save a campaign that was started
|
||||
in a previous (save-compatibility breaking) build, your save file
|
||||
will have been migrated and thus no longer compatible with the previous build.
|
||||
Therefore, we recommend backing up your saves and perhaps organizing them by version/build number.
|
||||
|
||||
For a more complete overview of our features, check the
|
||||
[changelog](https://github.com/dcs-retribution/dcs-retribution/blob/main/changelog.md).
|
||||
|
||||
## Downloads
|
||||
|
||||
Latest release is available here : https://github.com/dcs-retribution/dcs-retribution/releases
|
||||
|
||||
To download preview builds of the next version of DCS Retribution, see https://github.com/dcs-retribution/dcs-retribution/wiki/Preview-builds.
|
||||
To download preview builds of the next version of DCS Retribution, see https://github.com/dcs-retribution/dcs-retribution/wiki/Betas.
|
||||
|
||||
## DCS bugs
|
||||
|
||||
These DCS bugs prevent us from improving AI behavior. Please upvote them! (But please
|
||||
_don't_ spam them with comments):
|
||||
~~These DCS bugs prevent us from improving AI behavior. Please upvote them! (But please
|
||||
_don't_ spam them with comments):~~
|
||||
|
||||
* [A2A and SEAD escorts don't escort](https://forums.eagle.ru/topic/251798-options-for-alternate-ai-escort-behavior/?tab=comments#comment-4668033)
|
||||
* [DEAD can't use mixed loadouts effectively](https://forums.eagle.ru/topic/271941-ai-rtbs-after-firing-decoys-despite-full-load-of-bombs/)
|
||||
* ~~[A2A and SEAD escorts don't escort](https://forums.eagle.ru/topic/251798-options-for-alternate-ai-escort-behavior/?tab=comments#comment-4668033)~~
|
||||
* ~~[DEAD can't use mixed loadouts effectively](https://forums.eagle.ru/topic/271941-ai-rtbs-after-firing-decoys-despite-full-load-of-bombs/)~~
|
||||
|
||||
While DCS bugs are still a thing, we don't quite agree with the ones stated above.
|
||||
We believe to have addressed these, although some quirks still exist.
|
||||
For example, SEAD Escorts tend to be a little picky with what they engage,
|
||||
especially with older SAM systems.
|
||||
|
||||
## Bugs and feature requests
|
||||
|
||||
@@ -62,7 +68,7 @@ Our plans for future releases can be found on our
|
||||
[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,
|
||||
in progress, and done. Items in the Done column are in the
|
||||
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Preview-builds)
|
||||
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Betas)
|
||||
for that release. Items in the To do column are planned to be added to that release.
|
||||
|
||||
## Resources
|
||||
@@ -70,7 +76,7 @@ for that release. Items in the To do column are planned to be added to that rele
|
||||
Tutorials, contributors and developer's guides are available in the project's
|
||||
[Wiki](https://github.com/dcs-retribution/dcs-retribution/wiki/)
|
||||
|
||||
(These need to be written again, meanwhile you can refer to
|
||||
(Some historical information is also availabe on
|
||||
[Liberation's Wiki](https://github.com/dcs-liberation/dcs_liberation/wiki/))
|
||||
|
||||
## Special Thanks
|
||||
|
||||
427
changelog.md
427
changelog.md
@@ -1,78 +1,455 @@
|
||||
# Retribution 1.0.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.
|
||||
|
||||
## Features/Improvements
|
||||
* **[Engine]** Support for DCS v2.9.3.51704
|
||||
* **[Package Planning]** Option to "Auto-Create" package
|
||||
* **[Modding]** Custom weapons injection system (definition in aircraft's yaml file)
|
||||
* **[Payload Editor]** Ability to save/back-up payloads
|
||||
* **[Options]** New option in Settings: CAS engagement range (nmi)
|
||||
* **[Options]** New option in Settings: Convert untasked OPFOR aircraft into client slots
|
||||
* **[Options]** Split the **Disable idle aircraft at airfields** setting into **Disable untasked BLUFOR aircraft at airfields** and **Disable untasked OPFOR aircraft at airfields**
|
||||
* **[Options]** Split off the **Automatic AWACS package planning** and **Automatic Theater tanker package planning** settings from **Automatic package planning behavior** so players can choose to have AWACS and theater tankers auto-planned, while managing everything else themselves
|
||||
* **[Modding]** Updated support for Su-30 mod to V2.7.3 Beta
|
||||
* **[Modding]** Updated support for Su-57 mod to build-04
|
||||
* **[Modding]** Updated support for F-4B/C Phantom mod to 2.8.7.204
|
||||
* **[Modding]** Updated Community A-4E-C mod version support to 2.2.0 release.
|
||||
* **[Modding]** Added F/A-18E/F Super Hornet AI Tanker mod support (Chiller Juice Studios SuperBug Tanker AI version 1.4)
|
||||
* **[Modding]** Added VSN Super Étendard mod support (v2.5.5)
|
||||
* **[Modding]** Added F9F Panther mod support (version v2.8.7.101)
|
||||
* **[Modding]** Updated Irondome support to IDF Assets Pack V1.1, adding support for the David's Sling
|
||||
* **[Radios]** Added HF-FM band for AN/ARC-222
|
||||
* **[Radios]** Ability to define preset channels for radios on squadron level (for human pilots only)
|
||||
* **[Mission Planning]** Avoid helicopters being assigned as escort to planes and vice-versa
|
||||
* **[Mission Planning]** Allow attack helicopters to escort other helicopters
|
||||
* **[UI]** Allow changing waypoint names in FlightEdit's waypoints tab
|
||||
* **[Waypoints]** Allow user to add navigation waypoints where possible without degrading to a custom flight-plan
|
||||
* **[Campaign Management]** Improve squadron retreat logic to account for parking-slot sizes
|
||||
* **[Autoplanner]** Support for auto-planning Air Assaults
|
||||
* **[UI]** Improved frequency selector to support all modeled bands for every aircraft's intra-flight radio
|
||||
* **[Options]** New options in Settings: Helicopter waypoint altitude (feet AGL) for combat & cruise waypoints
|
||||
* **[Options]** New options in Settings: Spawn ground power trucks at ground starts in airbases/roadbases
|
||||
* **[Options]** Option for hiding TGOs (with IADS roles) on MFD
|
||||
* **[Plugins]** Splash Damage 2.1 with Clusters and Ship Radar effects.
|
||||
* **[COMMs]** Aircraft-specific callsigns will now also be used.
|
||||
* **[COMMs]** Ability to set a specific callsign to a flight.
|
||||
* **[Mission Generator]** Channel terrain fix on exclusion zones, sea zones and inclusion zones
|
||||
* **[Options]** Cheat-option for accessing Air Wing Config Dialog after campaign start (re-initializes turn if applied, thus plan your mission ___after___ making changes)
|
||||
* **[Options]** Option to enable unlimited fuel for AI (player and non-player flights)
|
||||
* **[Mission Generator]** F-15E Strike targets are automatically added as Mission Set 1
|
||||
* **[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
|
||||
* **[Options]** Option to de-spawn AI flights in the air if their start-type was manually set to In-Flight
|
||||
* **[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
|
||||
* **[Options]** Made AI jettisoning empty fuel tanks optional (disabled by default)
|
||||
* **[Options]** Add option (so it can be disabled when fixed in DCS) to force air-starts (except for the slots that work) at Nevatim due to https://forum.dcs.world/topic/335545-29-nevatim-ramp-starts-still-bugged/
|
||||
* **[Cheat]** Add cheat option to manually manage REDFOR's TGOs
|
||||
* **[UX]** Buy/Replace TGOs for free before the campaign has started
|
||||
* **[Data]** Ability to define "cruise" & "combat" altitudes for airplanes
|
||||
* **[Options]** Option to randomize altitudes for flights with airplanes
|
||||
* **[Options]** Options to configure/override maximum mission distance for airplanes & helicopters
|
||||
|
||||
## Fixes
|
||||
* **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task
|
||||
* **[New Game Wizard]** Faction selection overview doesn't update when inverting map
|
||||
* **[New Game Wizard]** Aircraft mods are now handled better when they are disabled
|
||||
* **[Payloads]** Added/Updated (missing) payloads
|
||||
* **[Aircraft Tasking]** Revised aircraft tasking, filtering out incompatible tasks for several aircraft
|
||||
* **[Data]** Corrected the class of the USS Samuel Chase from Logistics to LandingShip, in order to prevent it being spawned as part of AAA sites.
|
||||
* **[Mission Generation]** Helicopters oscillating due to over-speeding
|
||||
* **[Mission Generation]** Fix infinite loop when using "Fast-Forward to first contact"
|
||||
* **[Capture Logic]** Release all parking slots when an airbase is captured
|
||||
* **[Modding]** Swedish Military Assets Pack air defence presets are now correctly removed from the faction when the mod is disabled.
|
||||
* **[Mission Generation]** Naval aircraft not always returning to carrier
|
||||
* **[Mission Generation]** AI AirLift aircraft crashing into terrain due to insufficient waypoints
|
||||
* **[Mission Generation]** Fix friendly AI shooting at fires on the front-line
|
||||
|
||||
# Retribution v1.2.1 (hotfix)
|
||||
|
||||
## Fixes
|
||||
* **[Flight Plans]** SEAD Sweep not being auto-planned
|
||||
* **[Modding]** Python-4 no longer overwrites AIM-9X
|
||||
* **[Modding]** Restore original amount of pylons to F-16C when "ejecting" sufa mod
|
||||
|
||||
# Retribution v1.2.0
|
||||
|
||||
## Features/Improvements
|
||||
* **[Preset Groups]** Add SA-2 with ZSU-23/57
|
||||
* **[Campaign Design]** Ability to define almost all possible settings in the campaign's yaml file.
|
||||
* **[Campaign Design]** Ability to add roadbases and/or ground spawns to campaigns.
|
||||
* **[Campaign Design]** Ability to define SCENERY REMOVE OBJECTS ZONE triggers with the roadbase objects in campaign miz. This might not work reliably in multiplayer due to DCS issues. FARPs can be used to remove scenery objects in multiplayer.
|
||||
* **[Options]** Implemented an option in settings to disable the above SCENERY REMOVE OBJECTS ZONE triggers.
|
||||
* **[Campaign Management]** Improved squadron retreat logic at longer ranges.
|
||||
* **[Options]** Ability to load & save your settings.
|
||||
* **[Options]** Added a separate Doctrine page in settings with the following new options:
|
||||
* Minimum number of aircraft for autoplanner to plan OCA packages against
|
||||
* Airbase threat range (nmi)
|
||||
* SEAD Sweep threat buffer distance (nmi)
|
||||
* SEAD Escort/Sweep threat buffer distance (nmi)
|
||||
* TARCAP threat buffer distance (nmi)
|
||||
* AEW&C threat buffer distance (nmi)
|
||||
* Theater tanker threat buffer distance (nmi)
|
||||
* **[Options]** Improved the option to configure OPFOR autoplanner aggressiveness. The AI might now take even more risks and plan missions against defended targets.
|
||||
* **[Options]** Added three new options in Settings:
|
||||
* Autoplanner plans refueling flights for Strike packages
|
||||
* Autoplanner plans refueling flights for OCA packages
|
||||
* Autoplanner plans refueling flights for DEAD packages
|
||||
* **[UI]** Added fuel selector in flight's edit window.
|
||||
* **[Plugins]** Expose Splash Damage's "game_messages" option and set its default to false.
|
||||
* **[Mission Generation]** Improved AI SEAD capabilities, allowing for mixed loadouts using Decoys, ARMs & ASMs.
|
||||
* **[Modding]** Support for A-7E Corsair II (presumed latest available version)
|
||||
* **[Squadrons]** Added many new squadron's by Adecarcer
|
||||
* **[Plugins]** Updated 'expl_table' in Splash Damage script.
|
||||
* **[Mission Generation]** Also save kneeboards in txt-format, found under "kneeboards" within Retribution's installation folder after pressing take-off.
|
||||
* **[Modding]** Support for SW mod v2.55
|
||||
* **[Modding]** Support for Spanish & Australian Naval Assets v3.2.0 by desdemicabina
|
||||
* **[Modding]** Support for Iron Dome v1.2 by IDF Mods Project
|
||||
* **[New Game Wizard]** Re-organized generator options & show the regular settings menu instead of the limited "Difficulty & Automation" page.
|
||||
* **[Campaign Management]** Ability to operate harriers from FOBs/FARPs for <ins>__human pilots only__</ins>. Please note that the autoplanner won't generate flights for harriers at FOBs/FARPs, which means you need to plan your missions manually.
|
||||
* **[Mission Planning]** Allow NAV/REFUEL/DIVERT waypoints to be deleted without degrading to a custom flight-plan, also warning the user before actually degrading the flight-plan.
|
||||
* **[Campaign Generation]** Split "full-strength start" from "squadron aircraft limits" option.
|
||||
* **[Mission Generation]** General improvement w.r.t. DCS tasking, including a check for incompatible tasking.
|
||||
* **[Mission Generation]** OCA-Runway flights will remain at altitude when using guided bombs.
|
||||
* **[UX]** Added error message to indicate save-compatibility issues + fix to avoid total crash upon loading of last save.
|
||||
* **[UI]** Improved parking space information in air wing configuration dialog.
|
||||
* **[Squadrons]** Warning messages when opening up a squadron through the air wing dialog, indicating squadrons that potentially won't fit w.r.t. parking space.
|
||||
* **[Squadrons Transfers]** Determine number of available parking slots more accurately w.r.t. squadron transfers, taking aircraft dimensions into account which should prevent forced air-starts.
|
||||
* **[UX]** Allow usage of CTRL/SHIFT modifiers in ground unit transfer window.
|
||||
* **[Campaign Design]** Ability to define "spawn-routes" for convoys, allowing them to start from the road without having to edit the mission
|
||||
* **[Plugins]** Added "DCS Dismount" plugin.
|
||||
* **[Plugins]** Added "EWR Jammer" plugin (only for humans, may change in the future).
|
||||
* **[Campaign]** New campaign (Operation Desert Sabre) by Chimiste
|
||||
* **[Plugins]** Updated CTLD to latest released version
|
||||
* **[Options]** Renamed Maximum frontline length -> Maximum frontline width.
|
||||
* **[Squadrons]** Add livery selector in Squadron Dialog, allowing you to change the livery during the campaign.
|
||||
* **[New Game Wizard]** Automatically invert factions when 'Invert Map' is selected.
|
||||
* **[Flight Plans]** Added "SEAD Sweep" flight plan, which basically reintroduces the legacy "SEAD Escort" flight plan where the flight will engage whatever it can find without actually escorting the primary flight.
|
||||
* **[Flight Plans]** Added SEAD capability to F-16A MLU and SEAD Escort & SEAD to F-16A.
|
||||
* **[Mission Generation]** Spawn unused helicopters or LHA-capable aircraft at helipads at FOBs
|
||||
* **[Modding]** Support for F-15I Ra'am v1.0 by IDF Mods Project
|
||||
|
||||
## Fixes
|
||||
* **[New Game Wizard]** Settings would not persist when going back to a previous page (obsolete due to overhaul).
|
||||
* **[Mission Generation]** Unused aircraft are no longer claimed, fixing a bug where these aircraft would no longer be available after aborting the mission.
|
||||
* **[Mission Generation]** Fixed (potential) bug in helipad assignments at FOBs/FARPs.
|
||||
* **[Mission Generation]** Fix AI immediately returning to base when forced to air-start due to insufficient parking space.
|
||||
* **[Modding]** Fixed a bug where F-16Ds were not correctly removed from the faction when the F-16I/F-16D mod was not selected
|
||||
* **[UI]** Fixed F-16A MLU icon and banner.
|
||||
|
||||
# Retribution v1.1.1 (hotfix)
|
||||
|
||||
## Features/Improvements
|
||||
* **[Modding]** Support for IDF Mod Project F-16I Sufa & F-16D v3.6 mod
|
||||
* **[Modding]** Support for JAS-39 Gripen v1.8.5-beta mod
|
||||
|
||||
## Fixes
|
||||
* **[Plugins]** Fix bug where changes to plugin options doesn't do anything.
|
||||
* **[Campaign Management]** Fix bug in procurement when no squadrons are present.
|
||||
* **[Layouts]** Fix edge-case bug layout's group size.
|
||||
* **[Campaign Design]** Preset groups assigned to specific TGOs not working as intended.
|
||||
|
||||
# Retribution v1.1.0
|
||||
|
||||
## Features/Improvements
|
||||
* **[Mission Generation]** Given a CAS flight was planned, delay ground force attack until first CAS flight is on station
|
||||
* **[Mission Generation]** Add option to switch ATFLIR to LITENING automatically for ground based F/A-18C flights
|
||||
* **[Mission Generation]** Add option to configure OPFOR autoplanner aggressiveness and have the AI take risks and plan missions against defended targets
|
||||
* **[Mission Generation]** Add option to configure the desired tanker on-station time in settings
|
||||
* **[Mission Generation]** Reserve GUARD frequency on VHF/UHF
|
||||
* **[Mission Generation]** Randomization in radio frequency allocation
|
||||
* **[Mission Generation]** Configurable number of Combined Arms slots
|
||||
* **[Mission Generation]** Enable spectating & F11 free camera when the "Allow external views" option is selected
|
||||
* **[Cheat Menu]** Option to instantly transfer squadrons across bases.
|
||||
* **[Modding]** Support for IDF Mod Project F-16I Sufa & F-16D v3.2 mod
|
||||
* **[Modding]** Support for F/A-18E/F/G mod version 2.1
|
||||
* **[Modding]** Support for Swedish Military Assets for DCS by Currenthill Version 1.10
|
||||
* **[UI]** Add selectable units in faction overview during campaign generation.
|
||||
* **[UI]** Add button to rename pilots in Air Wing's Squadron dialog.
|
||||
* **[UI]** Add clone buttons for flights & packages.
|
||||
* **[UI]** Editing of flight's custom name.
|
||||
* **[UI]** Introduce custom names for packages (purely for organizational purposes).
|
||||
* **[UI]** Configurable UHF frequency (225-400MHz) for Packages, Carriers, LHAs, FOBs & FARPs.
|
||||
* **[UI]** Configurable Intra-Flight frequency for Flights.
|
||||
* **[UI]** Configurable TACAN for Carriers, LHAs & Tankers.
|
||||
* **[UI]** Configurable ICLS for capable Carriers & LHAs.
|
||||
* **[UI]** Configurable LINK4 for Carriers.
|
||||
* **[Kneeboard]** Show package information in Support page
|
||||
* **[Kneeboard]** Show extra weather information on 'Mission info' page
|
||||
* **[Kneeboard]** Show BRC in 'RWY' column for aircraft carriers on 'Mission info' page
|
||||
* **[Campaign Design]** Ability to define designated CTLD zones for Control Points (Airbases & FOBs/FARPs)
|
||||
* **[Campaign Design]** Ability to define preset groups for specific TGOs, given the preset group is accessible for the faction and the task matches.
|
||||
* **[Campaign Management]** Additional options for automated budget management.
|
||||
* **[Campaign Management]** New options to allow more control of randomized flight sizes (applicable for BARCAP/CAS/OCA/ANTI-SHIP).
|
||||
* **[Plugins]** Updated Splash Damage script to v2.0 by RotorOps.
|
||||
* **[Mission Generation]** Improvements to DEAD & STRIKE flights, allowing AI to handle a larger variety of weapons.
|
||||
* **[Campaign]** New campaign (1968 Yankee Station) by Adecarcer
|
||||
|
||||
## Fixes
|
||||
* **[UI]** Removed deprecated options
|
||||
* **[UI]** Add missing icon & banner for C130 Hercules mod
|
||||
* **[Mission Generation]** Avoid aircraft from being assigned to helicopter parking spots, resulting into air starts that usually crash.
|
||||
* **[Mission Generation]** Use stacking algorithm to create vertical separation between flights spawning mid-mission over their departure, usually resulting into mid-air collisions.
|
||||
* **[Mission Generation]** Fixed all callsigns being "Enfield 1-1" on dedicated servers.
|
||||
* **[Mission Generation]** Fixed AI ferry flights for helicopters when transferring to a FOB/FARP.
|
||||
* **[Mission Generation]** Fixed 'Uninitialized flight' exception when adding flights after aborted take-off.
|
||||
* **[Modding]** Fixed conflicts caused by HDS units
|
||||
* **[UX]** Gracefully handle corrupted preferences file.
|
||||
* **[Mission Generation]** Aircraft not using decoys during SEAD.
|
||||
|
||||
# Retribution v1.0.1 (hotfix)
|
||||
* **[Mission Generation]** Fix serialization issue when STRIKE flight has no escorts
|
||||
|
||||
# Retribution v1.0.0
|
||||
|
||||
## Features/Improvements
|
||||
* **[Engine]** Support for DCS v2.8.1.34437.
|
||||
* **[Briefing]** Add tanker info to mission briefing
|
||||
* **[Campaign]** Add 5 new campaigns by Oscar Juliet from WRL
|
||||
* **[Campaign]** Add ability to define livery overrides also for ground/naval units
|
||||
* **[Data]** Added data to support C-47 Skytrain.
|
||||
* **[Data]** Added data to support F-16A MLU.
|
||||
* **[Data]** Added data to support KS-19 & SON-9, including support for "AAA Site" layout.
|
||||
* **[Mission Generation]** Add option to configure the maximum front-line length in settings
|
||||
* **[Mission Generation]** Use Escort & SEAD tasks for Escort & SEAD Escort flights
|
||||
* **[Mission Generation]** Variable flight-size (2/3/4-ship) for
|
||||
BAI/ANTISHIP/DEAD/STRIKE/BARCAP/CAS/OCA/AIR-ASSAULT (main) missions
|
||||
* **[Mission Generation]** Add option to only generate night missions
|
||||
* **[Modding]** Support for F-15D 'Baz' mod version 1.0
|
||||
* **[Modding]** Support for Su-30 mod version 2.01B
|
||||
* **[Modding]** Support for A-6A Intruder version 2.7.5.01
|
||||
* **[Modding]** Support for F-4B Phantom II mod version v2.7.10.02, patch 2022.10.02
|
||||
* **[Modding]** Support for F-100 Super Sabre mod versions v2.7.18.01 & 2.7.18.30765 and patches 30.09.22 & 09.10.22
|
||||
* **[Modding]** Support for F-100 Super Sabre mod versions v2.7.18.01 & 2.7.18.30765 and patch 20.10.22
|
||||
* **[Modding]** Support for F-105 mod version 2.7.12.23x
|
||||
* **[Modding]** Support IDF Mod Project F-16I Sufa & F-16D v2.2 mod
|
||||
* **[Modding]** Support for F-84G mod version 2.5.7.01
|
||||
* **[Modding]** Updated F-104 mod version support to 2.7.11.222.01
|
||||
* **[Modding]** Updated Community A-4E-C mod version support to 2.1.0 release.
|
||||
* **[UI]** Add livery selector to Air Wing Configurator's squadrons.
|
||||
* **[Performance]** Added performance option: Maximum front-line unit supply per control point.
|
||||
* **[Performance]** Added performance option: Disable convoys.
|
||||
* **[Performance]** Added performance option: Front-line troops prefer roads.
|
||||
* **[Data]** Added data to support C-47 Skytrain.
|
||||
* **[Data]** Added data to support KS-19 & SON-9, including support for "AAA Site" layout.
|
||||
* **[Performance]** Added performance option: Disable idle aircraft at airfields.
|
||||
* **[Squadrons]** Squadron pilot limits enabled by default.
|
||||
* **[UI]** Add livery selector to Air Wing Configurator's squadrons.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Mission Generation]** Fixed issue where aircraft carriers would return after being killed.
|
||||
* **[Mission Generation]** Kneeboard STRIKE coordinates would sometimes get clipped when not fitting.
|
||||
* **[UI]** Fix exception when trying to add a waypoints to a flightplan.
|
||||
|
||||
|
||||
# Liberation:
|
||||
## Features/Improvements
|
||||
|
||||
* **[Data]** Added support for the ARA Veinticinco de Mayo.
|
||||
* **[Data]** Changed display name of the AI-only F-15E Strike Eagle for clarity.
|
||||
* **[Flight Planning]** Improved IP selection for targets that are near the center of a threat zone.
|
||||
* **[Flight Planning]** Moved CAS ingress point off the front line so that the AI begins their target search earlier.
|
||||
* **[Flight Planning]** Loadouts and aircraft properties can now be set per-flight member. Warning: AI flights should not use mixed loadouts.
|
||||
* **[Flight Planning]** Laser codes that are pre-assigned to weapons at mission start can now be chosen from a list in the loadout UI. This does not affect the aircraft's TGP, just the weapons. Currently only implemented for the F-15E S4+ and F-16C.
|
||||
* **[Mission Generation]** Configured target and initial points for F-15E S4+.
|
||||
* **[Modding]** Factions can now specify the ship type to be used for cargo shipping. The Handy Wind will be used by default, but WW2 factions can pick something more appropriate.
|
||||
* **[Modding]** Unit variants can now set a display name separate from their ID.
|
||||
* **[UI]** An error will be displayed when invalid fast-forward options are selected rather than beginning a never ending simulation.
|
||||
* **[UI]** Added cheats for instantly repairing and destroying runways.
|
||||
* **[UI]** Improved usability of the flight properties UI. It now shows human-readable names and uses more appropriate UI elements.
|
||||
* **[UI]** The map now shows the real front line bounds.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign]** Fixed error when canceling squadron transfer if the current location would be exactly full.
|
||||
* **[Data]** Fixed the class of the Samuel Chase so it can't be picked for a AAA or SHORAD site.
|
||||
* **[Data]** Allow CH-47D, CH-53E and UH-60A to operate from carriers and LHAs.
|
||||
* **[Data]** Added the F-15E's LANTIRN to the list of known targeting pods. Player F-15E flight with TGPs will now be assigned laser codes.
|
||||
* **[Flight Planning]** Patrolling flight plans (CAS, CAP, refueling, etc) now handle TOT offsets.
|
||||
* **[Mission Generation]** Restored previous AI behavior for anti-ship missions. A DCS update caused only a single aircraft in a flight to attack. The full flight will now attack like they used to.
|
||||
* **[Mission Generation]** Fix generation of OCA Runway missions to allow LGBs to be used.
|
||||
* **[Mission Generation]** Fixed AI flights flying far too slowly toward NAV points.
|
||||
* **[Mission Generation]** Fixed "division by zero" error on mission generation when a flight has an "In-Flight" start type and starts on top of a mission waypoint.
|
||||
* **[Modding]** Unit variants can now actually override base unit type properties.
|
||||
* **[Plugins]** Fixed Lua errors in Skynet plugin that would occur whenever one coalition had no IADS nodes.
|
||||
* **[UI]** Fixed deleting waypoints in custom flight plans deleting the wrong waypoint.
|
||||
* **[UI]** Fixed flight properties UI to support F-15E S4+ laser codes.
|
||||
* **[UI]** Fixed UI bug where altering an "ahead of package" TOT offset would change the offset back to a "behind pacakge" offset.
|
||||
* **[UI]** Fixed bug where changing TOT offsets could result in flight startup times that are in the past.
|
||||
* **[UI]** Flight plan paths are now drawn behind all other map elements, fixing rare cases where they could prevent other UI elements from being clickable.
|
||||
|
||||
# 8.1.0
|
||||
|
||||
Saves from 8.0.0 are compatible with 8.1.0
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.8.6.41363, including F-15E support.
|
||||
* **[UI]** Flight loadout/properties tab is now scrollable.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign]** Fixed liveries for premade squadrons all being off-by-one.
|
||||
* **[UI]** Fixed numbering of waypoints in the map and flight dialog (first waypoint is now 0 rather than 1).
|
||||
|
||||
# 8.0.0
|
||||
|
||||
Saves from 7.x are not compatible with 8.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.8.6.41066, including the new Sinai map.
|
||||
* **[UI]** Limited size of overfull airbase display and added scrollbar.
|
||||
* **[UI]** Waypoint altitudes can be edited in Waypoints tab of Edit Flight window.
|
||||
* **[UI]** Moved air wing and transfer menus to the toolbar to improve UI fit on low resolution displays.
|
||||
* **[UI]** Added basic game over dialog.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign]** Fix bug introduced in 7.0 where map strike target deaths are no longer tracked.
|
||||
* **[Mission Generation]** Fix crash during mission generation caused by out of date DCS data for the Gazelle.
|
||||
* **[Mission Generation]** Fix crash during mission generation when DCS beacon data is inconsistent.
|
||||
|
||||
# 7.1.0
|
||||
|
||||
Saves from 7.0.0 are compatible with 7.1.0
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Factions]** Replaced Patriot STRs "EWRs" with AN/FPS-117 for blue factions 1980 or newer.
|
||||
* **[Mission Generation]** Added option to prevent scud and V2 sites from firing at the start of the mission.
|
||||
* **[Mission Planning]** Per-flight TOT offsets can now be set in the flight details UI. This allows individual flights to be scheduled ahead of or behind the rest of the package.
|
||||
* **[UI]** Waypoint altitudes can be edited in Waypoints tab of Edit Flight window.
|
||||
* **[UI]** Parking capacity of each squadron's base is now shown during air wing configuration to avoid overcrowding bases when beginning the game with full squadrons.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Mission Planning]** BAI is once again plannable against missile sites and coastal defense batteries.
|
||||
* **[UI]** Fixed formatting of departure time in flight details dialog.
|
||||
|
||||
# 7.0.0
|
||||
|
||||
Saves from 6.x are not compatible with 7.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.8.3.37556.
|
||||
* **[Engine]** Saved games are now a zip file of save assets for easier bug reporting. The new extension is .liberation.zip. Drag and drop that file into bug reports.
|
||||
* **[Campaign]** Added options to limit squadron sizes and to begin all squadrons at maximum strength. Maximum squadron size is defined during air wing configuration with default values provided by the campaign.
|
||||
* **[Campaign]** Added handling for more DCS death events. This probably does not catch any deaths that weren't previously tracked, but it should record them sooner, which will improve results for game crashes or other early exits.
|
||||
* **[Campaign AI]** The campaign AI now prefers fulfilling missions with squadrons which have a matching primary task. Previously distance from target held a stronger influence than task preference. Primary tasks for squadrons are set by campaign designers but are user-configurable.
|
||||
* **[Flight Planning]** Package TOT and composition can be modified after advancing time in Liberation.
|
||||
* **[Mission Generation]** Units on the front line are now hidden on MFDs.
|
||||
* **[Mission Generation]** Preset radio channels will now be configured for both A-10C modules.
|
||||
* **[Mission Generation]** The A-10C II now uses separate radios for inter- and intra-flight comms (similar to other modern aircraft).
|
||||
* **[Mission Generation]** Wind speeds no longer follow a uniform distribution. Median wind speeds are now much lower and the standard deviation has been reduced considerably at altitude but increased somewhat at MSL.
|
||||
* **[Mission Generation]** Improved task generation for SEAD flights carrying TALDs.
|
||||
* **[Mission Generation]** Added task timeout for SEAD flights with TALDs to prevent AI from overflying the target.
|
||||
* **[Modding]** Updated Community A-4E-C mod version support to 2.1.0 release.
|
||||
* **[Modding]** Add support for VSN F-4B and F-4C mod.
|
||||
* **[Modding]** Aircraft task capabilities and preferred aircraft for each task are now moddable in the aircraft unit yaml files. Each aircraft has a weight per task. Higher weights are given higher preference.
|
||||
* **[Modding]** The `mission_types` field in squadron files has been removed. Squadron task capability is now determined by airframe, and the auto-assignable list has always been overridden by the campaign settings.
|
||||
* **[Modding]** Wind speed generation inputs are now moddable. See https://dcs-liberation.rtfd.io/en/latest/modding/weather.html.
|
||||
* **[New Game Wizard]** Choices for some options will be remembered for the next new game. Not all settings will be preserved, as many are campaign dependent.
|
||||
* **[New Game Wizard]** Lua plugins can now be set while creating a new game.
|
||||
* **[New Game Wizard]** Squadrons can be directly replaced with a preset during air wing configuration rather than needing to remove and create a new squadron.
|
||||
* **[New Game Wizard]** Squadron liveries can now be selected during air wing configuration.
|
||||
* **[Squadrons]** Squadron-specific mission capability lists no longer restrict players from assigning missions outside the squadron's preferences.
|
||||
* **[New Game Wizard]** Squadrons can be directly replaced with a preset during air wing configuration rather than needing to remove and create a new squadron.
|
||||
* **[UI]** The orientation of objects like SAMs, EWRs, garrisons, and ships can now be manually adjusted.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign]** Fixed a longstanding bug where oversized airlifts could corrupt a save with empty convoys.
|
||||
* **[Campaign]** Aircraft with built-in TGPs but without an external pod will no longer degrade automatic loadouts to iron bombs.
|
||||
* **[Engine]** Fixed crash in startup caused by a corrupted Liberation preferences file.
|
||||
* **[Flight Planning]** AEW&C missions are now plannable over FOBs and LHAs.
|
||||
* **[Flight Planning]** BAI is no longer plannable against buildings.
|
||||
* **[Modding]** Fixed an issue where Falklands campaigns created or edited with new versions of DCS could not be loaded.
|
||||
* **[Modding]** Fixed decoding of campaign yaml files to use UTF-8 rather than the system locale's default. It's now possible to use "Bf 109 K-4 Kurfürst" as a preferred aircraft type.
|
||||
* **[Mission Generation]** Planes will no longer spawn in helipads that are not also designated for fixed wing parking.
|
||||
* **[Mission Generation]** Potentially an issue where ground war planning game state could become corrupted, preventing mission generation.
|
||||
* **[Mission Generation]** Refueling tasks will now only be created for flights that have a tanker in their package.
|
||||
* **[Mission Generation]** Fixed missing Tanker task on recovery tanker missions.
|
||||
* **[UI]** Fixed error when resetting air wing configuration during game setup.
|
||||
* **[UI]** Fixed flight plan recreation when changing mission type with "Recreate as" flight options.
|
||||
* **[UI]** Fixed failure to launch UI when Liberation persistent preferences file was corrupt.
|
||||
|
||||
|
||||
# 6.1.1
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Data]** Fixed unit ID for the KS-19 AAA. KS-19 would not previously generate correctly in missions. A new game is required for this fix to take effect.
|
||||
* **[Flight Planning]** Automatic flight planning will no longer accidentally plan a recovery tanker instead of a theater refueling package. This fixes a potential crash during mission generation when opfor plans a refueling task at a sunk carrier. You'll need to skip the current turn to force opfor to replan their flights to get the fix.
|
||||
* **[Mission Generation]** Using heliports (airports without any runways) will no longer cause mission generation to fail.
|
||||
|
||||
# 6.1.0
|
||||
|
||||
Saves from 6.0.0 are compatible with 6.1.0
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Factions]** Defaulted bluefor modern to use Georgian and Ukrainian liveries for Russian aircraft.
|
||||
* **[Factions]** Added Peru.
|
||||
* **[Modding]** Added support for the HMS Ariadne, Achilles, and Castle class.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Flight Planning]** Fixes CAS flights not having landing waypoints.
|
||||
* **[Squadrons]** Fixed the livery for the VF-33 F-14A squadron.
|
||||
* **[UI]** Fixed an issue where manual submit of mission results did not end the mission correctly.
|
||||
|
||||
# 6.0.0
|
||||
|
||||
Saves from 5.x are not compatible with 6.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.7.18.30348.
|
||||
* **[Mission Generation]** Added an option to fast-forward mission generation until the point of first contact (WIP).
|
||||
* **[Mission Generation]** Added performance option to not cull IADS when culling would effect how mission is played at target area.
|
||||
* **[Mission Generation]** Reworked the ground object generation which now uses a new layout system
|
||||
* **[Mission Generation]** Added information about the modulation (AM/FM) of the assigned frequencies to the kneeboard and assign AM modulation instead of FM for JTAC.
|
||||
* **[Engine]** Support for DCS 2.8.0.33006.
|
||||
* **[Factions]** Updated the Faction file structure. Older custom faction files will not work correctly and have to be updated to the new structure.
|
||||
* **[Flight Planning]** Added preset formations for different flight types at hold, join, ingress, and split waypoints. Air to Air flights will tend toward line-abreast and spread-four formations. Air to ground flights will tend towards trail formation.
|
||||
* **[Flight Planning]** Added the ability to plan tankers for recovery on package flights. AI does not plan.
|
||||
* **[Flight Planning]** Added preset formations for different flight types at hold, join, ingress, and split waypoints. Air to Air flights will tend toward line-abreast and spread-four formations. Air to ground flights will tend towards trail formation.
|
||||
* **[Flight Planning]** Added the ability to plan tankers for recovery on package flights. This mission type will not be planned automatically.
|
||||
* **[Flight Planning]** Air to Ground flights now have ECM enabled on lock at the join point, and SEAD/DEAD also have ECM enabled on detection and lock at ingress.
|
||||
* **[Flight Planning]** AWACS flightplan changed from orbit to a racetrack to reduce data link disconnects which were caused by blind spots as a result of the bank angle.
|
||||
* **[Flight Planning]** Added a new helo mission type: AirAssault which can be used to load and transport infantry troops from a pickup zone or a carrier to an enemy CP to capture it.
|
||||
* **[Flight Planning]** Improved the Airlift mission type so that it now can be enforced within the unit transfer dialog and implemented CTLD support. This allows user to spawn sling loadable crates at the pickup location and fly transport flights.
|
||||
* **[Flight Planning]** Added a new helo mission type: AirAssault which can be used to load and transport infantry troops from a pickup zone or a carrier to an enemy CP to capture it.
|
||||
* **[Flight Planning]** Improved the Airlift mission type so that it now can be enforced within the unit transfer dialog and implemented CTLD support. This allows user to spawn sling loadable crates at the pickup location and fly transport flights.
|
||||
* **[Mission Generation]** Added an option to fast-forward mission generation until the point of first contact (WIP).
|
||||
* **[Mission Generation]** Added performance option to not cull IADS when culling would affect how mission is played at target area.
|
||||
* **[Mission Generation]** Reworked the ground object generation which now uses a new layout system
|
||||
* **[Mission Generation]** Added information about the modulation (AM/FM) of the assigned frequencies to the kneeboard and assign AM modulation instead of FM for JTAC.
|
||||
* **[Mission Generation]** Added ice halos.
|
||||
* **[Mission Generation]** Adjusted wind speeds. Wind speeds at high altitude are generally higher now.
|
||||
* **[Mission Generation]** Added turbulence. Higher in Summer and Winter, also higher at day time than at nighttime.
|
||||
* **[Modding]** Updated UH-60L mod version support to 1.3.1
|
||||
* **[Modding]** Updated the High Digit SAMs implementation and added the HQ-2 as well as the upgraded SA-2 and SA-3 Launchers from the mod. Threat range circles will now also be displayed correctly.
|
||||
* **[Modding]** Theater information such as climate properties is now moddable.
|
||||
* **[Modding]** Allow campaign designers to define default values for the economy settings (starting budget and multiplier).
|
||||
* **[Modding]** Campaigns can now optionally define their start time by including a time in the `recommended_start_date` field. There is not currently a way to override the start time in the UI.
|
||||
* **[Plugins]** Allow full support of the SkynetIADS plugin with all advanced features (connection nodes, power sources, command centers) if campaign supports it.
|
||||
* **[Plugins]** Added support for the CTLD script by ciribob with many possible customization options and updated the JTAC Autolase to the CTLD included script.
|
||||
* **[UI]** Added options to the loadout editor for setting properties such as HMD choice.
|
||||
* **[UI]** Added separate images for the different carrier types.
|
||||
* **[UI]** Add Accept/Reset buttons to Air Wing Configurator screen.
|
||||
* **[Campaign]** Allow campaign designers to define default values for the economy settings (starting budget and multiplier).
|
||||
* **[Plugins]** Allow full support of the SkynetIADS plugin with all advanced features (connection nodes, power sources, command centers) if campaign supports it.
|
||||
* **[Plugins]** Added support for the CTLD script by ciribob with many possible customization options and updated the JTAC Autolase to the CTLD included script.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Engine]** Fixed issue that prevented some weapon types like torpedoes from being recognized.
|
||||
* **[Flight Planning]** Fixed a miscalculation of waypoint TOTs that would require time travel.
|
||||
* **[Loadouts]** Improved the range of the F-16 CAS loadout by adding bags.
|
||||
* **[Mission Generation]** AAA ground units now spawn correctly at the frontline
|
||||
* **[Mission Generation]** Fixed SA-13 incorrectly created as SA-8 Loading Unit which will not be spawned in the generated mission.
|
||||
* **[Mission Generation]** Fixed adding additional mission types for a squadron causing error messages when the mission type is not supported by the aircraft type by default
|
||||
* **[Mission Generation]** Fixed an issue where SEAD/DEAD/BAI flights fired all missiles / bombs against a single unit in a group instead of targeting the whole group.
|
||||
* **[Mission Generation]** Fixed an issue which generated the helipads at FARPs incorrectly and placed the helicopters within each other.
|
||||
* **[Mission Generation]** Fixed an issue with SEAD missions flown by the AI when using the Skynet Plugin and anti-radiation missiles (ARM). The AI now correctly engages the SAM when it comes alive instead of diving into it.
|
||||
* **[Mission Generation]** Fixed an issue where SEAD/DEAD/BAI flights fired all missiles / bombs against a single unit in a group instead of targeting the whole group.
|
||||
* **[Mission Generation]** Fixed adding additional mission types for a squadron causing error messages when the mission type is not supported by the aircraft type by default
|
||||
* **[Mission Generation]** AAA ground units now spawn correctly at the frontline
|
||||
* **[Mission Generation]** Fixed mission scripting error caused by control points with apostrophes in their names, such as Tha'lah.
|
||||
* **[Mission Generation]** Fixed generation issue that would cause AI helicopters to get stuck after taking off from a FARP.
|
||||
* **[Modding]** Loadouts with invalid weapons (typically new DCS weapons not yet available in Liberation) will be ignored rather than causing an error.
|
||||
* **[Mission Generation]** Fixed mission scripting error caused by control points with apostrophes in their names, such as Tha'lah.
|
||||
* **[Modding]** Campaigns that used quad zones for scenery targets will no longer load. Only circular zones were ever supported, but an implementation quirk allowed them to load in a way that would misbehave. A "No white triggerzones found" message during campaign generation is the sign of a broken campaign.
|
||||
* **[Squadrons]** Helicopter squadrons can no longer be assigned to FOBs that are not FARPs.
|
||||
* **[Modding]** Loadouts with invalid weapons (typically new DCS weapons not yet available in Liberation) will be ignored rather than causing an error.
|
||||
* **[Squadrons]** Fixed issue in air wing configuration that would allow squadrons to be created with no home base if no base was available.
|
||||
* **[UI]** Fixed and issue where the liberation main exe was still running after application close.
|
||||
* **[UI]** Disable player slots for non-flyable aircraft.
|
||||
* **[Squadrons]** Helicopter squadrons can no longer be assigned to FOBs that are not FARPs.
|
||||
* **[UI]** Add vanilla theme weather and time of day icons
|
||||
|
||||
* **[UI]** Disable player slots for non-flyable aircraft.
|
||||
* **[UI]** Fixed and issue where the liberation main exe was still running after application close.
|
||||
|
||||
# 5.2.0
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DCS Liberation Client
|
||||
# DCS Retribution Client
|
||||
|
||||
This is a React app for the front-end of DCS Liberation. It is a work in
|
||||
This is a React app for the front-end of DCS Retribution. It is a work in
|
||||
progress that just barely implements the map. This is not useful for players
|
||||
yet.
|
||||
|
||||
|
||||
20488
client/package-lock.json
generated
20488
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
"axios": "^1.1.2",
|
||||
"axios": "^1.6.0",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"esri-leaflet": "^3.0.8",
|
||||
"leaflet": "^1.9.2",
|
||||
@@ -62,18 +62,26 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rtk-query/codegen-openapi": "^1.0.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^3.3.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/leaflet": "^1.8.0",
|
||||
"@types/redux-logger": "^3.0.9",
|
||||
"@types/websocket": "^1.0.5",
|
||||
"electron": "^21.1.0",
|
||||
"electron": "^22.3.25",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"generate-license-file": "^2.0.0",
|
||||
"got": "^11.8.5",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"license-checker": "^25.0.1",
|
||||
"nth-check": "^2.0.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"wait-on": "^6.0.1"
|
||||
},
|
||||
"jest": {
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(@?react-leaflet|axios)/)"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "identity-obj-proxy"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import App from "./App";
|
||||
import { store } from "./app/store";
|
||||
import { setupStore } from "./app/store";
|
||||
import { render } from "@testing-library/react";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
test("app renders", () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Provider store={setupStore()}>
|
||||
<App />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
@@ -50,14 +50,6 @@ const injectedRtkApi = api.injectEndpoints({
|
||||
url: `/debug/waypoint-geometries/hold/${queryArg.flightId}`,
|
||||
}),
|
||||
}),
|
||||
getDebugIpZones: build.query<
|
||||
GetDebugIpZonesApiResponse,
|
||||
GetDebugIpZonesApiArg
|
||||
>({
|
||||
query: (queryArg) => ({
|
||||
url: `/debug/waypoint-geometries/ip/${queryArg.flightId}`,
|
||||
}),
|
||||
}),
|
||||
getDebugJoinZones: build.query<
|
||||
GetDebugJoinZonesApiResponse,
|
||||
GetDebugJoinZonesApiArg
|
||||
@@ -245,11 +237,6 @@ export type GetDebugHoldZonesApiResponse =
|
||||
export type GetDebugHoldZonesApiArg = {
|
||||
flightId: string;
|
||||
};
|
||||
export type GetDebugIpZonesApiResponse =
|
||||
/** status 200 Successful Response */ IpZones;
|
||||
export type GetDebugIpZonesApiArg = {
|
||||
flightId: string;
|
||||
};
|
||||
export type GetDebugJoinZonesApiResponse =
|
||||
/** status 200 Successful Response */ JoinZones;
|
||||
export type GetDebugJoinZonesApiArg = {
|
||||
@@ -379,12 +366,6 @@ export type HoldZones = {
|
||||
permissibleZones: LatLng[][][];
|
||||
preferredLines: LatLng[][];
|
||||
};
|
||||
export type IpZones = {
|
||||
homeBubble: LatLng[][];
|
||||
ipBubble: LatLng[][];
|
||||
permissibleZone: LatLng[][];
|
||||
safeZones: LatLng[][][];
|
||||
};
|
||||
export type JoinZones = {
|
||||
homeBubble: LatLng[][];
|
||||
targetBubble: LatLng[][];
|
||||
@@ -426,6 +407,7 @@ export type Tgo = {
|
||||
detection_ranges: number[];
|
||||
dead: boolean;
|
||||
sidc: string;
|
||||
task?: string[];
|
||||
};
|
||||
export type SupplyRoute = {
|
||||
id: string;
|
||||
@@ -497,7 +479,6 @@ export const {
|
||||
useSetControlPointDestinationMutation,
|
||||
useClearControlPointDestinationMutation,
|
||||
useGetDebugHoldZonesQuery,
|
||||
useGetDebugIpZonesQuery,
|
||||
useGetDebugJoinZonesQuery,
|
||||
useListFlightsQuery,
|
||||
useGetFlightByIdQuery,
|
||||
|
||||
@@ -30,11 +30,6 @@ export const liberationApi = _liberationApi.enhanceEndpoints({
|
||||
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
|
||||
],
|
||||
},
|
||||
getDebugIpZones: {
|
||||
providesTags: (result, error, arg) => [
|
||||
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
|
||||
],
|
||||
},
|
||||
getDebugJoinZones: {
|
||||
providesTags: (result, error, arg) => [
|
||||
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
|
||||
|
||||
@@ -3,36 +3,48 @@ import combatReducer from "../api/combatSlice";
|
||||
import controlPointsReducer from "../api/controlPointsSlice";
|
||||
import flightsReducer from "../api/flightsSlice";
|
||||
import frontLinesReducer from "../api/frontLinesSlice";
|
||||
import iadsNetworkReducer from "../api/iadsNetworkSlice";
|
||||
import mapReducer from "../api/mapSlice";
|
||||
import navMeshReducer from "../api/navMeshSlice";
|
||||
import supplyRoutesReducer from "../api/supplyRoutesSlice";
|
||||
import tgosReducer from "../api/tgosSlice";
|
||||
import iadsNetworkReducer from "../api/iadsNetworkSlice";
|
||||
import threatZonesReducer from "../api/threatZonesSlice";
|
||||
import unculledZonesReducer from "../api/unculledZonesSlice";
|
||||
import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
|
||||
import unculledZonesReducer from "../api/unculledZonesSlice";
|
||||
import {
|
||||
Action,
|
||||
PreloadedState,
|
||||
ThunkAction,
|
||||
combineReducers,
|
||||
configureStore,
|
||||
} from "@reduxjs/toolkit";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
combat: combatReducer,
|
||||
controlPoints: controlPointsReducer,
|
||||
flights: flightsReducer,
|
||||
frontLines: frontLinesReducer,
|
||||
map: mapReducer,
|
||||
navmeshes: navMeshReducer,
|
||||
supplyRoutes: supplyRoutesReducer,
|
||||
iadsNetwork: iadsNetworkReducer,
|
||||
tgos: tgosReducer,
|
||||
threatZones: threatZonesReducer,
|
||||
[baseApi.reducerPath]: baseApi.reducer,
|
||||
unculledZones: unculledZonesReducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(baseApi.middleware),
|
||||
const rootReducer = combineReducers({
|
||||
combat: combatReducer,
|
||||
controlPoints: controlPointsReducer,
|
||||
flights: flightsReducer,
|
||||
frontLines: frontLinesReducer,
|
||||
map: mapReducer,
|
||||
navmeshes: navMeshReducer,
|
||||
supplyRoutes: supplyRoutesReducer,
|
||||
iadsNetwork: iadsNetworkReducer,
|
||||
tgos: tgosReducer,
|
||||
threatZones: threatZonesReducer,
|
||||
[baseApi.reducerPath]: baseApi.reducer,
|
||||
unculledZones: unculledZonesReducer,
|
||||
});
|
||||
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export function setupStore(preloadedState?: PreloadedState<RootState>) {
|
||||
return configureStore({
|
||||
reducer: rootReducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(baseApi.middleware),
|
||||
preloadedState: preloadedState,
|
||||
});
|
||||
}
|
||||
|
||||
export type AppStore = ReturnType<typeof setupStore>;
|
||||
export type AppDispatch = AppStore["dispatch"];
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
export type AppThunk<ReturnType = void> = ThunkAction<
|
||||
ReturnType,
|
||||
RootState,
|
||||
|
||||
53
client/src/components/aircraft/Aircraft.test.tsx
Normal file
53
client/src/components/aircraft/Aircraft.test.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import Aircraft from "./Aircraft";
|
||||
import { render } from "@testing-library/react";
|
||||
import { Icon } from "leaflet";
|
||||
|
||||
const mockMarker = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
Marker: (props: any) => {
|
||||
mockMarker(props);
|
||||
},
|
||||
}));
|
||||
|
||||
test("grounded aircraft do not render", async () => {
|
||||
const { container } = render(
|
||||
<Aircraft
|
||||
flight={{
|
||||
id: "",
|
||||
blue: true,
|
||||
position: undefined,
|
||||
sidc: "",
|
||||
waypoints: [],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
test("in-flight aircraft render", async () => {
|
||||
render(
|
||||
<Aircraft
|
||||
flight={{
|
||||
id: "",
|
||||
blue: true,
|
||||
position: {
|
||||
lat: 10,
|
||||
lng: 20,
|
||||
},
|
||||
sidc: "foobar",
|
||||
waypoints: [],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(mockMarker).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
position: {
|
||||
lat: 10,
|
||||
lng: 20,
|
||||
},
|
||||
icon: expect.any(Icon),
|
||||
})
|
||||
);
|
||||
});
|
||||
53
client/src/components/aircraftlayer/AircraftLayer.test.tsx
Normal file
53
client/src/components/aircraftlayer/AircraftLayer.test.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import AircraftLayer from "./AircraftLayer";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockLayerGroup = jest.fn();
|
||||
const mockMarker = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Marker: (props: any) => {
|
||||
mockMarker(props);
|
||||
},
|
||||
}));
|
||||
|
||||
test("layer is empty by default", async () => {
|
||||
renderWithProviders(<AircraftLayer />);
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockMarker).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("layer has aircraft if non-empty", async () => {
|
||||
renderWithProviders(<AircraftLayer />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
id: "bar",
|
||||
blue: false,
|
||||
sidc: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
selected: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockMarker).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import AirDefenseRangeLayer, { colorFor } from "./AirDefenseRangeLayer";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockLayerGroup = jest.fn();
|
||||
const mockCircle = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Circle: (props: any) => {
|
||||
mockCircle(props);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("colorFor", () => {
|
||||
it("has a unique color for each configuration", () => {
|
||||
const params = [
|
||||
[false, false],
|
||||
[false, true],
|
||||
[true, false],
|
||||
[true, true],
|
||||
];
|
||||
var colors = new Set<string>();
|
||||
for (const [blue, detection] of params) {
|
||||
colors.add(colorFor(blue, detection));
|
||||
}
|
||||
expect(colors.size).toEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AirDefenseRangeLayer", () => {
|
||||
it("draws nothing when there are no TGOs", () => {
|
||||
renderWithProviders(<AirDefenseRangeLayer blue={true} />);
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockCircle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not draw wrong range types", () => {
|
||||
renderWithProviders(<AirDefenseRangeLayer blue={true} />, {
|
||||
preloadedState: {
|
||||
tgos: {
|
||||
tgos: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
name: "Foo",
|
||||
control_point_name: "Bar",
|
||||
category: "AA",
|
||||
blue: false,
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
units: [],
|
||||
threat_ranges: [],
|
||||
detection_ranges: [20],
|
||||
dead: false,
|
||||
sidc: "",
|
||||
task: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockCircle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("draws threat ranges", () => {
|
||||
renderWithProviders(<AirDefenseRangeLayer blue={true} />, {
|
||||
preloadedState: {
|
||||
tgos: {
|
||||
tgos: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
name: "Foo",
|
||||
control_point_name: "Bar",
|
||||
category: "AA",
|
||||
blue: true,
|
||||
position: {
|
||||
lat: 10,
|
||||
lng: 20,
|
||||
},
|
||||
units: [],
|
||||
threat_ranges: [10],
|
||||
detection_ranges: [20],
|
||||
dead: false,
|
||||
sidc: "",
|
||||
task: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockCircle).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
center: {
|
||||
lat: 10,
|
||||
lng: 20,
|
||||
},
|
||||
radius: 10,
|
||||
color: colorFor(true, false),
|
||||
interactive: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("draws detection ranges", () => {
|
||||
renderWithProviders(<AirDefenseRangeLayer blue={true} detection />, {
|
||||
preloadedState: {
|
||||
tgos: {
|
||||
tgos: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
name: "Foo",
|
||||
control_point_name: "Bar",
|
||||
category: "AA",
|
||||
blue: true,
|
||||
position: {
|
||||
lat: 10,
|
||||
lng: 20,
|
||||
},
|
||||
units: [],
|
||||
threat_ranges: [10],
|
||||
detection_ranges: [20],
|
||||
dead: false,
|
||||
sidc: "",
|
||||
task: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockCircle).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
center: {
|
||||
lat: 10,
|
||||
lng: 20,
|
||||
},
|
||||
radius: 20,
|
||||
color: colorFor(true, true),
|
||||
interactive: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ interface TgoRangeCirclesProps {
|
||||
detection?: boolean;
|
||||
}
|
||||
|
||||
function colorFor(blue: boolean, detection: boolean) {
|
||||
export function colorFor(blue: boolean, detection: boolean) {
|
||||
if (blue) {
|
||||
return detection ? "#bb89ff" : "#0084ff";
|
||||
}
|
||||
|
||||
132
client/src/components/combat/Combat.test.tsx
Normal file
132
client/src/components/combat/Combat.test.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import Combat from "./Combat";
|
||||
import { LatLng } from "leaflet";
|
||||
|
||||
const mockPolyline = jest.fn();
|
||||
const mockPolygon = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
Polyline: (props: any) => {
|
||||
mockPolyline(props);
|
||||
},
|
||||
Polygon: (props: any) => {
|
||||
mockPolygon(props);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("Combat", () => {
|
||||
describe("footprint", () => {
|
||||
it("is not interactive", () => {
|
||||
renderWithProviders(
|
||||
<Combat
|
||||
combat={{
|
||||
id: "foo",
|
||||
flight_position: null,
|
||||
target_positions: null,
|
||||
footprint: [[new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)]],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolygon).toBeCalledWith(
|
||||
expect.objectContaining({ interactive: false })
|
||||
);
|
||||
});
|
||||
|
||||
// Fails because we don't handle multi-poly combat footprints correctly.
|
||||
it.skip("renders single polygons", () => {
|
||||
const boundary = [new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)];
|
||||
renderWithProviders(
|
||||
<Combat
|
||||
combat={{
|
||||
id: "foo",
|
||||
flight_position: null,
|
||||
target_positions: null,
|
||||
footprint: [boundary],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolygon).toBeCalledWith(
|
||||
expect.objectContaining({ positions: boundary })
|
||||
);
|
||||
});
|
||||
|
||||
// Fails because we don't handle multi-poly combat footprints correctly.
|
||||
it.skip("renders multiple polygons", () => {
|
||||
const boundary = [new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)];
|
||||
renderWithProviders(
|
||||
<Combat
|
||||
combat={{
|
||||
id: "foo",
|
||||
flight_position: null,
|
||||
target_positions: null,
|
||||
footprint: [boundary, boundary],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolygon).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lines", () => {
|
||||
it("is not interactive", () => {
|
||||
renderWithProviders(
|
||||
<Combat
|
||||
combat={{
|
||||
id: "foo",
|
||||
flight_position: new LatLng(0, 0),
|
||||
target_positions: [new LatLng(1, 0)],
|
||||
footprint: null,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolyline).toBeCalledWith(
|
||||
expect.objectContaining({ interactive: false })
|
||||
);
|
||||
});
|
||||
|
||||
it("renders single line", () => {
|
||||
renderWithProviders(
|
||||
<Combat
|
||||
combat={{
|
||||
id: "foo",
|
||||
flight_position: new LatLng(0, 0),
|
||||
target_positions: [new LatLng(0, 1)],
|
||||
footprint: null,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolyline).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
positions: [new LatLng(0, 0), new LatLng(0, 1)],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("renders multiple lines", () => {
|
||||
renderWithProviders(
|
||||
<Combat
|
||||
combat={{
|
||||
id: "foo",
|
||||
flight_position: new LatLng(0, 0),
|
||||
target_positions: [new LatLng(0, 1), new LatLng(1, 0)],
|
||||
footprint: null,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolyline).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("renders nothing if no footprint or targets", () => {
|
||||
const { container } = renderWithProviders(
|
||||
<Combat
|
||||
combat={{
|
||||
id: "foo",
|
||||
flight_position: new LatLng(0, 0),
|
||||
target_positions: null,
|
||||
footprint: null,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
48
client/src/components/combatlayer/CombatLayer.test.tsx
Normal file
48
client/src/components/combatlayer/CombatLayer.test.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import CombatLayer from "./CombatLayer";
|
||||
import { LatLng } from "leaflet";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockPolyline = jest.fn();
|
||||
const mockLayerGroup = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Polyline: (props: any) => {
|
||||
mockPolyline(props);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("CombatLayer", () => {
|
||||
it("renders each combat", () => {
|
||||
renderWithProviders(<CombatLayer />, {
|
||||
preloadedState: {
|
||||
combat: {
|
||||
combat: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
flight_position: new LatLng(0, 0),
|
||||
target_positions: [new LatLng(0, 1)],
|
||||
footprint: null,
|
||||
},
|
||||
bar: {
|
||||
id: "foo",
|
||||
flight_position: new LatLng(0, 0),
|
||||
target_positions: [new LatLng(0, 1)],
|
||||
footprint: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it("renders LayerGroup but no contents if no combat", () => {
|
||||
renderWithProviders(<CombatLayer />);
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
expect(mockPolyline).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import ControlPointsLayer from "./ControlPointsLayer";
|
||||
import { LatLng } from "leaflet";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockMarker = jest.fn();
|
||||
const mockLayerGroup = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Marker: (props: any) => {
|
||||
mockMarker(props);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("ControlPointsLayer", () => {
|
||||
it("renders each control point", () => {
|
||||
renderWithProviders(<ControlPointsLayer />, {
|
||||
preloadedState: {
|
||||
controlPoints: {
|
||||
controlPoints: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
name: "Foo",
|
||||
blue: true,
|
||||
position: new LatLng(0, 0),
|
||||
mobile: false,
|
||||
sidc: "",
|
||||
},
|
||||
bar: {
|
||||
id: "bar",
|
||||
name: "Bar",
|
||||
blue: false,
|
||||
position: new LatLng(1, 0),
|
||||
mobile: false,
|
||||
sidc: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockMarker).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it("renders LayerGroup but no contents if no combat", () => {
|
||||
renderWithProviders(<ControlPointsLayer />);
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
expect(mockMarker).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import CullingExclusionZones from "./CullingExclusionZones";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockCircle = jest.fn();
|
||||
const mockLayerGroup = jest.fn();
|
||||
const mockLayerControlOverlay = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
LayersControl: {
|
||||
Overlay: (props: PropsWithChildren<any>) => {
|
||||
mockLayerControlOverlay(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
},
|
||||
Circle: (props: any) => {
|
||||
mockCircle(props);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("CullingExclusionZones", () => {
|
||||
it("is empty there are no exclusion zones", () => {
|
||||
renderWithProviders(<CullingExclusionZones />);
|
||||
expect(mockCircle).not.toHaveBeenCalled();
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockLayerControlOverlay).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe("zone circles", () => {
|
||||
it("are drawn in the correct locations", () => {
|
||||
renderWithProviders(<CullingExclusionZones />, {
|
||||
preloadedState: {
|
||||
unculledZones: {
|
||||
zones: [
|
||||
{
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
radius: 10,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
radius: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockCircle).toHaveBeenCalledTimes(2);
|
||||
expect(mockCircle).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
center: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
radius: 10,
|
||||
})
|
||||
);
|
||||
expect(mockCircle).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
center: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
radius: 2,
|
||||
})
|
||||
);
|
||||
});
|
||||
it("are not interactive", () => {});
|
||||
});
|
||||
});
|
||||
@@ -30,18 +30,10 @@ const CullingExclusionCircles = (props: CullingExclusionCirclesProps) => {
|
||||
|
||||
export default function CullingExclusionZones() {
|
||||
const data = useAppSelector(selectUnculledZones).zones;
|
||||
var cez = <></>;
|
||||
|
||||
if (!data) {
|
||||
console.log("Empty response when loading culling exclusion zones");
|
||||
} else {
|
||||
cez = (
|
||||
<CullingExclusionCircles zones={data}></CullingExclusionCircles>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<LayersControl.Overlay name="Culling exclusion zones">
|
||||
{cez}
|
||||
<CullingExclusionCircles zones={data}></CullingExclusionCircles>
|
||||
</LayersControl.Overlay>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Flight } from "../../api/liberationApi";
|
||||
import { useGetCommitBoundaryForFlightQuery } from "../../api/liberationApi";
|
||||
import WaypointMarker from "../waypointmarker";
|
||||
import { ReactElement } from "react";
|
||||
import { Polyline as LPolyline } from "leaflet";
|
||||
import { ReactElement, useEffect, useRef } from "react";
|
||||
import { Polyline } from "react-leaflet";
|
||||
|
||||
const BLUE_PATH = "#0084ff";
|
||||
@@ -27,16 +28,44 @@ const pathColor = (props: FlightPlanProps) => {
|
||||
function FlightPlanPath(props: FlightPlanProps) {
|
||||
const color = pathColor(props);
|
||||
const waypoints = props.flight.waypoints;
|
||||
|
||||
const polylineRef = useRef<LPolyline | null>(null);
|
||||
|
||||
// Flight paths should be drawn under everything else. There seems to be an
|
||||
// issue where `interactive: false` doesn't do as its told (there's nuance,
|
||||
// see the bug for details). It looks better if we draw the other elements on
|
||||
// top of the flight plans anyway, so just push the flight plan to the back.
|
||||
//
|
||||
// https://github.com/dcs-liberation/dcs_liberation/issues/3295
|
||||
//
|
||||
// It's not possible to z-index a polyline (and leaflet says it never will be,
|
||||
// because this is a limitation of SVG, not leaflet:
|
||||
// https://github.com/Leaflet/Leaflet/issues/185), so we need to use
|
||||
// bringToBack() to push the flight paths to the back of the drawing once
|
||||
// they've been added to the map. They'll still draw on top of the map, but
|
||||
// behind everything than was added before them. Anything added after always
|
||||
// goes on top.
|
||||
useEffect(() => {
|
||||
if (props.selected) {
|
||||
polylineRef.current?.bringToFront();
|
||||
}
|
||||
else {
|
||||
polylineRef.current?.bringToBack();
|
||||
}
|
||||
});
|
||||
|
||||
if (waypoints == null) {
|
||||
return <></>;
|
||||
}
|
||||
const points = waypoints
|
||||
.filter((waypoint) => waypoint.include_in_path)
|
||||
.map((waypoint) => waypoint.position);
|
||||
|
||||
return (
|
||||
<Polyline
|
||||
positions={points}
|
||||
pathOptions={{ color: color, interactive: false }}
|
||||
ref={polylineRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
409
client/src/components/flightplanslayer/FlightPlansLayer.test.tsx
Normal file
409
client/src/components/flightplanslayer/FlightPlansLayer.test.tsx
Normal file
@@ -0,0 +1,409 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import FlightPlansLayer from "./FlightPlansLayer";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockPolyline = jest.fn();
|
||||
const mockLayerGroup = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Polyline: (props: any) => {
|
||||
mockPolyline(props);
|
||||
},
|
||||
}));
|
||||
|
||||
// The waypoints in test data below should all use `should_make: false`. Markers
|
||||
// need useMap() to check the zoom level to decide if they should be drawn or
|
||||
// not, and we don't have good options here for mocking that behavior.
|
||||
describe("FlightPlansLayer", () => {
|
||||
describe("unselected flights", () => {
|
||||
it("are drawn", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={true} />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
bar: {
|
||||
id: "bar",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
selected: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// For some reason passing ref to PolyLine causes it and its group to be
|
||||
// redrawn, so these numbers don't match what you'd expect from the test.
|
||||
// It probably needs to be rewritten without mocks.
|
||||
expect(mockPolyline).toHaveBeenCalledTimes(3);
|
||||
expect(mockLayerGroup).toBeCalledTimes(2);
|
||||
});
|
||||
it("are not drawn if wrong coalition", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={true} />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
bar: {
|
||||
id: "bar",
|
||||
blue: false,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
selected: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).toHaveBeenCalledTimes(1);
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
it("are not drawn when only selected flights are to be drawn", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={true} selectedOnly />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
selected: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).not.toHaveBeenCalled();
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
describe("selected flights", () => {
|
||||
it("are drawn", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={true} />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
bar: {
|
||||
id: "bar",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
selected: "foo",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).toHaveBeenCalledTimes(2);
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
it("are not drawn twice", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={true} />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
selected: "foo",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).toHaveBeenCalledTimes(1);
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
it("are not drawn if red", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={false} selectedOnly />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
blue: false,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
selected: "foo",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).not.toHaveBeenCalled();
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
it("are not drawn if there are no flights", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={true} />);
|
||||
expect(mockPolyline).not.toHaveBeenCalled();
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
32
client/src/components/frontline/FrontLine.test.tsx
Normal file
32
client/src/components/frontline/FrontLine.test.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import FrontLine from "./FrontLine";
|
||||
import { PolylineProps } from "react-leaflet";
|
||||
|
||||
const mockPolyline = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
Polyline: (props: PolylineProps) => {
|
||||
mockPolyline(props);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("FrontLine", () => {
|
||||
it("is drawn in the correct location", () => {
|
||||
const extents = [
|
||||
{ lat: 0, lng: 0 },
|
||||
{ lat: 1, lng: 0 },
|
||||
];
|
||||
renderWithProviders(
|
||||
<FrontLine
|
||||
front={{
|
||||
id: "",
|
||||
extents: extents,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolyline).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
positions: extents,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import FrontLinesLayer from "./FrontLinesLayer";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockPolyline = jest.fn();
|
||||
const mockLayerGroup = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Polyline: (props: any) => {
|
||||
mockPolyline(props);
|
||||
},
|
||||
}));
|
||||
|
||||
// The waypoints in test data below should all use `should_make: false`. Markers
|
||||
// need useMap() to check the zoom level to decide if they should be drawn or
|
||||
// not, and we don't have good options here for mocking that behavior.
|
||||
describe("FrontLinesLayer", () => {
|
||||
it("draws nothing when there are no front lines", () => {
|
||||
renderWithProviders(<FrontLinesLayer />);
|
||||
expect(mockPolyline).not.toHaveBeenCalled();
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("draws front lines", () => {
|
||||
const extents = [
|
||||
{ lat: 0, lng: 0 },
|
||||
{ lat: 1, lng: 1 },
|
||||
];
|
||||
renderWithProviders(<FrontLinesLayer />, {
|
||||
preloadedState: {
|
||||
frontLines: {
|
||||
fronts: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
extents: extents,
|
||||
},
|
||||
bar: {
|
||||
id: "bar",
|
||||
extents: extents,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).toHaveBeenCalledTimes(2);
|
||||
expect(mockPolyline).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
positions: extents,
|
||||
})
|
||||
);
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { useAppSelector } from "../../app/hooks";
|
||||
import FrontLine from "../frontline";
|
||||
import { LayerGroup } from "react-leaflet";
|
||||
|
||||
export default function SupplyRoutesLayer() {
|
||||
export default function FrontLinesLayer() {
|
||||
const fronts = useAppSelector(selectFrontLines).fronts;
|
||||
return (
|
||||
<LayerGroup>
|
||||
|
||||
@@ -53,6 +53,18 @@ export default function LiberationMap() {
|
||||
<LayersControl.Overlay name="Air defenses" checked>
|
||||
<TgosLayer categories={["aa"]} />
|
||||
</LayersControl.Overlay>
|
||||
<LayersControl.Overlay name="LORAD" >
|
||||
<TgosLayer categories={["aa"]} task={"LORAD"} />
|
||||
</LayersControl.Overlay>
|
||||
<LayersControl.Overlay name="MERAD" >
|
||||
<TgosLayer categories={["aa"]} task={"MERAD"} />
|
||||
</LayersControl.Overlay>
|
||||
<LayersControl.Overlay name="SHORAD" >
|
||||
<TgosLayer categories={["aa"]} task={"SHORAD"} />
|
||||
</LayersControl.Overlay>
|
||||
<LayersControl.Overlay name="AAA" >
|
||||
<TgosLayer categories={["aa"]} task={"AAA"} />
|
||||
</LayersControl.Overlay>
|
||||
<LayersControl.Overlay name="Factories" checked>
|
||||
<TgosLayer categories={["factory"]} />
|
||||
</LayersControl.Overlay>
|
||||
|
||||
125
client/src/components/navmesh/NavMeshLayer.test.tsx
Normal file
125
client/src/components/navmesh/NavMeshLayer.test.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import NavMeshLayer from "./NavMeshLayer";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockPolygon = jest.fn();
|
||||
const mockLayerGroup = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Polygon: (props: any) => {
|
||||
mockPolygon(props);
|
||||
},
|
||||
}));
|
||||
|
||||
// The waypoints in test data below should all use `should_make: false`. Markers
|
||||
// need useMap() to check the zoom level to decide if they should be drawn or
|
||||
// not, and we don't have good options here for mocking that behavior.
|
||||
describe("NavMeshLayer", () => {
|
||||
it("draws blue meshes", () => {
|
||||
const poly1 = [
|
||||
[
|
||||
{ lat: -1, lng: 0 },
|
||||
{ lat: 0, lng: 1 },
|
||||
{ lat: 1, lng: 0 },
|
||||
],
|
||||
];
|
||||
const poly2 = [
|
||||
[
|
||||
{ lat: -1, lng: 0 },
|
||||
{ lat: 0, lng: -1 },
|
||||
{ lat: 1, lng: 0 },
|
||||
],
|
||||
];
|
||||
renderWithProviders(<NavMeshLayer blue={true} />, {
|
||||
preloadedState: {
|
||||
navmeshes: {
|
||||
blue: [
|
||||
{
|
||||
poly: poly1,
|
||||
threatened: false,
|
||||
},
|
||||
{
|
||||
poly: poly2,
|
||||
threatened: true,
|
||||
},
|
||||
],
|
||||
red: [
|
||||
{
|
||||
poly: [
|
||||
[
|
||||
{ lat: -1, lng: 0 },
|
||||
{ lat: 0, lng: 2 },
|
||||
{ lat: 1, lng: 0 },
|
||||
],
|
||||
],
|
||||
threatened: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolygon).toHaveBeenCalledTimes(2);
|
||||
expect(mockPolygon).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
fillColor: "#00ff00",
|
||||
positions: poly1,
|
||||
interactive: false,
|
||||
})
|
||||
);
|
||||
expect(mockPolygon).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
fillColor: "#ff0000",
|
||||
positions: poly2,
|
||||
interactive: false,
|
||||
})
|
||||
);
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("draws red navmesh", () => {
|
||||
renderWithProviders(<NavMeshLayer blue={false} />, {
|
||||
preloadedState: {
|
||||
navmeshes: {
|
||||
blue: [
|
||||
{
|
||||
poly: [
|
||||
[
|
||||
{ lat: -1, lng: 0 },
|
||||
{ lat: 0, lng: 1 },
|
||||
{ lat: 1, lng: 0 },
|
||||
],
|
||||
],
|
||||
threatened: false,
|
||||
},
|
||||
{
|
||||
poly: [
|
||||
[
|
||||
{ lat: -1, lng: 0 },
|
||||
{ lat: 0, lng: -1 },
|
||||
{ lat: 1, lng: 0 },
|
||||
],
|
||||
],
|
||||
threatened: true,
|
||||
},
|
||||
],
|
||||
red: [
|
||||
{
|
||||
poly: [
|
||||
[
|
||||
{ lat: -1, lng: 0 },
|
||||
{ lat: 0, lng: 2 },
|
||||
{ lat: 1, lng: 0 },
|
||||
],
|
||||
],
|
||||
threatened: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolygon).toHaveBeenCalledTimes(1);
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -6,14 +6,21 @@ import { LayerGroup } from "react-leaflet";
|
||||
interface TgosLayerProps {
|
||||
categories?: string[];
|
||||
exclude?: true;
|
||||
task?: string;
|
||||
}
|
||||
|
||||
export default function TgosLayer(props: TgosLayerProps) {
|
||||
const allTgos = Object.values(useAppSelector(selectTgos).tgos);
|
||||
const categoryFilter = props.categories ?? [];
|
||||
const taskFilter = props.task ?? "";
|
||||
const tgos = allTgos.filter(
|
||||
(tgo) => categoryFilter.includes(tgo.category) === !(props.exclude ?? false)
|
||||
);
|
||||
(tgo) => {
|
||||
if (taskFilter && tgo.task){
|
||||
return taskFilter === tgo.task[0]
|
||||
}
|
||||
return categoryFilter.includes(tgo.category) === !(props.exclude ?? false)
|
||||
}
|
||||
);
|
||||
return (
|
||||
<LayerGroup>
|
||||
{tgos.map((tgo) => {
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { useGetDebugIpZonesQuery } from "../../api/liberationApi";
|
||||
import { LayerGroup, Polygon } from "react-leaflet";
|
||||
|
||||
interface IpZonesProps {
|
||||
flightId: string;
|
||||
}
|
||||
|
||||
function IpZones(props: IpZonesProps) {
|
||||
const { data, error, isLoading } = useGetDebugIpZonesQuery({
|
||||
flightId: props.flightId,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error("Error while loading waypoint IP zone info", error);
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
console.log("Waypoint IP zone returned empty response");
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Polygon
|
||||
positions={data.homeBubble}
|
||||
color="#ffff00"
|
||||
fillOpacity={0.1}
|
||||
interactive={false}
|
||||
/>
|
||||
<Polygon
|
||||
positions={data.ipBubble}
|
||||
color="#bb89ff"
|
||||
fillOpacity={0.1}
|
||||
interactive={false}
|
||||
/>
|
||||
<Polygon
|
||||
positions={data.permissibleZone}
|
||||
color="#ffffff"
|
||||
fillOpacity={0.1}
|
||||
interactive={false}
|
||||
/>
|
||||
|
||||
{data.safeZones.map((zone, idx) => {
|
||||
return (
|
||||
<Polygon
|
||||
key={idx}
|
||||
positions={zone}
|
||||
color="#80BA80"
|
||||
fillOpacity={0.1}
|
||||
interactive={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface IpZonesLayerProps {
|
||||
flightId: string | null;
|
||||
}
|
||||
|
||||
export function IpZonesLayer(props: IpZonesLayerProps) {
|
||||
return (
|
||||
<LayerGroup>
|
||||
{props.flightId ? <IpZones flightId={props.flightId} /> : <></>}
|
||||
</LayerGroup>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { selectSelectedFlightId } from "../../api/flightsSlice";
|
||||
import { useAppSelector } from "../../app/hooks";
|
||||
import { HoldZonesLayer } from "./HoldZones";
|
||||
import { IpZonesLayer } from "./IpZones";
|
||||
import { JoinZonesLayer } from "./JoinZones";
|
||||
import { LayersControl } from "react-leaflet";
|
||||
|
||||
@@ -16,9 +15,6 @@ export function WaypointDebugZonesControls() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<LayersControl.Overlay name="IP zones">
|
||||
<IpZonesLayer flightId={selectedFlightId} />
|
||||
</LayersControl.Overlay>
|
||||
<LayersControl.Overlay name="Join zones">
|
||||
<JoinZonesLayer flightId={selectedFlightId} />
|
||||
</LayersControl.Overlay>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import App from "./App";
|
||||
import { store } from "./app/store";
|
||||
import { setupStore } from "./app/store";
|
||||
import { SocketProvider } from "./components/socketprovider/socketprovider";
|
||||
import "./index.css";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
@@ -12,7 +12,7 @@ const root = ReactDOM.createRoot(
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<Provider store={setupStore()}>
|
||||
<SocketProvider>
|
||||
<App />
|
||||
</SocketProvider>
|
||||
|
||||
30
client/src/testutils/index.tsx
Normal file
30
client/src/testutils/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
// https://redux.js.org/usage/writing-tests
|
||||
import { setupStore } from "../app/store";
|
||||
import type { AppStore, RootState } from "../app/store";
|
||||
import type { PreloadedState } from "@reduxjs/toolkit";
|
||||
import { render } from "@testing-library/react";
|
||||
import type { RenderOptions } from "@testing-library/react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
// This type interface extends the default options for render from RTL, as well
|
||||
// as allows the user to specify other things such as initialState, store.
|
||||
interface ExtendedRenderOptions extends Omit<RenderOptions, "queries"> {
|
||||
preloadedState?: PreloadedState<RootState>;
|
||||
store?: AppStore;
|
||||
}
|
||||
|
||||
export function renderWithProviders(
|
||||
ui: React.ReactElement,
|
||||
{
|
||||
preloadedState = {},
|
||||
// Automatically create a store instance if no store was passed in
|
||||
store = setupStore(preloadedState),
|
||||
...renderOptions
|
||||
}: ExtendedRenderOptions = {}
|
||||
) {
|
||||
function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element {
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
}
|
||||
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
|
||||
}
|
||||
8
codecov.yaml
Normal file
8
codecov.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
coverage:
|
||||
status:
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
34
docs/conf.py
Normal file
34
docs/conf.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# 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"
|
||||
copyright = "2023, DCS Liberation Team"
|
||||
author = "DCS Liberation Team"
|
||||
release = "7.0.0"
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = [
|
||||
"myst_parser",
|
||||
"sphinx_rtd_theme",
|
||||
"sphinx.ext.autosectionlabel",
|
||||
"sphinx.ext.todo",
|
||||
]
|
||||
|
||||
templates_path = ["_templates"]
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_static_path = ["_static"]
|
||||
|
||||
todo_include_todos = True
|
||||
8
docs/dev/design/index.rst
Normal file
8
docs/dev/design/index.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
Design docs
|
||||
===========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
turnless.md
|
||||
8
docs/dev/index.rst
Normal file
8
docs/dev/index.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
Developer documentation
|
||||
=======================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
design/index.rst
|
||||
6
docs/game/index.rst
Normal file
6
docs/game/index.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
Manual
|
||||
======
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
16
docs/index.rst
Normal file
16
docs/index.rst
Normal file
@@ -0,0 +1,16 @@
|
||||
DCS Liberation
|
||||
==============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
game/index.rst
|
||||
modding/index.rst
|
||||
dev/index.rst
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`search`
|
||||
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@@ -0,0 +1,35 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.https://www.sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
10
docs/modding/index.rst
Normal file
10
docs/modding/index.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
Modding guide
|
||||
=============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
fuel-consumption-measurement.md
|
||||
layouts.rst
|
||||
weather.rst
|
||||
397
docs/modding/layouts.rst
Normal file
397
docs/modding/layouts.rst
Normal file
@@ -0,0 +1,397 @@
|
||||
The Layout System
|
||||
=================
|
||||
|
||||
.. note::
|
||||
The documentation of the layout system is still WIP and not
|
||||
complete as the development of this feature involves a major refactoring
|
||||
of the base code. Therefore this documentation is currently used for
|
||||
development purpose primarily. The documentation will be updated soon.
|
||||
Any help in updating this wiki page is appreciated!
|
||||
|
||||
The Layout System is a new way of defining how ground objects like SAM
|
||||
Sites or other Vehicle / Ship Groups will be generated (which type of
|
||||
units, how many units, alignment and orientation). It is a complete
|
||||
rework of the previous generator-based logic which was written in python
|
||||
code. The new system allows to define layouts with easy to write yaml
|
||||
code and the use of the DCS Mission Editor for easier placement of the
|
||||
units. The layout system also introduced a new logical grouping of Units
|
||||
and layouts for them, the Armed Forces, which will allow major
|
||||
improvements to the Ground Warfare in upcoming features.
|
||||
|
||||
**Armed Forces**
|
||||
|
||||
The Armed Forces is a new system introduced with the layout system which will
|
||||
allow to identitfy and group possible units from the faction together with
|
||||
available layouts for these groups. It is comparable to the AirWing and Squadron
|
||||
implementation but just for Ground and Naval Forces. All possible Force Groups
|
||||
(grouping of 1 or more units and and the available layouts for them) will be
|
||||
generated during campaign initialization and will be used later by many
|
||||
different systems. A Force Group can also include static objects which was not
|
||||
possible before the introduction of the layout system. It is also possible to
|
||||
define presets of these Force Groups within the faction file which is handy for
|
||||
more complex groups like a SA-10 Battery or similar. Example: `SA-10.yaml`_.
|
||||
which includes all the units like SR, TR, LN and has the layout of a
|
||||
`S-300_Site.yaml`_.
|
||||
|
||||
.. _SA-10.yaml: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/groups/SA-10.yaml
|
||||
.. _S-300_Site.yaml: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/layouts/anti_air/S-300_Site.yaml
|
||||
|
||||
**The Layout System**
|
||||
|
||||
In the previous system the generator which created the ground object was written
|
||||
in python which made modifications and reusability very complicated. To allow
|
||||
easier handling of the layouts and decoupling of alignment of units and the
|
||||
actual unit type (for example Ural-375) the layout system was introduced.
|
||||
Previously we had a generator for every different SAM Site, now we can just
|
||||
reuse the alignemnt (e.g. 6 Launchers in a circle alignment) for multiple SAM
|
||||
Systems and introduce more variety.
|
||||
|
||||
This new System allows Users and Designers to easily create or modify
|
||||
layouts as the new alginment and orientation of units is defined with
|
||||
the DCS Mission editor. An additional .yaml file allows the
|
||||
configuration of the layout with settings like allow unit types or
|
||||
random amounts of units. In total the new system reduces the complexity
|
||||
and allows to precisely align / orient units as needed and create
|
||||
realistic looking ground units.
|
||||
|
||||
As the whole ground unit generation and handling was reworked it is now
|
||||
also possible to add static units to a ground object, so even
|
||||
Fortifcation or similar can be added to templates in the future.
|
||||
|
||||
General Concept
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. figure:: images/layouts.png
|
||||
:alt: Overview
|
||||
|
||||
Overview
|
||||
|
||||
All possible Force Groups will be generated during campaign
|
||||
initialization by checking all possible units for the specific faction
|
||||
and all available layouts. The code will automatically match general
|
||||
layouts with available units. It is also possible to define preset
|
||||
groups within the faction files which group many units and the prefered
|
||||
layouts for the group. This is especially handy for unique layouts which
|
||||
are not defined as ``global``. For example complex sam sites like the
|
||||
S-300 or Patriot which have very specific alignment of the different
|
||||
units.
|
||||
|
||||
Layouts will be matched to units based on the special definition given
|
||||
in the corresponding yaml file. For example a layout which is defined as
|
||||
global and allows the unit_class SHORAD will automatically be used for
|
||||
all available SHORAD units which are defined in the faction file.
|
||||
|
||||
.. todo:: Describe the optional flag.
|
||||
|
||||
All these generated ForceGroups will be managed by the ArmedForces class
|
||||
of the specific coalition. This class will be used by other parts of the
|
||||
system like the start_generator or the BuyMenu. The ArmedForces class
|
||||
will then generate the TheaterGroundObject which will be used by
|
||||
liberation.
|
||||
|
||||
Example for a customized Ground Object Buy Menu which makes use of
|
||||
Templates and UnitGroups:
|
||||
|
||||
.. figure:: images/ground_object_buy_menu.png
|
||||
:alt: Ground object buy menu
|
||||
|
||||
Ground object buy menu
|
||||
|
||||
How to modify or add layouts
|
||||
----------------------------
|
||||
|
||||
.. warning::
|
||||
Whenever changes were made to layouts they have to be re-imported into
|
||||
Liberation. See :ref:`Import Layouts into Liberation`.
|
||||
|
||||
A layout consists of two special files:
|
||||
|
||||
- layout.miz which defines the actual positioning and alignment of the
|
||||
groups / units
|
||||
- layout.yaml which defines the necessary information like amount of
|
||||
units, possible types or classes.
|
||||
|
||||
To add a new template a new yaml has to be created as every yaml can
|
||||
only define exact one template. Best practice is to copy paste an
|
||||
existing template which is similar to the one to be created as starting
|
||||
point. The next step is to create a new .miz file and align Units and
|
||||
statics to the wishes. Even if existing ones can be reused, best
|
||||
practice is to always create a fresh one to prevent side effects. The
|
||||
most important part is to use a new Group for every different Unit Type.
|
||||
It is not possible to mix Unit Types in one group within a template. For
|
||||
example it is not possible to have a logistic truck and a AAA in the
|
||||
same group. The miz file will only be used to get the exact position and
|
||||
orientation of the units, therefore it is irrelevant which type of unit
|
||||
will be used. The unit type will be later defined inside the yaml file.
|
||||
For the next step all Group names have to be added to the yaml file.
|
||||
Take care to that these names match exactly! Assign the unit_types or
|
||||
unit_classes properties to math the needs.
|
||||
|
||||
The Layout miz
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
The miz file is used to define the positioning and orientation of the
|
||||
different units for the template. The actual unit which is used is
|
||||
irrelevant. It is important to use a unique and meaningful name for the
|
||||
groups as this will be used in the yaml file as well. The information
|
||||
which will be extracted from the miz file are just the coordinates and
|
||||
heading of the units.
|
||||
|
||||
*Important*: Every different unit type has to be in a separate Group for
|
||||
the template to work. You can not add units of different types to the
|
||||
same group. They can get merged back together during generation by
|
||||
setting the group property. In the example below both groups
|
||||
``AAA Site 0`` and ``AAA Site 1`` have the group = 1 which means that
|
||||
they will be in the same dcs group during generation.
|
||||
|
||||
*Important*: Liberation expects every template to be designed with an
|
||||
orientation of heading 0 (North) in mind. The complete GroundObject will
|
||||
during the campaign generation process be rotated to match the
|
||||
orientation defined by the campaign designer. If the layout was not
|
||||
created with an orientation of heading 0 the later generated
|
||||
GroundObject will likely be misaligned and not work properly.
|
||||
|
||||
.. todo::
|
||||
max amount of possible units is defined from the miz. Example if later the
|
||||
group should have 6 units than there have to be 6 defined in the miz.
|
||||
|
||||
.. figure:: images/layout_miz_example.png
|
||||
:alt: Example template mission
|
||||
|
||||
Example template mission
|
||||
|
||||
The Layout configuration file
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. todo:: Description about the layout yaml file.
|
||||
|
||||
Possible Information:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Property
|
||||
- Type
|
||||
- Required
|
||||
- Description
|
||||
- Example
|
||||
* - name
|
||||
- ``str``
|
||||
- Yes
|
||||
- A name to identify the template
|
||||
- .. code:: yaml
|
||||
|
||||
name: Armor Group
|
||||
* - tasks
|
||||
- list of ``GroupTask``
|
||||
- Yes
|
||||
- A list of tasks which the template can fulfill
|
||||
- .. code:: yaml
|
||||
|
||||
tasks:
|
||||
- AAA
|
||||
- SHORAD
|
||||
* - generic
|
||||
- ``bool``, default false
|
||||
- No
|
||||
- True if this template will be used to create general ``UnitGroups``
|
||||
-
|
||||
* - description
|
||||
- ``str``
|
||||
- No
|
||||
- Short description of the template
|
||||
-
|
||||
* - groups
|
||||
- List of ``Groups``
|
||||
- Yes
|
||||
- See below for definition of a group
|
||||
-
|
||||
* - layout_file
|
||||
- ``str``
|
||||
- No
|
||||
- The .miz file which has the groups/units of the layout included. Only
|
||||
needed if the file has a different name than the yaml file
|
||||
- .. code:: yaml
|
||||
|
||||
layout_file: resources/layouts/naval/legacy_naval_templates.miz
|
||||
|
||||
.. todo:: Group and SubGroup
|
||||
|
||||
A group has 1..N sub groups. The name of the Group will be used later
|
||||
within the DCS group name.
|
||||
|
||||
All SubGroups will be merged into one DCS Group
|
||||
|
||||
Every unit type has to be defined as a sub group as following:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Property
|
||||
- Type
|
||||
- Required
|
||||
- Description
|
||||
* - name
|
||||
- ``str``
|
||||
- Yes
|
||||
- The group name used in the .miz. Must match exactly!
|
||||
* - optional
|
||||
- ``bool``, default: false
|
||||
- No
|
||||
- Defines wether the layout can be used without this group if the faction
|
||||
has no access to the unit type or the user wants to disable this group
|
||||
* - fill
|
||||
- ``bool``, default: false
|
||||
- No
|
||||
- If the group is optional the layout is used from a PresetGroup this
|
||||
property tells the system if it should use any possible faction
|
||||
accessible unit to fill up this slot if no capable one was defined in
|
||||
the preset yaml.
|
||||
* - unit_count
|
||||
- list of ``int``
|
||||
- No
|
||||
- Amount of units to be generated for this group. Can be fixed or a range
|
||||
where it will be picked randomly
|
||||
* - unit_types
|
||||
- list of DCS unit type IDs
|
||||
- No
|
||||
- Specific unit_types for ground units. Complete list from `vehicles.py`_.
|
||||
This list is extended by all supported mods!
|
||||
* - unit_classes
|
||||
- list of unit classes
|
||||
- No
|
||||
- Unit classes of supported units. Defined by ``UnitClass`` in
|
||||
`game/data/units.py`_.
|
||||
* - statics
|
||||
- list of static types
|
||||
- No
|
||||
- Specific unit_types of statics. Complete list from `statics.py`_
|
||||
|
||||
.. _vehicles.py: https://github.com/pydcs/dcs/blob/master/dcs/vehicles.py
|
||||
.. _game/data/units.py: https://github.com/dcs-liberation/dcs_liberation/blob/develop/game/data/units.py
|
||||
.. _statics.py: https://github.com/pydcs/dcs/blob/master/dcs/statics.py
|
||||
|
||||
Complete example of a generic template for an Aircraft Carrier group:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
name: Carrier Group
|
||||
generic: true
|
||||
tasks:
|
||||
- AircraftCarrier
|
||||
groups:
|
||||
- Carrier: # Group Name of the DCS Group
|
||||
- name: Carrier Group 0 # Sub Group used in the layout.miz
|
||||
unit_count:
|
||||
- 1
|
||||
unit_classes:
|
||||
- AircraftCarrier
|
||||
- Escort: # Group name of the 2nd Group
|
||||
- name: Carrier Group 1
|
||||
unit_count:
|
||||
- 4
|
||||
unit_classes:
|
||||
- Destroyer
|
||||
layout_file: resources/layouts/naval/legacy_naval_templates.miz
|
||||
|
||||
Import Layouts into Liberation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For performance improvements all layouts are serialized to a so called
|
||||
pickle file inside the save folder defined in the liberation
|
||||
preferences. Every time changes are made to the layouts this file has to
|
||||
be recreated. It can be recreated by either deleting the layouts.p file
|
||||
manually or using the special option in the Liberation Toolbar
|
||||
(Developer Tools -> Import Layouts). It will also be recreated after
|
||||
each Liberation update as it will check the Version Number and recreate
|
||||
it when changes are recognized.
|
||||
|
||||
Migration from Generators
|
||||
-------------------------
|
||||
|
||||
The previous generators were migrated using a script which build a group using
|
||||
the generator. All of these groups were save into one .miz file
|
||||
`original_generator_layouts.miz`_. This miz file can be used to verify the
|
||||
templates and to generalize similar templates to decouple the layout from the
|
||||
actual units. As this is a time-consuming and sphisticated task this will be
|
||||
done over time. With the first step the technical requirements will be fulfilled
|
||||
so that the generalization can happen afterwards the technical pr gets merged.
|
||||
|
||||
.. _original_generator_layouts.miz: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/layouts/original_generator_layouts.miz
|
||||
|
||||
Updates for Factions
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
With the rework there were also some changes to the faction file
|
||||
definitions. Older faction files can not be loaded anymore and have to
|
||||
be adopted to the new changes. During migration all default factions
|
||||
were automatically updated, so they will work out of the box.
|
||||
|
||||
You can find more detailed information about how to customize the
|
||||
faction file in `Custom factions`_.
|
||||
|
||||
What was changed:
|
||||
|
||||
* Removed the ``ewrs`` list. All EWRs are now defined in the list
|
||||
``air_defense_units``.
|
||||
* Added the ``air_defense_units`` list. All units with the Role AntiAir can be
|
||||
defined here as `GroundUnitType`_. All possible units are defined in
|
||||
`resources/units/ground_units`_.
|
||||
* Added ``preset_groups``. This list allows to define Preset Groups (described
|
||||
above) like SAM Systems consisting of Launcher, SR, TR and so on instead of
|
||||
adding them each to “air_defense_units”. The presets are defined in
|
||||
`resources/groups`_
|
||||
* Migrated ``air_defenses`` to air_defense_units and preset_sets.
|
||||
* ``Missiles`` are migrated to GroundUnitTypes instead of Generator names (see
|
||||
air_defense_units for how to use)
|
||||
* Removed ``cruisers``, ``destroyers`` and ``naval_generators``. Migrated them
|
||||
to naval_units and preset_groups
|
||||
* Added ``naval_units`` with the correct ship name found in
|
||||
`resources/units/ships`_.
|
||||
* ``aircraft_carrier`` and ``helicopter_carrier`` were moved to ``naval_units``
|
||||
as well.
|
||||
|
||||
.. _Custom factions: https://github.com/dcs-liberation/dcs_liberation/wiki/Custom-Factions
|
||||
.. _GroundUnitType: https://github.com/dcs-liberation/dcs_liberation/blob/develop/game/dcs/groundunittype.py
|
||||
.. _resources/units/ground_units: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/units/ground_units
|
||||
.. _resources/units/ships: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/units/ships
|
||||
.. _resources/groups: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/groups
|
||||
|
||||
Preset Groups
|
||||
-------------
|
||||
|
||||
Instead of adding the exact name of the previous generator to add
|
||||
complex groups like SAM sites or similar to the faction it is now
|
||||
possible to add preset groups to the faction file. As described earlier
|
||||
such a preset group (Force Group) can be defined very easy with a yaml
|
||||
file. This file allows to define the name, tasking, units, statics and
|
||||
the prefered layouts. The first task defines the primary role of the
|
||||
ForceGroup which gets generated from the preset.
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
name: SA-10/S-300PS # The name of the group
|
||||
tasks: # Define at least 1 task
|
||||
- LORAD # The task(s) the Group can fulfill
|
||||
units: # Define at least 1 unit
|
||||
- SAM SA-10 S-300 "Grumble" Clam Shell SR
|
||||
- SAM SA-10 S-300 "Grumble" Big Bird SR
|
||||
- SAM SA-10 S-300 "Grumble" C2
|
||||
- SAM SA-10 S-300 "Grumble" Flap Lid TR
|
||||
- SAM SA-10 S-300 "Grumble" TEL D
|
||||
- SAM SA-10 S-300 "Grumble" TEL C
|
||||
statics: # Optional
|
||||
- # Add some statics here
|
||||
layouts: # Define at least one layout
|
||||
- S-300 Site # prefered layouts for these groups
|
||||
|
||||
Resources:
|
||||
|
||||
* A list of all available preset groups can be found here: `resources/groups`_
|
||||
* All possible tasks can be found in the `game/data/groups.py`_
|
||||
* Units are defined with the variant name found in `resources/units`_
|
||||
|
||||
.. _game/data/groups.py: https://github.com/dcs-liberation/dcs_liberation/blob/develop/game/data/groups.py
|
||||
.. _resources/units: https://github.com/dcs-liberation/dcs_liberation/tree/develop/resources/units
|
||||
76
docs/modding/weather.rst
Normal file
76
docs/modding/weather.rst
Normal file
@@ -0,0 +1,76 @@
|
||||
#######
|
||||
Weather
|
||||
#######
|
||||
|
||||
Weather conditions in DCS Liberation are randomly generated at the start of each
|
||||
turn. Some of the inputs to that generator (more to come) can be controlled via
|
||||
the config files in ``resources/weather``.
|
||||
|
||||
**********
|
||||
Archetypes
|
||||
**********
|
||||
|
||||
A weather archetype defines the the conditions for a style of weather, such as
|
||||
"clear", or "raining". There are currently four archetypes:
|
||||
|
||||
1. clear
|
||||
2. cloudy
|
||||
3. raining
|
||||
4. thunderstorm
|
||||
|
||||
The odds of each archetype appearing in each season are defined in the theater
|
||||
yaml files (``resources/theaters/*/info.yaml``).
|
||||
|
||||
.. literalinclude:: ../../resources/weather/archetypes/clear.yaml
|
||||
:language: yaml
|
||||
:linenos:
|
||||
:caption: resources/weather/archetypes/clear.yaml
|
||||
|
||||
Wind speeds
|
||||
===========
|
||||
|
||||
DCS missions define wind with a speed and heading at each of three altitudes:
|
||||
|
||||
1. MSL
|
||||
2. 2000 meters
|
||||
3. 8000 meters
|
||||
|
||||
Blending between each altitude band is done in a manner defined by DCS.
|
||||
|
||||
Liberation randomly generates a direction for the wind at MSL, and each other
|
||||
altitude band will have wind within +/- 90 degrees of that heading.
|
||||
|
||||
Wind speeds can be modded by altering the ``speed`` dict in the archetype yaml.
|
||||
The only random distribution currently supported is the Weibull distribution, so
|
||||
all archetypes currently use:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
speed:
|
||||
weibull:
|
||||
...
|
||||
|
||||
The Weibull distribution has two parameters: a shape and a scale.
|
||||
|
||||
The scale is simplest to understand. 63.2% of all outcomes of the distribution
|
||||
are below the scale parameter.
|
||||
|
||||
The shape controls where the peak of the distribution is. See the examples in
|
||||
the links below for illustrations and guidelines, but generally speaking low
|
||||
values (between 1 and 2.6) will cause low speeds to be more common, medium
|
||||
values (around 3) will be fairly evenly distributed around the median, and high
|
||||
values (greater than 3.7) will cause high speeds to be more common. As wind
|
||||
speeds tend to be higher at higher altitudes and fairly slow close to the
|
||||
ground, you typically want a low value for MSL, a medium value for 2000m, and a
|
||||
high value for 8000m.
|
||||
|
||||
For examples, see https://statisticsbyjim.com/probability/weibull-distribution/.
|
||||
To experiment with different inputs, use Wolfram Alpha, e.g.
|
||||
https://www.wolframalpha.com/input?i=weibull+distribution+1.5+5.
|
||||
|
||||
When generating wind speeds, each subsequent altitude band will have the lower
|
||||
band's speed added to its scale parameter. That is, for the example above, the
|
||||
actual scale parameter of ``at_2000m`` will be ``20 + wind speed at MSL``, and
|
||||
the scale parameter of ``at_8000m`` will be ``20 + wind speed at 2000m``. This
|
||||
is to ensure that a generally windy day (high wind speed at MSL) will create
|
||||
similarly high winds at higher altitudes and vice versa.
|
||||
2
docs/requirements.txt
Normal file
2
docs/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
myst-parser
|
||||
sphinx_rtd_theme
|
||||
@@ -1,2 +1,3 @@
|
||||
from .game import Game
|
||||
from .version import VERSION
|
||||
from .migrator import Migrator
|
||||
|
||||
@@ -191,11 +191,12 @@ class ForceGroup:
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
game: Game,
|
||||
task: Optional[GroupTask],
|
||||
) -> TheaterGroundObject:
|
||||
"""Create a random TheaterGroundObject from the available templates"""
|
||||
layout = random.choice(self.layouts)
|
||||
return self.create_ground_object_for_layout(
|
||||
layout, name, location, control_point, game
|
||||
layout, name, location, control_point, game, task
|
||||
)
|
||||
|
||||
def create_ground_object_for_layout(
|
||||
@@ -205,9 +206,10 @@ class ForceGroup:
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
game: Game,
|
||||
task: Optional[GroupTask],
|
||||
) -> TheaterGroundObject:
|
||||
"""Create a TheaterGroundObject for the given template"""
|
||||
go = layout.create_ground_object(name, location, control_point)
|
||||
go = layout.create_ground_object(name, location, control_point, task)
|
||||
# Generate all groups using the randomization if it defined
|
||||
for tgo_group in layout.groups:
|
||||
for unit_group in tgo_group.unit_groups:
|
||||
@@ -287,7 +289,7 @@ class ForceGroup:
|
||||
unit.id = game.next_unit_id()
|
||||
# Add unit name escaped so that we do not have scripting issues later
|
||||
unit.name = escape_string_for_lua(
|
||||
unit.unit_type.name if unit.unit_type else unit.type.name
|
||||
unit.unit_type.variant_id if unit.unit_type else unit.type.name
|
||||
)
|
||||
unit.position = PointWithHeading.from_point(
|
||||
ground_object.position + unit.position,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .flight import Flight
|
||||
from .flighttype import FlightType
|
||||
from .flight import Flight
|
||||
from .flightwaypoint import FlightWaypoint
|
||||
from .package import Package
|
||||
|
||||
@@ -1,686 +0,0 @@
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from typing import Type
|
||||
|
||||
from dcs.helicopters import (
|
||||
AH_1W,
|
||||
AH_64A,
|
||||
AH_64D,
|
||||
AH_64D_BLK_II,
|
||||
CH_47D,
|
||||
CH_53E,
|
||||
Ka_50,
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
Mi_26,
|
||||
Mi_28N,
|
||||
Mi_8MT,
|
||||
OH_58D,
|
||||
SA342L,
|
||||
SA342M,
|
||||
SH_60B,
|
||||
UH_1H,
|
||||
UH_60A,
|
||||
)
|
||||
from dcs.planes import (
|
||||
AJS37,
|
||||
AV8BNA,
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
A_20G,
|
||||
A_50,
|
||||
An_26B,
|
||||
B_17G,
|
||||
B_1B,
|
||||
B_52H,
|
||||
Bf_109K_4,
|
||||
C_101CC,
|
||||
C_130,
|
||||
C_17A,
|
||||
C_47,
|
||||
E_2C,
|
||||
E_3A,
|
||||
FA_18C_hornet,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_117A,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_15E,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
H_6J,
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
I_16,
|
||||
JF_17,
|
||||
J_11A,
|
||||
Ju_88A4,
|
||||
KC130,
|
||||
KC135MPRS,
|
||||
KC_135,
|
||||
KJ_2000,
|
||||
L_39ZA,
|
||||
MQ_9_Reaper,
|
||||
M_2000C,
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
MiG_25PD,
|
||||
MiG_27K,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29S,
|
||||
MiG_31,
|
||||
Mirage_2000_5,
|
||||
Mirage_F1B,
|
||||
Mirage_F1BE,
|
||||
Mirage_F1CE,
|
||||
Mirage_F1EE,
|
||||
Mirage_F1EQ,
|
||||
Mirage_F1M_CE,
|
||||
Mirage_F1M_EE,
|
||||
Mirage_F1C_200,
|
||||
Mirage_F1CT,
|
||||
MosquitoFBMkVI,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
RQ_1A_Predator,
|
||||
S_3B,
|
||||
S_3B_Tanker,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_27,
|
||||
Su_30,
|
||||
Su_33,
|
||||
Su_34,
|
||||
Tornado_GR4,
|
||||
Tornado_IDS,
|
||||
Tu_142,
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
WingLoong_I,
|
||||
Yak_40,
|
||||
)
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
from pydcs_extensions.a6a.a6a import VSN_A6A
|
||||
from pydcs_extensions.f100.f100 import VSN_F100
|
||||
from pydcs_extensions.f104.f104 import VSN_F104C, VSN_F104G, VSN_F104S, VSN_F104S_AG
|
||||
from pydcs_extensions.f105.f105 import VSN_F105D, VSN_F105G
|
||||
from pydcs_extensions.f15d.f15d import F_15D
|
||||
from pydcs_extensions.f22a.f22a import F_22A
|
||||
from pydcs_extensions.f4b.f4b import VSN_F4B
|
||||
from pydcs_extensions.f84g.f84g import VSN_F84G
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
|
||||
from pydcs_extensions.su30.su30 import Su_30MKA, Su_30MKI, Su_30MKM, Su_30SM
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
from pydcs_extensions.uh60l.uh60l import KC130J, UH_60L
|
||||
from .flighttype import FlightType
|
||||
|
||||
# All aircraft lists are in priority order. Aircraft higher in the list will be
|
||||
# preferred over those lower in the list.
|
||||
# TODO: These lists really ought to be era (faction) dependent.
|
||||
# Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but
|
||||
# factions that also have F-4s should not.
|
||||
|
||||
# Used for CAP, Escort, and intercept if there is not a specialised aircraft available
|
||||
CAP_CAPABLE = [
|
||||
Su_57,
|
||||
F_22A,
|
||||
F_15C,
|
||||
F_15D,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
Su_33,
|
||||
J_11A,
|
||||
Su_30,
|
||||
Su_30MKA,
|
||||
Su_30MKI,
|
||||
Su_30MKM,
|
||||
Su_30SM,
|
||||
Su_27,
|
||||
MiG_29S,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
JF_17,
|
||||
JAS39Gripen,
|
||||
F_16A,
|
||||
F_4E,
|
||||
VSN_F4B,
|
||||
MiG_31,
|
||||
MiG_25PD,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
MiG_23MLD,
|
||||
MiG_21Bis,
|
||||
Mirage_2000_5,
|
||||
Mirage_F1B,
|
||||
Mirage_F1BE,
|
||||
Mirage_F1CE,
|
||||
Mirage_F1EE,
|
||||
Mirage_F1EQ,
|
||||
Mirage_F1M_CE,
|
||||
Mirage_F1M_EE,
|
||||
Mirage_F1C_200,
|
||||
Mirage_F1CT,
|
||||
F_15E,
|
||||
M_2000C,
|
||||
F_5E_3,
|
||||
VSN_F104S,
|
||||
VSN_F104G,
|
||||
VSN_F104C,
|
||||
MiG_19P,
|
||||
VSN_F100,
|
||||
A_4E_C,
|
||||
F_86F_Sabre,
|
||||
MiG_15bis,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
VSN_F84G,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
MosquitoFBMkVI,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
I_16,
|
||||
]
|
||||
|
||||
|
||||
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
|
||||
CAS_CAPABLE = [
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
Hercules,
|
||||
Su_34,
|
||||
Su_25TM,
|
||||
Su_25T,
|
||||
Su_25,
|
||||
F_15D,
|
||||
F_15E,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
Tornado_GR4,
|
||||
Tornado_IDS,
|
||||
JAS39Gripen_AG,
|
||||
JF_17,
|
||||
AV8BNA,
|
||||
A_10A,
|
||||
B_1B,
|
||||
A_4E_C,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
AJS37,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
Su_33,
|
||||
F_4E,
|
||||
S_3B,
|
||||
Su_30,
|
||||
Su_30MKA,
|
||||
Su_30MKI,
|
||||
Su_30MKM,
|
||||
Su_30SM,
|
||||
MiG_29S,
|
||||
MiG_27K,
|
||||
MiG_29A,
|
||||
MiG_21Bis,
|
||||
AH_64D_BLK_II,
|
||||
AH_64D,
|
||||
AH_64A,
|
||||
AH_1W,
|
||||
OH_58D,
|
||||
SA342M,
|
||||
SA342L,
|
||||
Ka_50,
|
||||
Mi_28N,
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
Mi_8MT,
|
||||
H_6J,
|
||||
MiG_19P,
|
||||
MiG_15bis,
|
||||
M_2000C,
|
||||
Mirage_F1B,
|
||||
Mirage_F1BE,
|
||||
Mirage_F1CE,
|
||||
Mirage_F1EE,
|
||||
Mirage_F1EQ,
|
||||
Mirage_F1M_CE,
|
||||
Mirage_F1M_EE,
|
||||
Mirage_F1CT,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
UH_1H,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
MosquitoFBMkVI,
|
||||
I_16,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
WingLoong_I,
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator,
|
||||
VSN_A6A,
|
||||
VSN_F100,
|
||||
VSN_F105G,
|
||||
VSN_F105D,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
VSN_F104C,
|
||||
VSN_F4B,
|
||||
VSN_F84G,
|
||||
]
|
||||
|
||||
|
||||
# Aircraft used for SEAD and SEAD Escort tasks. Must be capable of the CAS DCS task.
|
||||
SEAD_CAPABLE = [
|
||||
JF_17,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
Tornado_IDS,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
F_4E,
|
||||
A_4E_C,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
JAS39Gripen_AG,
|
||||
AV8BNA,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
Su_34,
|
||||
Su_30,
|
||||
Su_30MKA,
|
||||
Su_30MKI,
|
||||
Su_30MKM,
|
||||
Su_30SM,
|
||||
MiG_27K,
|
||||
Tornado_GR4,
|
||||
VSN_F105G,
|
||||
]
|
||||
|
||||
|
||||
# Aircraft used for DEAD tasks. Must be capable of the CAS DCS task.
|
||||
DEAD_CAPABLE = SEAD_CAPABLE + [
|
||||
AJS37,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
F_16A,
|
||||
JAS39Gripen_AG,
|
||||
B_1B,
|
||||
B_52H,
|
||||
Tu_160,
|
||||
Tu_95MS,
|
||||
H_6J,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
MosquitoFBMkVI,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
VSN_A6A,
|
||||
VSN_F105D,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
VSN_F104C,
|
||||
VSN_F100,
|
||||
VSN_F84G,
|
||||
]
|
||||
|
||||
|
||||
# Aircraft used for Strike mission
|
||||
STRIKE_CAPABLE = [
|
||||
F_117A,
|
||||
B_1B,
|
||||
B_52H,
|
||||
Tu_160,
|
||||
Tu_95MS,
|
||||
Tu_22M3,
|
||||
H_6J,
|
||||
F_15D,
|
||||
F_15E,
|
||||
AJS37,
|
||||
Tornado_GR4,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
F_16A,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
JAS39Gripen_AG,
|
||||
Tornado_IDS,
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_25TM,
|
||||
Su_25T,
|
||||
Su_25,
|
||||
Su_34,
|
||||
Su_33,
|
||||
Su_30,
|
||||
Su_30MKA,
|
||||
Su_30MKI,
|
||||
Su_30MKM,
|
||||
Su_30SM,
|
||||
Su_27,
|
||||
MiG_29S,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
F_4E,
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
S_3B,
|
||||
A_4E_C,
|
||||
M_2000C,
|
||||
Mirage_F1B,
|
||||
Mirage_F1BE,
|
||||
Mirage_F1CE,
|
||||
Mirage_F1EE,
|
||||
Mirage_F1EQ,
|
||||
Mirage_F1M_CE,
|
||||
Mirage_F1M_EE,
|
||||
Mirage_F1CT,
|
||||
MiG_27K,
|
||||
MiG_21Bis,
|
||||
MiG_15bis,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
B_17G,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
MosquitoFBMkVI,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
VSN_A6A,
|
||||
VSN_F100,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
VSN_F104C,
|
||||
VSN_F105G,
|
||||
VSN_F105D,
|
||||
VSN_F4B,
|
||||
VSN_F84G,
|
||||
]
|
||||
|
||||
|
||||
ANTISHIP_CAPABLE = [
|
||||
AJS37,
|
||||
Tu_142,
|
||||
Tu_22M3,
|
||||
H_6J,
|
||||
F_16A,
|
||||
FA_18C_hornet,
|
||||
JAS39Gripen_AG,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
JF_17,
|
||||
Su_34,
|
||||
Su_30,
|
||||
Su_30MKA,
|
||||
Su_30MKI,
|
||||
Su_30MKM,
|
||||
Su_30SM,
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
AV8BNA,
|
||||
S_3B,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
MosquitoFBMkVI,
|
||||
C_101CC,
|
||||
SH_60B,
|
||||
]
|
||||
|
||||
|
||||
# This list does not "inherit" from the strike list because some strike aircraft can
|
||||
# only carry guided weapons, and the AI cannot do runway attack with dguided weapons.
|
||||
# https://github.com/dcs-liberation/dcs_liberation/issues/1703
|
||||
RUNWAY_ATTACK_CAPABLE = [
|
||||
JF_17,
|
||||
Tornado_IDS,
|
||||
M_2000C,
|
||||
H_6J,
|
||||
B_1B,
|
||||
B_52H,
|
||||
Tu_22M3,
|
||||
H_6J,
|
||||
F_15E,
|
||||
AJS37,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
F_16A,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
JAS39Gripen_AG,
|
||||
Tornado_IDS,
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_25TM,
|
||||
Su_25T,
|
||||
Su_25,
|
||||
Su_34,
|
||||
Su_33,
|
||||
Su_30,
|
||||
Su_30MKA,
|
||||
Su_30MKI,
|
||||
Su_30MKM,
|
||||
Su_30SM,
|
||||
Su_27,
|
||||
MiG_29S,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
F_4E,
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
S_3B,
|
||||
A_4E_C,
|
||||
M_2000C,
|
||||
Mirage_F1B,
|
||||
Mirage_F1BE,
|
||||
Mirage_F1CE,
|
||||
Mirage_F1EE,
|
||||
Mirage_F1EQ,
|
||||
Mirage_F1M_CE,
|
||||
Mirage_F1M_EE,
|
||||
Mirage_F1CT,
|
||||
MiG_27K,
|
||||
MiG_21Bis,
|
||||
MiG_15bis,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
B_17G,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
MosquitoFBMkVI,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
VSN_A6A,
|
||||
VSN_F105G,
|
||||
VSN_F105D,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
VSN_F104C,
|
||||
VSN_F100,
|
||||
VSN_F4B,
|
||||
]
|
||||
|
||||
# For any aircraft that isn't necessarily directly involved in strike
|
||||
# missions in a direct combat sense, but can transport objects and infantry.
|
||||
TRANSPORT_CAPABLE = [
|
||||
C_17A,
|
||||
Hercules,
|
||||
C_130,
|
||||
C_47,
|
||||
IL_76MD,
|
||||
An_26B,
|
||||
Yak_40,
|
||||
CH_53E,
|
||||
CH_47D,
|
||||
UH_60L,
|
||||
SH_60B,
|
||||
UH_60A,
|
||||
UH_1H,
|
||||
Mi_8MT,
|
||||
Mi_8MT,
|
||||
Mi_26,
|
||||
]
|
||||
|
||||
AIR_ASSAULT_CAPABLE = [
|
||||
CH_53E,
|
||||
CH_47D,
|
||||
UH_60L,
|
||||
SH_60B,
|
||||
UH_60A,
|
||||
UH_1H,
|
||||
Mi_8MT,
|
||||
Mi_26,
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
Hercules,
|
||||
C_47,
|
||||
]
|
||||
|
||||
DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
|
||||
|
||||
AEWC_CAPABLE = [
|
||||
E_3A,
|
||||
E_2C,
|
||||
A_50,
|
||||
KJ_2000,
|
||||
]
|
||||
|
||||
# Priority is given to the tankers that can carry the most fuel.
|
||||
REFUELING_CAPABALE = [
|
||||
KC_135,
|
||||
KC135MPRS,
|
||||
IL_78M,
|
||||
KC130J,
|
||||
KC130,
|
||||
S_3B_Tanker,
|
||||
]
|
||||
|
||||
|
||||
def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
|
||||
cap_missions = (
|
||||
FlightType.BARCAP,
|
||||
FlightType.INTERCEPTION,
|
||||
FlightType.SWEEP,
|
||||
FlightType.TARCAP,
|
||||
)
|
||||
if task in cap_missions:
|
||||
return CAP_CAPABLE
|
||||
elif task == FlightType.ANTISHIP:
|
||||
return ANTISHIP_CAPABLE
|
||||
elif task == FlightType.BAI:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.CAS:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.SEAD:
|
||||
return SEAD_CAPABLE
|
||||
elif task == FlightType.SEAD_ESCORT:
|
||||
return SEAD_CAPABLE
|
||||
elif task == FlightType.DEAD:
|
||||
return DEAD_CAPABLE
|
||||
elif task == FlightType.OCA_AIRCRAFT:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.OCA_RUNWAY:
|
||||
return RUNWAY_ATTACK_CAPABLE
|
||||
elif task == FlightType.STRIKE:
|
||||
return STRIKE_CAPABLE
|
||||
elif task == FlightType.ESCORT:
|
||||
return CAP_CAPABLE
|
||||
elif task == FlightType.AEWC:
|
||||
return AEWC_CAPABLE
|
||||
elif task == FlightType.REFUELING:
|
||||
return REFUELING_CAPABALE
|
||||
elif task == FlightType.TRANSPORT:
|
||||
return TRANSPORT_CAPABLE
|
||||
elif task == FlightType.AIR_ASSAULT:
|
||||
return AIR_ASSAULT_CAPABLE
|
||||
else:
|
||||
logging.error(f"Unplannable flight type: {task}")
|
||||
return []
|
||||
|
||||
|
||||
def aircraft_for_task(task: FlightType) -> list[AircraftType]:
|
||||
dcs_types = dcs_types_for_task(task)
|
||||
types: list[AircraftType] = []
|
||||
for dcs_type in dcs_types:
|
||||
types.extend(AircraftType.for_dcs_type(dcs_type))
|
||||
return types
|
||||
|
||||
|
||||
def tasks_for_aircraft(aircraft: AircraftType) -> list[FlightType]:
|
||||
tasks: list[FlightType] = []
|
||||
for task in FlightType:
|
||||
if task is FlightType.FERRY:
|
||||
# Not a plannable task, so skip it.
|
||||
continue
|
||||
if aircraft in aircraft_for_task(task):
|
||||
tasks.append(task)
|
||||
return tasks
|
||||
@@ -1,16 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import uuid
|
||||
from collections.abc import Iterator
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, List, Optional, TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
from dcs.planes import C_101CC, C_101EB, Su_33
|
||||
from dcs.planes import C_101CC, C_101EB, Su_33, FA_18C_hornet
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from .flightmembers import FlightMembers
|
||||
from .flightroster import FlightRoster
|
||||
from .flightstate import FlightState, Navigating, Uninitialized
|
||||
from .flightstate.killed import Killed
|
||||
from .loadouts import Loadout
|
||||
from .flighttype import FlightType
|
||||
from .loadouts import Weapon
|
||||
from ..radio.CallsignContainer import CallsignContainer
|
||||
from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
|
||||
from ..radio.TacanContainer import TacanContainer
|
||||
from ..radio.radios import RadioFrequency
|
||||
from ..radio.tacan import TacanChannel
|
||||
from ..sidc import (
|
||||
Entity,
|
||||
SidcDescribable,
|
||||
@@ -20,24 +32,26 @@ from ..sidc import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
from game.sim.simulationresults import SimulationResults
|
||||
from game.squadrons import Squadron, Pilot
|
||||
from game.theater import ControlPoint
|
||||
from game.transfers import TransferOrder
|
||||
from game.data.weapons import WeaponType
|
||||
from .flightmember import FlightMember
|
||||
from .flightplans.flightplan import FlightPlan
|
||||
from .flighttype import FlightType
|
||||
from .flightwaypoint import FlightWaypoint
|
||||
from .package import Package
|
||||
from .starttype import StartType
|
||||
|
||||
F18_TGP_PYLON: int = 4
|
||||
|
||||
class Flight(SidcDescribable):
|
||||
|
||||
class Flight(
|
||||
SidcDescribable, RadioFrequencyContainer, TacanContainer, CallsignContainer
|
||||
):
|
||||
def __init__(
|
||||
self,
|
||||
package: Package,
|
||||
country: str,
|
||||
squadron: Squadron,
|
||||
count: int,
|
||||
flight_type: FlightType,
|
||||
@@ -46,23 +60,35 @@ class Flight(SidcDescribable):
|
||||
custom_name: Optional[str] = None,
|
||||
cargo: Optional[TransferOrder] = None,
|
||||
roster: Optional[FlightRoster] = None,
|
||||
frequency: Optional[RadioFrequency] = None,
|
||||
channel: Optional[TacanChannel] = None,
|
||||
callsign_tcn: Optional[str] = None,
|
||||
claim_inv: bool = True,
|
||||
) -> None:
|
||||
self.id = uuid.uuid4()
|
||||
self.package = package
|
||||
self.country = country
|
||||
self.coalition = squadron.coalition
|
||||
self.squadron = squadron
|
||||
self.squadron.claim_inventory(count)
|
||||
if roster is None:
|
||||
self.roster = FlightRoster(self.squadron, initial_size=count)
|
||||
else:
|
||||
self.roster = roster
|
||||
self.divert = divert
|
||||
self.flight_type = flight_type
|
||||
self.loadout = Loadout.default_for(self)
|
||||
if claim_inv:
|
||||
self.squadron.claim_inventory(count)
|
||||
if roster is None:
|
||||
self.roster = FlightMembers(self, initial_size=count)
|
||||
else:
|
||||
self.roster = FlightMembers.from_roster(self, roster)
|
||||
self.divert = divert
|
||||
|
||||
self.start_type = start_type
|
||||
self.use_custom_loadout = False
|
||||
self.custom_name = custom_name
|
||||
self.group_id: int = 0
|
||||
|
||||
self.frequency = frequency
|
||||
if self.unit_type.dcs_unit_type.tacan:
|
||||
self.tacan = channel
|
||||
self.tcn_name = callsign_tcn
|
||||
|
||||
self.initialize_fuel()
|
||||
self.use_same_loadout_for_all_members = True
|
||||
|
||||
# Only used by transport missions.
|
||||
self.cargo = cargo
|
||||
@@ -86,10 +112,49 @@ class Flight(SidcDescribable):
|
||||
|
||||
self._flight_plan_builder = FlightPlanBuilderTypes.for_flight(self)(self)
|
||||
|
||||
is_f18 = self.squadron.aircraft.dcs_unit_type.id == FA_18C_hornet.id
|
||||
on_land = not self.squadron.location.is_fleet
|
||||
if on_land and is_f18 and self.coalition.game.settings.atflir_autoswap:
|
||||
for fm in self.roster.members:
|
||||
fm.loadout.pylons[F18_TGP_PYLON] = Weapon.with_clsid(
|
||||
str(
|
||||
FA_18C_hornet.Pylon4.AN_AAQ_28_LITENING___Targeting_Pod[1][
|
||||
"clsid"
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
# altitude offset for planes
|
||||
offset_factor = self.coalition.game.settings.max_plane_altitude_offset
|
||||
offset_factor = random.randint(0, offset_factor)
|
||||
self.plane_altitude_offset = 1000 * offset_factor * random.choice([-1, 1])
|
||||
|
||||
@property
|
||||
def available_callsigns(self) -> List[str]:
|
||||
callsigns = set()
|
||||
dcs_unit = self.squadron.aircraft.dcs_unit_type
|
||||
category = dcs_unit.category
|
||||
category = "Air" if category == "Interceptor" else category
|
||||
for name in self.squadron.coalition.faction.country.callsign[category]:
|
||||
callsigns.add(name)
|
||||
if hasattr(dcs_unit, "callnames"):
|
||||
country_name = self.squadron.coalition.faction.country.name
|
||||
for c in dcs_unit.callnames:
|
||||
if "Combined Joint Task Forces" in country_name or c == country_name:
|
||||
for name in dcs_unit.callnames[c]:
|
||||
callsigns.add(name)
|
||||
return sorted(callsigns)
|
||||
|
||||
@property
|
||||
def flight_plan(self) -> FlightPlan[Any]:
|
||||
return self._flight_plan_builder.get_or_build()
|
||||
|
||||
def degrade_to_custom_flight_plan(self) -> None:
|
||||
from .flightplans.custom import Builder as CustomBuilder
|
||||
|
||||
self._flight_plan_builder = CustomBuilder(self, self.flight_plan.waypoints[1:])
|
||||
self.recreate_flight_plan()
|
||||
|
||||
def __getstate__(self) -> dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
# Avoid persisting the flight state since that's not (currently) used outside
|
||||
@@ -101,7 +166,11 @@ class Flight(SidcDescribable):
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
state["state"] = Uninitialized(self, state["squadron"].settings)
|
||||
if "use_same_loadout_for_all_members" not in state:
|
||||
state["use_same_loadout_for_all_members"] = True
|
||||
self.__dict__.update(state)
|
||||
if isinstance(self.roster, FlightRoster):
|
||||
self.roster = FlightMembers.from_roster(self, self.roster)
|
||||
|
||||
@property
|
||||
def blue(self) -> bool:
|
||||
@@ -140,17 +209,28 @@ class Flight(SidcDescribable):
|
||||
return self.squadron.aircraft
|
||||
|
||||
@property
|
||||
def is_helo(self) -> bool:
|
||||
return self.unit_type.dcs_unit_type.helicopter
|
||||
def is_lha(self) -> bool:
|
||||
return self.unit_type.lha_capable
|
||||
|
||||
@property
|
||||
def from_cp(self) -> ControlPoint:
|
||||
return self.departure
|
||||
def is_helo(self) -> bool:
|
||||
return self.unit_type.helicopter
|
||||
|
||||
@property
|
||||
def is_hercules(self) -> bool:
|
||||
return self.unit_type == AircraftType.named("C-130J-30 Super Hercules")
|
||||
|
||||
@property
|
||||
def points(self) -> List[FlightWaypoint]:
|
||||
return self.flight_plan.waypoints[1:]
|
||||
|
||||
@property
|
||||
def custom_targets(self) -> List[MissionTarget]:
|
||||
return [
|
||||
MissionTarget(wpt.name, wpt.position)
|
||||
for wpt in self.flight_plan.layout.custom_waypoints
|
||||
]
|
||||
|
||||
def position(self) -> Point:
|
||||
return self.state.estimate_position()
|
||||
|
||||
@@ -165,31 +245,51 @@ class Flight(SidcDescribable):
|
||||
def missing_pilots(self) -> int:
|
||||
return self.roster.missing_pilots
|
||||
|
||||
def iter_members(self) -> Iterator[FlightMember]:
|
||||
yield from self.roster.members
|
||||
|
||||
def set_flight_type(self, var: FlightType) -> None:
|
||||
self.flight_type = var
|
||||
|
||||
# Update _flight_plan_builder so that the builder class remains relevant
|
||||
# to the flight type
|
||||
from .flightplans.flightplanbuildertypes import FlightPlanBuilderTypes
|
||||
|
||||
self._flight_plan_builder = FlightPlanBuilderTypes.for_flight(self)(self)
|
||||
|
||||
def return_pilots_and_aircraft(self) -> None:
|
||||
self.roster.clear()
|
||||
self.squadron.claim_inventory(-self.count)
|
||||
|
||||
def max_takeoff_fuel(self) -> Optional[float]:
|
||||
# Special case so Su 33 and C101 can take off
|
||||
def initialize_fuel(self) -> None:
|
||||
unit_type = self.unit_type.dcs_unit_type
|
||||
self.fuel = unit_type.fuel_max
|
||||
# Special cases where we want less fuel for takeoff
|
||||
if unit_type == Su_33:
|
||||
if self.flight_type.is_air_to_air:
|
||||
return Su_33.fuel_max / 2.2
|
||||
self.fuel = Su_33.fuel_max / 2.2
|
||||
else:
|
||||
return Su_33.fuel_max * 0.8
|
||||
self.fuel = Su_33.fuel_max * 0.8
|
||||
elif unit_type in {C_101EB, C_101CC}:
|
||||
return unit_type.fuel_max * 0.5
|
||||
return None
|
||||
self.fuel = unit_type.fuel_max * 0.5
|
||||
elif unit_type == Hercules:
|
||||
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:
|
||||
return any(
|
||||
m.loadout.has_weapon_of_type(weapon_type) for m in self.iter_members()
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self) -> str:
|
||||
string = f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
return f"{self.custom_name} - {string}"
|
||||
return string
|
||||
|
||||
def abort(self) -> None:
|
||||
from .flightplans.rtb import RtbFlightPlan
|
||||
@@ -241,9 +341,20 @@ class Flight(SidcDescribable):
|
||||
Killed(self.state.estimate_position(), self, self.squadron.settings)
|
||||
)
|
||||
events.update_flight(self)
|
||||
for pilot in self.roster.pilots:
|
||||
for pilot in self.roster.iter_pilots():
|
||||
if pilot is not None:
|
||||
results.kill_pilot(self, pilot)
|
||||
|
||||
def recreate_flight_plan(self) -> None:
|
||||
self._flight_plan_builder.regenerate()
|
||||
def recreate_flight_plan(self, dump_debug_info: bool = False) -> None:
|
||||
self._flight_plan_builder.regenerate(dump_debug_info)
|
||||
|
||||
@staticmethod
|
||||
def clone_flight(flight: Flight) -> Flight:
|
||||
return Flight(
|
||||
flight.package,
|
||||
flight.squadron,
|
||||
flight.count,
|
||||
flight.flight_type,
|
||||
flight.start_type,
|
||||
flight.divert,
|
||||
)
|
||||
|
||||
49
game/ato/flightmember.py
Normal file
49
game/ato/flightmember.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from game.ato.loadouts import Loadout
|
||||
from game.lasercodes import LaserCode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.squadrons import Pilot
|
||||
|
||||
|
||||
class FlightMember:
|
||||
def __init__(self, pilot: Pilot | None, loadout: Loadout) -> None:
|
||||
self.pilot = pilot
|
||||
self.loadout = loadout
|
||||
self.use_custom_loadout = False
|
||||
self.tgp_laser_code: LaserCode | None = None
|
||||
self.weapon_laser_code: LaserCode | None = None
|
||||
self.properties: dict[str, bool | float | int] = {}
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
if "tgp_laser_code" not in state:
|
||||
state["tgp_laser_code"] = None
|
||||
if "weapon_laser_code" not in state:
|
||||
state["weapon_laser_code"] = None
|
||||
self.__dict__.update(state)
|
||||
|
||||
def assign_tgp_laser_code(self, code: LaserCode) -> None:
|
||||
if self.tgp_laser_code is not None:
|
||||
raise RuntimeError(
|
||||
f"{self.pilot} already has already been assigned laser code "
|
||||
f"{self.tgp_laser_code}"
|
||||
)
|
||||
self.tgp_laser_code = code
|
||||
|
||||
def release_tgp_laser_code(self) -> None:
|
||||
if self.tgp_laser_code is None:
|
||||
raise RuntimeError(f"{self.pilot} has no assigned laser code")
|
||||
|
||||
if self.weapon_laser_code == self.tgp_laser_code:
|
||||
self.weapon_laser_code = None
|
||||
self.tgp_laser_code.release()
|
||||
self.tgp_laser_code = None
|
||||
|
||||
@property
|
||||
def is_player(self) -> bool:
|
||||
if self.pilot is None:
|
||||
return False
|
||||
return self.pilot.player
|
||||
100
game/ato/flightmembers.py
Normal file
100
game/ato/flightmembers.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from .flightmember import FlightMember
|
||||
from .flightroster import FlightRoster
|
||||
from .iflightroster import IFlightRoster
|
||||
from .loadouts import Loadout
|
||||
from ..data.weapons import Weapon
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.squadrons import Pilot, Squadron
|
||||
from .flight import Flight
|
||||
|
||||
|
||||
class FlightMembers(IFlightRoster):
|
||||
def __init__(self, flight: Flight, initial_size: int = 0) -> None:
|
||||
self.flight = flight
|
||||
self.members: list[FlightMember] = []
|
||||
self.resize(initial_size)
|
||||
|
||||
@property
|
||||
def squadron(self) -> Squadron:
|
||||
return self.flight.squadron
|
||||
|
||||
@staticmethod
|
||||
def from_roster(flight: Flight, roster: FlightRoster) -> FlightMembers:
|
||||
members = FlightMembers(flight)
|
||||
loadout = Loadout.default_for(flight)
|
||||
if flight.squadron.aircraft.variant_id == "F-15I Ra'am":
|
||||
loadout.pylons[16] = Weapon.with_clsid("{IDF_MODS_PROJECT_F-15I_Raam_Dome}")
|
||||
members.members = [FlightMember(p, loadout) for p in roster.pilots]
|
||||
return members
|
||||
|
||||
def iter_pilots(self) -> Iterator[Pilot | None]:
|
||||
yield from (m.pilot for m in self.members)
|
||||
|
||||
def pilot_at(self, idx: int) -> Pilot | None:
|
||||
return self.members[idx].pilot
|
||||
|
||||
@property
|
||||
def max_size(self) -> int:
|
||||
return len(self.members)
|
||||
|
||||
@property
|
||||
def player_count(self) -> int:
|
||||
return len([m for m in self.members if m.is_player])
|
||||
|
||||
@property
|
||||
def missing_pilots(self) -> int:
|
||||
return len([m for m in self.members if m.pilot is None])
|
||||
|
||||
def resize(self, new_size: int) -> None:
|
||||
if self.max_size > new_size:
|
||||
for member in self.members[new_size:]:
|
||||
if (pilot := member.pilot) is not None:
|
||||
self.flight.squadron.return_pilot(pilot)
|
||||
if (code := member.tgp_laser_code) is not None:
|
||||
code.release()
|
||||
self.members = self.members[:new_size]
|
||||
return
|
||||
if self.max_size:
|
||||
loadout = self.members[0].loadout.clone()
|
||||
else:
|
||||
loadout = Loadout.default_for(self.flight)
|
||||
if self.flight.squadron.aircraft.variant_id == "F-15I Ra'am":
|
||||
loadout.pylons[16] = Weapon.with_clsid("{IDF_MODS_PROJECT_F-15I_Raam_Dome}")
|
||||
for _ in range(new_size - self.max_size):
|
||||
member = FlightMember(self.flight.squadron.claim_available_pilot(), loadout)
|
||||
member.use_custom_loadout = loadout.is_custom
|
||||
self.members.append(member)
|
||||
|
||||
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
|
||||
if pilot is not None:
|
||||
self.flight.squadron.claim_pilot(pilot)
|
||||
if (current_pilot := self.pilot_at(index)) is not None:
|
||||
self.flight.squadron.return_pilot(current_pilot)
|
||||
self.members[index].pilot = pilot
|
||||
|
||||
def clear(self) -> None:
|
||||
self.flight.squadron.return_pilots(
|
||||
[p for p in self.iter_pilots() if p is not None]
|
||||
)
|
||||
for member in self.members:
|
||||
if (code := member.tgp_laser_code) is not None:
|
||||
code.release()
|
||||
|
||||
def use_same_loadout_for_all_members(self) -> None:
|
||||
if not self.members:
|
||||
return
|
||||
loadout = self.members[0].loadout
|
||||
for member in self.members[1:]:
|
||||
# Do not clone the loadout, we want any changes in the UI to be mirrored
|
||||
# across all flight members.
|
||||
member.loadout = loadout
|
||||
|
||||
def use_distinct_loadouts_for_each_member(self) -> None:
|
||||
for member in self.members:
|
||||
member.loadout = member.loadout.clone()
|
||||
20
game/ato/flightplans/_common_ctld.py
Normal file
20
game/ato/flightplans/_common_ctld.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Union, TYPE_CHECKING, Tuple
|
||||
|
||||
from game.theater.interfaces.CTLD import CTLD
|
||||
from game.theater import ControlPoint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dcs import Point
|
||||
|
||||
|
||||
def generate_random_ctld_point(cp: Union[ControlPoint, CTLD]) -> Point:
|
||||
if isinstance(cp, CTLD) and cp.ctld_zones:
|
||||
zone: Tuple[Point, float] = random.choice(cp.ctld_zones)
|
||||
pos, radius = zone
|
||||
return pos.random_point_within(radius)
|
||||
elif isinstance(cp, CTLD) and isinstance(cp, ControlPoint):
|
||||
return cp.position.random_point_within(2000, 200)
|
||||
raise RuntimeError("Could not generate CTLD point")
|
||||
@@ -6,7 +6,7 @@ from typing import Type
|
||||
from game.ato.flightplans.ibuilder import IBuilder
|
||||
from game.ato.flightplans.patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||
from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
||||
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
|
||||
from game.utils import Distance, Heading, Speed, knots, meters, nautical_miles
|
||||
|
||||
|
||||
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
||||
@@ -48,7 +48,9 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
|
||||
orbit_heading = heading_to_threat_boundary
|
||||
|
||||
# Station 80nm outside the threat zone.
|
||||
threat_buffer = nautical_miles(80)
|
||||
threat_buffer = nautical_miles(
|
||||
self.coalition.game.settings.aewc_threat_buffer_min_distance
|
||||
)
|
||||
if self.threat_zones.threatened(location.position):
|
||||
orbit_distance = distance_to_threat + threat_buffer
|
||||
else:
|
||||
@@ -66,12 +68,9 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
|
||||
orbit_heading.left.degrees, racetrack_half_distance
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
builder = WaypointBuilder(self.flight)
|
||||
|
||||
if self.flight.unit_type.patrol_altitude is not None:
|
||||
altitude = self.flight.unit_type.patrol_altitude
|
||||
else:
|
||||
altitude = feet(25000)
|
||||
altitude = builder.get_patrol_altitude
|
||||
|
||||
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
|
||||
|
||||
@@ -88,7 +87,8 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self) -> AewcFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> AewcFlightPlan:
|
||||
return AewcFlightPlan(self.flight, self.layout())
|
||||
|
||||
@@ -1,119 +1,178 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import Iterator, TYPE_CHECKING, Type
|
||||
|
||||
from game.ato.flightplans.airlift import AirliftLayout
|
||||
from game.ato.flightplans.standard import StandardFlightPlan
|
||||
from game.theater.controlpoint import ControlPointType
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.utils import Distance, feet, meters
|
||||
from .ibuilder import IBuilder
|
||||
from ._common_ctld import generate_random_ctld_point
|
||||
from .formationattack import (
|
||||
FormationAttackLayout,
|
||||
FormationAttackBuilder,
|
||||
FormationAttackFlightPlan,
|
||||
)
|
||||
from .planningerror import PlanningError
|
||||
from .uizonedisplay import UiZone, UiZoneDisplay
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ..flightwaypoint import FlightWaypointType
|
||||
from ...theater.interfaces.CTLD import CTLD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dcs import Point
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirAssaultLayout(AirliftLayout):
|
||||
target: FlightWaypoint
|
||||
@dataclass
|
||||
class AirAssaultLayout(FormationAttackLayout):
|
||||
# The pickup point is optional because we don't always need to load the cargo. When
|
||||
# departing from a carrier, LHA, or off-map spawn, the cargo is pre-loaded.
|
||||
pickup: FlightWaypoint | None = None
|
||||
drop_off: FlightWaypoint | None = None
|
||||
# This is an implementation detail used by CTLD. The aircraft will not go to this
|
||||
# waypoint. It is used by CTLD as the destination for unloaded troops.
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to_pickup
|
||||
if self.pickup:
|
||||
if self.pickup is not None:
|
||||
yield self.pickup
|
||||
yield from self.nav_to_drop_off
|
||||
yield self.drop_off
|
||||
yield self.target
|
||||
yield from self.nav_to_home
|
||||
yield from self.nav_to
|
||||
yield self.join
|
||||
yield self.ingress
|
||||
if self.drop_off is not None:
|
||||
yield self.drop_off
|
||||
yield self.targets[0]
|
||||
yield from self.nav_from
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
|
||||
class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.drop_off
|
||||
def is_airassault(self) -> bool:
|
||||
return True
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
if waypoint == self.tot_waypoint:
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
if self.flight.is_helo and self.layout.drop_off is not None:
|
||||
return self.layout.drop_off
|
||||
return self.layout.targets[0]
|
||||
|
||||
@property
|
||||
def ingress_time(self) -> datetime:
|
||||
tot = self.tot
|
||||
travel_time = self.travel_time_between_waypoints(
|
||||
self.layout.ingress, self.tot_waypoint
|
||||
)
|
||||
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) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def engagement_distance(self) -> Distance:
|
||||
# The radius of the WaypointZone created at the target location
|
||||
def ctld_target_zone_radius(self) -> Distance:
|
||||
return meters(2500)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
def ui_zone(self) -> UiZone:
|
||||
return UiZone(
|
||||
[self.layout.targets[0].position],
|
||||
self.ctld_target_zone_radius,
|
||||
)
|
||||
|
||||
class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
|
||||
class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
def layout(self) -> AirAssaultLayout:
|
||||
if not self.flight.is_helo and not self.flight.is_hercules:
|
||||
raise PlanningError(
|
||||
"Air assault is only usable by helicopters and Anubis' C-130 mod"
|
||||
)
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude
|
||||
builder = WaypointBuilder(self.flight)
|
||||
|
||||
altitude = builder.get_cruise_altitude
|
||||
altitude_is_agl = self.flight.is_helo
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
|
||||
if not self.flight.is_helo or self.flight.departure.cptype in [
|
||||
if self.flight.is_hercules or self.flight.departure.cptype in [
|
||||
ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||
ControlPointType.LHA_GROUP,
|
||||
ControlPointType.OFF_MAP,
|
||||
]:
|
||||
# Non-Helo flights or Off_Map will be preloaded
|
||||
# Off_Map spawns will be preloaded
|
||||
# Carrier operations load the logistics directly from the carrier
|
||||
pickup = None
|
||||
pickup_position = self.flight.departure.position
|
||||
else:
|
||||
# Create a special pickup zone for Helos from Airbase / FOB
|
||||
pickup = builder.pickup(
|
||||
pickup = builder.pickup_zone(
|
||||
MissionTarget(
|
||||
"Pickup Zone",
|
||||
self.flight.departure.position.random_point_within(1200, 600),
|
||||
self._generate_ctld_pickup(),
|
||||
)
|
||||
)
|
||||
pickup.alt = altitude
|
||||
pickup_position = pickup.position
|
||||
assault_area = builder.assault_area(self.package.target)
|
||||
heading = self.package.target.position.heading_between_point(pickup_position)
|
||||
drop_off_zone = MissionTarget(
|
||||
"Dropoff zone",
|
||||
self.package.target.position.point_from_heading(heading, 1200),
|
||||
|
||||
ingress = builder.ingress(
|
||||
FlightWaypointType.INGRESS_AIR_ASSAULT,
|
||||
self.package.waypoints.ingress,
|
||||
self.package.target,
|
||||
)
|
||||
|
||||
assault_area = builder.assault_area(self.package.target)
|
||||
if self.flight.is_hercules:
|
||||
assault_area.only_for_player = False
|
||||
assault_area.alt = feet(1000)
|
||||
|
||||
tgt = self.package.target
|
||||
if isinstance(tgt, CTLD) and tgt.ctld_zones:
|
||||
top3 = sorted(
|
||||
tgt.ctld_zones, key=lambda x: ingress.position.distance_to_point(x[0])
|
||||
)[:3]
|
||||
pos, dist = random.choice(top3)
|
||||
drop_pos = pos.random_point_within(dist)
|
||||
else:
|
||||
heading = tgt.position.heading_between_point(ingress.position)
|
||||
drop_pos = tgt.position.point_from_heading(heading, 1200)
|
||||
drop_off_zone = MissionTarget("Dropoff zone", drop_pos)
|
||||
dz = builder.dropoff_zone(drop_off_zone) if self.flight.is_helo else None
|
||||
|
||||
return AirAssaultLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to_pickup=builder.nav_path(
|
||||
self.flight.departure.position,
|
||||
pickup_position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
pickup=pickup,
|
||||
nav_to_drop_off=builder.nav_path(
|
||||
nav_to=builder.nav_path(
|
||||
pickup_position,
|
||||
drop_off_zone.position,
|
||||
self.package.waypoints.ingress,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
drop_off=builder.drop_off(drop_off_zone),
|
||||
stopover=None,
|
||||
target=assault_area,
|
||||
nav_to_home=builder.nav_path(
|
||||
ingress=ingress,
|
||||
drop_off=dz,
|
||||
targets=[assault_area],
|
||||
nav_from=builder.nav_path(
|
||||
drop_off_zone.position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
@@ -122,7 +181,16 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
hold=None,
|
||||
join=builder.join(ingress.position),
|
||||
split=builder.split(self.flight.arrival.position),
|
||||
refuel=None,
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self) -> AirAssaultFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> AirAssaultFlightPlan:
|
||||
return AirAssaultFlightPlan(self.flight, self.layout())
|
||||
|
||||
def _generate_ctld_pickup(self) -> Point:
|
||||
assert isinstance(self.flight.departure, CTLD)
|
||||
return generate_random_ctld_point(self.flight.departure)
|
||||
|
||||
@@ -1,44 +1,98 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.utils import feet
|
||||
from game.utils import Distance
|
||||
from ._common_ctld import generate_random_ctld_point
|
||||
from .ibuilder import IBuilder
|
||||
from .planningerror import PlanningError
|
||||
from .standard import StandardFlightPlan, StandardLayout
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ...theater.interfaces.CTLD import CTLD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
from dcs import Point
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AirliftLayout(StandardLayout):
|
||||
nav_to_pickup: list[FlightWaypoint]
|
||||
pickup_ascent: FlightWaypoint | None
|
||||
pickup_descent: FlightWaypoint | None
|
||||
# There will not be a pickup waypoint when the pickup airfield is the departure
|
||||
# airfield for cargo planes, as the cargo is pre-loaded. Helicopters will still pick
|
||||
# up the cargo near the airfield.
|
||||
pickup: FlightWaypoint | None
|
||||
# pickup_zone will be used for player flights to create the CTLD stuff
|
||||
ctld_pickup_zone: FlightWaypoint | None
|
||||
drop_off_ascent: FlightWaypoint | None
|
||||
nav_to_drop_off: list[FlightWaypoint]
|
||||
drop_off: FlightWaypoint
|
||||
stopover: FlightWaypoint | None
|
||||
nav_to_home: list[FlightWaypoint]
|
||||
drop_off_descent: FlightWaypoint | None
|
||||
# There will not be a drop-off waypoint when the drop-off airfield and the arrival
|
||||
# airfield is the same for a cargo plane, as planes will land to unload and we don't
|
||||
# want a double landing. Helicopters will still drop their cargo near the airfield
|
||||
# before landing.
|
||||
drop_off: FlightWaypoint | None
|
||||
# drop_off_zone will be used for player flights to create the CTLD stuff
|
||||
ctld_drop_off_zone: FlightWaypoint | None
|
||||
return_ascent: FlightWaypoint
|
||||
return_descent: FlightWaypoint
|
||||
|
||||
def add_waypoint(
|
||||
self, wpt: FlightWaypoint, next_wpt: Optional[FlightWaypoint]
|
||||
) -> bool:
|
||||
new_wpt = self.get_midpoint(wpt, next_wpt)
|
||||
if wpt.waypoint_type in [
|
||||
FlightWaypointType.PICKUP_ZONE,
|
||||
FlightWaypointType.CARGO_STOP,
|
||||
]:
|
||||
self.nav_to_drop_off.insert(0, new_wpt)
|
||||
return True
|
||||
return super().add_waypoint(wpt, next_wpt)
|
||||
|
||||
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
|
||||
if waypoint in self.nav_to_drop_off:
|
||||
self.nav_to_drop_off.remove(waypoint)
|
||||
return True
|
||||
elif super().delete_waypoint(waypoint):
|
||||
return True
|
||||
return False
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to_pickup
|
||||
if self.pickup_ascent is not None:
|
||||
yield self.pickup_ascent
|
||||
yield from self.nav_to
|
||||
if self.pickup_descent is not None:
|
||||
yield self.pickup_descent
|
||||
if self.pickup is not None:
|
||||
yield self.pickup
|
||||
if self.ctld_pickup_zone is not None:
|
||||
yield self.ctld_pickup_zone
|
||||
if self.drop_off_ascent is not None:
|
||||
yield self.drop_off_ascent
|
||||
yield from self.nav_to_drop_off
|
||||
yield self.drop_off
|
||||
if self.stopover is not None:
|
||||
yield self.stopover
|
||||
yield from self.nav_to_home
|
||||
if self.drop_off_descent is not None:
|
||||
yield self.drop_off_descent
|
||||
if self.drop_off is not None:
|
||||
yield self.drop_off
|
||||
yield self.return_ascent
|
||||
if self.ctld_drop_off_zone is not None:
|
||||
yield self.ctld_drop_off_zone
|
||||
yield from self.nav_from
|
||||
yield self.return_descent
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
|
||||
@@ -48,18 +102,26 @@ class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.drop_off
|
||||
# The TOT is the time that the cargo will be dropped off. If the drop-off
|
||||
# location is the arrival airfield and this is not a helicopter flight, there
|
||||
# will not be a separate drop-off waypoint; the arrival landing waypoint is the
|
||||
# drop-off waypoint.
|
||||
return self.layout.drop_off or self.layout.arrival
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
# TOT planning isn't really useful for transports. 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) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
|
||||
@@ -71,37 +133,64 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||
"Cannot plan transport mission for flight with no cargo."
|
||||
)
|
||||
|
||||
altitude = feet(1500)
|
||||
altitude_is_agl = True
|
||||
builder = WaypointBuilder(self.flight)
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
altitude = builder.get_cruise_altitude
|
||||
altitude_is_agl = self.flight.is_helo
|
||||
|
||||
pickup_ascent = None
|
||||
pickup_descent = None
|
||||
pickup = None
|
||||
stopover = None
|
||||
drop_off_ascent = None
|
||||
drop_off_descent = None
|
||||
drop_off = None
|
||||
pickup_zone = None
|
||||
drop_off_zone = None
|
||||
|
||||
if cargo.origin != self.flight.departure:
|
||||
pickup = builder.cargo_stop(cargo.origin)
|
||||
pickup_ascent = self._create_ascent_or_descent(
|
||||
builder,
|
||||
self.flight.departure.position,
|
||||
cargo.origin.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
)
|
||||
pickup_descent = self._create_ascent_or_descent(
|
||||
builder,
|
||||
cargo.origin.position,
|
||||
self.flight.departure.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
)
|
||||
if cargo.next_stop != self.flight.arrival:
|
||||
drop_off = builder.cargo_stop(cargo.next_stop)
|
||||
drop_off_ascent = self._create_ascent_or_descent(
|
||||
builder,
|
||||
cargo.origin.position,
|
||||
cargo.next_stop.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
)
|
||||
drop_off_descent = self._create_ascent_or_descent(
|
||||
builder,
|
||||
cargo.next_stop.position,
|
||||
cargo.origin.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
)
|
||||
|
||||
if self.flight.is_helo:
|
||||
# Create a pickupzone where the cargo will be spawned
|
||||
pickup_zone = MissionTarget(
|
||||
"Pickup Zone", cargo.origin.position.random_point_within(1000, 200)
|
||||
# Create CTLD Zones for Helo flights
|
||||
pickup_zone = builder.pickup_zone(
|
||||
MissionTarget("Pickup Zone", self._generate_ctld_pickup())
|
||||
)
|
||||
pickup = builder.pickup(pickup_zone)
|
||||
# If The cargo is at the departure controlpoint, the pickup waypoint should
|
||||
# only be created for client flights
|
||||
pickup.only_for_player = cargo.origin == self.flight.departure
|
||||
|
||||
# Create a dropoff zone where the cargo should be dropped
|
||||
drop_off_zone = MissionTarget(
|
||||
"Dropoff zone",
|
||||
cargo.next_stop.position.random_point_within(1000, 200),
|
||||
drop_off_zone = builder.dropoff_zone(
|
||||
MissionTarget("Dropoff zone", self._generate_ctld_dropoff())
|
||||
)
|
||||
drop_off = builder.drop_off(drop_off_zone)
|
||||
|
||||
# Add an additional stopover point so that the flight can refuel
|
||||
stopover = builder.stopover(cargo.next_stop)
|
||||
else:
|
||||
# Fixed Wing will get stopover points for pickup and dropoff
|
||||
if cargo.origin != self.flight.departure:
|
||||
pickup = builder.stopover(cargo.origin, "PICKUP")
|
||||
drop_off = builder.stopover(cargo.next_stop, "DROP OFF")
|
||||
# Show the zone waypoints only to the player
|
||||
pickup_zone.only_for_player = True
|
||||
drop_off_zone.only_for_player = True
|
||||
|
||||
nav_to_pickup = builder.nav_path(
|
||||
self.flight.departure.position,
|
||||
@@ -110,38 +199,82 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||
altitude_is_agl,
|
||||
)
|
||||
|
||||
if self.flight.client_count > 0:
|
||||
# Normal Landing Waypoint
|
||||
arrival = builder.land(self.flight.arrival)
|
||||
else:
|
||||
# The AI Needs another Stopover point to actually fly back to the original
|
||||
# base. Otherwise the Cargo drop will be the new Landing Waypoint and the
|
||||
# AI will end its mission there instead of flying back.
|
||||
# https://forum.dcs.world/topic/211775-landing-to-refuel-and-rearm-the-landingrefuar-waypoint/
|
||||
arrival = builder.stopover(self.flight.arrival, "LANDING")
|
||||
return_ascent = self._create_ascent_or_descent(
|
||||
builder,
|
||||
cargo.next_stop.position
|
||||
if cargo.next_stop != self.flight.arrival
|
||||
else cargo.origin.position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
)
|
||||
return_descent = self._create_ascent_or_descent(
|
||||
builder,
|
||||
self.flight.arrival.position,
|
||||
cargo.next_stop.position
|
||||
if cargo.next_stop != self.flight.arrival
|
||||
else cargo.origin.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
)
|
||||
|
||||
return AirliftLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to_pickup=nav_to_pickup,
|
||||
pickup_ascent=pickup_ascent,
|
||||
nav_to=nav_to_pickup,
|
||||
pickup_descent=pickup_descent,
|
||||
pickup=pickup,
|
||||
ctld_pickup_zone=pickup_zone,
|
||||
drop_off_ascent=drop_off_ascent,
|
||||
nav_to_drop_off=builder.nav_path(
|
||||
cargo.origin.position,
|
||||
cargo.next_stop.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
drop_off_descent=drop_off_descent,
|
||||
drop_off=drop_off,
|
||||
stopover=stopover,
|
||||
nav_to_home=builder.nav_path(
|
||||
ctld_drop_off_zone=drop_off_zone,
|
||||
return_ascent=return_ascent,
|
||||
nav_from=builder.nav_path(
|
||||
cargo.origin.position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
arrival=arrival,
|
||||
return_descent=return_descent,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self) -> AirliftFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> AirliftFlightPlan:
|
||||
return AirliftFlightPlan(self.flight, self.layout())
|
||||
|
||||
def _generate_ctld_pickup(self) -> Point:
|
||||
cargo = self.flight.cargo
|
||||
if cargo and cargo.origin and isinstance(cargo.origin, CTLD):
|
||||
return generate_random_ctld_point(cargo.origin)
|
||||
raise RuntimeError("Could not generate CTLD pickup")
|
||||
|
||||
def _generate_ctld_dropoff(self) -> Point:
|
||||
cargo = self.flight.cargo
|
||||
if cargo and cargo.transport and isinstance(cargo.transport.destination, CTLD):
|
||||
return generate_random_ctld_point(cargo.transport.destination)
|
||||
raise RuntimeError("Could not generate CTLD dropoff")
|
||||
|
||||
@staticmethod
|
||||
def _create_ascent_or_descent(
|
||||
builder: WaypointBuilder,
|
||||
start: Point,
|
||||
end: Point,
|
||||
alt: Distance,
|
||||
agl: bool,
|
||||
) -> FlightWaypoint:
|
||||
distance = start.distance_to_point(end)
|
||||
rdistance = 1000 if agl else min(distance / 10, 20000)
|
||||
heading = round(start.heading_between_point(end))
|
||||
rheading = random.randint(heading - 30, heading + 30) % 360
|
||||
pos = start.point_from_heading(float(rheading), rdistance)
|
||||
return builder.nav(pos, alt, agl)
|
||||
|
||||
@@ -35,11 +35,11 @@ class Builder(FormationAttackBuilder[AntiShipFlightPlan, FormationAttackLayout])
|
||||
else:
|
||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||
|
||||
return self._build(FlightWaypointType.INGRESS_BAI, targets)
|
||||
return self._build(FlightWaypointType.INGRESS_ANTI_SHIP, targets)
|
||||
|
||||
@staticmethod
|
||||
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]:
|
||||
return [StrikeTarget(f"{g.group_name} at {tgo.name}", g) for g in tgo.groups]
|
||||
|
||||
def build(self) -> AntiShipFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> AntiShipFlightPlan:
|
||||
return AntiShipFlightPlan(self.flight, self.layout())
|
||||
|
||||
@@ -39,5 +39,5 @@ class Builder(FormationAttackBuilder[BaiFlightPlan, FormationAttackLayout]):
|
||||
|
||||
return self._build(FlightWaypointType.INGRESS_BAI, targets)
|
||||
|
||||
def build(self) -> BaiFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> BaiFlightPlan:
|
||||
return BaiFlightPlan(self.flight, self.layout())
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from datetime import timedelta
|
||||
from typing import Type
|
||||
|
||||
from game.theater import FrontLine
|
||||
from game.utils import Distance, Speed, feet
|
||||
from game.utils import Distance, Speed
|
||||
from .capbuilder import CapBuilder
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||
@@ -41,14 +40,9 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
|
||||
|
||||
start_pos, end_pos = self.cap_racetrack_for_objective(location, barcap=True)
|
||||
|
||||
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||
patrol_alt = max(
|
||||
self.doctrine.min_patrol_altitude,
|
||||
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
||||
)
|
||||
builder = WaypointBuilder(self.flight)
|
||||
patrol_alt = builder.get_patrol_altitude
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
start, end = builder.race_track(start_pos, end_pos, patrol_alt)
|
||||
|
||||
return PatrollingLayout(
|
||||
@@ -64,7 +58,8 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self) -> BarCapFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> BarCapFlightPlan:
|
||||
return BarCapFlightPlan(self.flight, self.layout())
|
||||
|
||||
@@ -60,7 +60,9 @@ class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
# distance from the nearest enemy airbase, but since they are by
|
||||
# definition in enemy territory they can't avoid the threat zone
|
||||
# without being useless.
|
||||
min_distance_from_enemy = nautical_miles(20)
|
||||
min_distance_from_enemy = nautical_miles(
|
||||
self.coalition.game.settings.tarcap_threat_buffer_min_distance
|
||||
)
|
||||
distance_to_airfield = meters(
|
||||
closest_airfield.position.distance_to_point(
|
||||
self.package.target.position
|
||||
|
||||
@@ -6,35 +6,41 @@ from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.theater import FrontLine
|
||||
from game.utils import Distance, Speed, kph, meters
|
||||
from game.utils import Distance, Speed, kph, dcs_to_shapely_point
|
||||
from game.utils import nautical_miles
|
||||
from .ibuilder import IBuilder
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||
from .uizonedisplay import UiZone, UiZoneDisplay
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ...flightplan.ipsolver import IpSolver
|
||||
from ...persistency import waypoint_debug_directory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class CasLayout(PatrollingLayout):
|
||||
target: FlightWaypoint
|
||||
ingress: FlightWaypoint
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to
|
||||
if self.ingress:
|
||||
yield self.ingress
|
||||
yield self.patrol_start
|
||||
yield self.target
|
||||
yield self.patrol_end
|
||||
yield from self.nav_from
|
||||
yield self.departure
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
class CasFlightPlan(PatrollingFlightPlan[CasLayout]):
|
||||
class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
@@ -52,12 +58,13 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout]):
|
||||
|
||||
@property
|
||||
def engagement_distance(self) -> Distance:
|
||||
max_length = self.flight.coalition.game.settings.max_frontline_length * 1000
|
||||
return meters(max_length) / 2
|
||||
return nautical_miles(
|
||||
self.flight.coalition.game.settings.cas_engagement_range_distance
|
||||
)
|
||||
|
||||
@property
|
||||
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
return {self.layout.patrol_start, self.layout.target, self.layout.patrol_end}
|
||||
return {self.layout.ingress, self.layout.patrol_start, self.layout.patrol_end}
|
||||
|
||||
def request_escort_at(self) -> FlightWaypoint | None:
|
||||
return self.layout.patrol_start
|
||||
@@ -65,9 +72,18 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout]):
|
||||
def dismiss_escort_at(self) -> FlightWaypoint | None:
|
||||
return self.layout.patrol_end
|
||||
|
||||
def ui_zone(self) -> UiZone:
|
||||
midpoint = (
|
||||
self.layout.patrol_start.position + self.layout.patrol_end.position
|
||||
) / 2
|
||||
return UiZone(
|
||||
[midpoint],
|
||||
self.engagement_distance,
|
||||
)
|
||||
|
||||
|
||||
class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
||||
def layout(self) -> CasLayout:
|
||||
def layout(self, dump_debug_info: bool) -> CasLayout:
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, FrontLine):
|
||||
@@ -77,49 +93,79 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
||||
FrontLineConflictDescription,
|
||||
)
|
||||
|
||||
bounds = FrontLineConflictDescription.frontline_bounds(
|
||||
location, self.theater, self.coalition.game.settings
|
||||
)
|
||||
ingress = bounds.left_position
|
||||
center = bounds.center
|
||||
egress = bounds.right_position
|
||||
bounds = FrontLineConflictDescription.frontline_bounds(location, self.theater)
|
||||
patrol_start = bounds.left_position
|
||||
patrol_end = bounds.right_position
|
||||
|
||||
ingress_distance = ingress.distance_to_point(self.flight.departure.position)
|
||||
egress_distance = egress.distance_to_point(self.flight.departure.position)
|
||||
if egress_distance < ingress_distance:
|
||||
ingress, egress = egress, ingress
|
||||
start_distance = patrol_start.distance_to_point(self.flight.departure.position)
|
||||
end_distance = patrol_end.distance_to_point(self.flight.departure.position)
|
||||
if end_distance < start_distance:
|
||||
patrol_start, patrol_end = patrol_end, patrol_start
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
builder = WaypointBuilder(self.flight)
|
||||
|
||||
is_helo = self.flight.unit_type.dcs_unit_type.helicopter
|
||||
ingress_egress_altitude = (
|
||||
self.doctrine.ingress_altitude if not is_helo else meters(50)
|
||||
ingress_egress_altitude = builder.get_combat_altitude
|
||||
use_agl_patrol_altitude = is_helo
|
||||
|
||||
ip_solver = IpSolver(
|
||||
dcs_to_shapely_point(self.flight.departure.position),
|
||||
dcs_to_shapely_point(patrol_start),
|
||||
self.doctrine,
|
||||
self.threat_zones.all,
|
||||
)
|
||||
use_agl_ingress_egress = is_helo
|
||||
ip_solver.set_debug_properties(
|
||||
waypoint_debug_directory() / "IP", self.theater.terrain
|
||||
)
|
||||
ingress_point_shapely = ip_solver.solve()
|
||||
if dump_debug_info:
|
||||
ip_solver.dump_debug_info()
|
||||
|
||||
ingress_point = patrol_start.new_in_same_map(
|
||||
ingress_point_shapely.x, ingress_point_shapely.y
|
||||
)
|
||||
|
||||
patrol_start_waypoint = builder.nav(
|
||||
patrol_start, ingress_egress_altitude, use_agl_patrol_altitude
|
||||
)
|
||||
patrol_start_waypoint.name = "FLOT START"
|
||||
patrol_start_waypoint.pretty_name = "FLOT start"
|
||||
patrol_start_waypoint.description = "FLOT boundary"
|
||||
|
||||
patrol_end_waypoint = builder.nav(
|
||||
patrol_end, ingress_egress_altitude, use_agl_patrol_altitude
|
||||
)
|
||||
patrol_end_waypoint.name = "FLOT END"
|
||||
patrol_end_waypoint.pretty_name = "FLOT end"
|
||||
patrol_end_waypoint.description = "FLOT boundary"
|
||||
|
||||
ingress = builder.ingress(
|
||||
FlightWaypointType.INGRESS_CAS, ingress_point, location
|
||||
)
|
||||
ingress.description = f"Ingress to provide CAS at {location}"
|
||||
|
||||
return CasLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
self.flight.departure.position,
|
||||
ingress,
|
||||
ingress_point,
|
||||
ingress_egress_altitude,
|
||||
use_agl_ingress_egress,
|
||||
use_agl_patrol_altitude,
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
egress,
|
||||
patrol_end,
|
||||
self.flight.arrival.position,
|
||||
ingress_egress_altitude,
|
||||
use_agl_ingress_egress,
|
||||
use_agl_patrol_altitude,
|
||||
),
|
||||
patrol_start=builder.ingress(
|
||||
FlightWaypointType.INGRESS_CAS, ingress, location
|
||||
),
|
||||
target=builder.cas(center),
|
||||
patrol_end=builder.egress(egress, location),
|
||||
ingress=ingress,
|
||||
patrol_start=patrol_start_waypoint,
|
||||
patrol_end=patrol_end_waypoint,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self) -> CasFlightPlan:
|
||||
return CasFlightPlan(self.flight, self.layout())
|
||||
def build(self, dump_debug_info: bool = False) -> CasFlightPlan:
|
||||
return CasFlightPlan(self.flight, self.layout(dump_debug_info))
|
||||
|
||||
@@ -2,22 +2,21 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from .flightplan import FlightPlan, Layout
|
||||
from .ibuilder import IBuilder
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from .. import Flight
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class CustomLayout(Layout):
|
||||
custom_waypoints: list[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.custom_waypoints
|
||||
@@ -28,6 +27,10 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def is_custom(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
target_types = (
|
||||
@@ -41,23 +44,35 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
|
||||
return waypoint
|
||||
return self.layout.departure
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.tot_waypoint:
|
||||
return self.package.time_over_target
|
||||
return self.tot
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
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[CustomFlightPlan, CustomLayout]):
|
||||
def layout(self) -> CustomLayout:
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
return CustomLayout(builder.takeoff(self.flight.departure), [])
|
||||
def __init__(
|
||||
self, flight: Flight, waypoints: list[FlightWaypoint] | None = None
|
||||
) -> None:
|
||||
super().__init__(flight)
|
||||
if waypoints is None:
|
||||
waypoints = []
|
||||
self.waypoints = waypoints
|
||||
|
||||
def build(self) -> CustomFlightPlan:
|
||||
def layout(self) -> CustomLayout:
|
||||
builder = WaypointBuilder(self.flight)
|
||||
return CustomLayout(builder.takeoff(self.flight.departure), self.waypoints)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> CustomFlightPlan:
|
||||
return CustomFlightPlan(self.flight, self.layout())
|
||||
|
||||
@@ -37,5 +37,5 @@ class Builder(FormationAttackBuilder[DeadFlightPlan, FormationAttackLayout]):
|
||||
|
||||
return self._build(FlightWaypointType.INGRESS_DEAD)
|
||||
|
||||
def build(self) -> DeadFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> DeadFlightPlan:
|
||||
return DeadFlightPlan(self.flight, self.layout())
|
||||
|
||||
@@ -2,12 +2,15 @@ from __future__ import annotations
|
||||
|
||||
from typing import Type
|
||||
|
||||
from .airassault import AirAssaultLayout
|
||||
from .airlift import AirliftLayout
|
||||
from .formationattack import (
|
||||
FormationAttackBuilder,
|
||||
FormationAttackFlightPlan,
|
||||
FormationAttackLayout,
|
||||
)
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from .. import FlightType
|
||||
|
||||
|
||||
class EscortFlightPlan(FormationAttackFlightPlan):
|
||||
@@ -20,37 +23,81 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
builder = WaypointBuilder(self.flight)
|
||||
ingress, target = builder.escort(
|
||||
self.package.waypoints.ingress, self.package.target
|
||||
)
|
||||
hold = builder.hold(self._hold_point())
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
refuel = None
|
||||
if self.package.waypoints.refuel is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
ingress.only_for_player = True
|
||||
target.only_for_player = True
|
||||
hold = None
|
||||
if not self.flight.is_helo:
|
||||
hold = builder.hold(self._hold_point())
|
||||
|
||||
join_pos = (
|
||||
self.package.waypoints.ingress
|
||||
if self.flight.is_helo
|
||||
else self.package.waypoints.join
|
||||
)
|
||||
join = builder.join(join_pos)
|
||||
|
||||
split = builder.split(self._get_split())
|
||||
|
||||
is_helo = builder.flight.is_helo
|
||||
initial = builder.escort_hold(
|
||||
target.position if is_helo else self.package.waypoints.initial,
|
||||
)
|
||||
|
||||
pf = self.package.primary_flight
|
||||
if pf and pf.flight_type in [FlightType.AIR_ASSAULT, FlightType.TRANSPORT]:
|
||||
layout = pf.flight_plan.layout
|
||||
assert isinstance(layout, AirAssaultLayout) or isinstance(
|
||||
layout, AirliftLayout
|
||||
)
|
||||
if isinstance(layout, AirliftLayout):
|
||||
ascent = layout.pickup_ascent or layout.drop_off_ascent
|
||||
assert ascent is not None
|
||||
join = builder.join(ascent.position)
|
||||
if layout.pickup and layout.drop_off_ascent:
|
||||
join = builder.join(layout.drop_off_ascent.position)
|
||||
elif layout.pickup:
|
||||
join = builder.join(layout.pickup.position)
|
||||
split = builder.split(layout.arrival.position)
|
||||
if layout.drop_off:
|
||||
initial = builder.escort_hold(
|
||||
layout.drop_off.position,
|
||||
)
|
||||
|
||||
refuel = self._build_refuel(builder)
|
||||
|
||||
departure = builder.takeoff(self.flight.departure)
|
||||
nav_to = builder.nav_path(
|
||||
hold.position if hold else departure.position,
|
||||
join.position,
|
||||
builder.get_cruise_altitude,
|
||||
)
|
||||
|
||||
nav_from = builder.nav_path(
|
||||
refuel.position if refuel else split.position,
|
||||
self.flight.arrival.position,
|
||||
builder.get_cruise_altitude,
|
||||
)
|
||||
|
||||
return FormationAttackLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
departure=departure,
|
||||
hold=hold,
|
||||
nav_to=builder.nav_path(
|
||||
hold.position, join.position, self.doctrine.ingress_altitude
|
||||
),
|
||||
nav_to=nav_to,
|
||||
join=join,
|
||||
ingress=ingress,
|
||||
initial=initial,
|
||||
targets=[target],
|
||||
split=split,
|
||||
refuel=refuel,
|
||||
nav_from=builder.nav_path(
|
||||
split.position,
|
||||
self.flight.arrival.position,
|
||||
self.doctrine.ingress_altitude,
|
||||
),
|
||||
nav_from=nav_from,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self) -> EscortFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> EscortFlightPlan:
|
||||
return EscortFlightPlan(self.flight, self.layout())
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.utils import feet
|
||||
@@ -15,17 +15,16 @@ if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class FerryLayout(StandardLayout):
|
||||
nav_to_destination: list[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to_destination
|
||||
yield from self.nav_to
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
|
||||
@@ -37,16 +36,20 @@ class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.arrival
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
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) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
|
||||
@@ -58,17 +61,17 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
|
||||
f"{self.flight.departure}"
|
||||
)
|
||||
|
||||
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
|
||||
builder = WaypointBuilder(self.flight)
|
||||
altitude_is_agl = self.flight.is_helo
|
||||
altitude = (
|
||||
feet(1500)
|
||||
feet(self.coalition.game.settings.heli_cruise_alt_agl)
|
||||
if altitude_is_agl
|
||||
else self.flight.unit_type.preferred_patrol_altitude
|
||||
else builder.get_patrol_altitude
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
return FerryLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to_destination=builder.nav_path(
|
||||
nav_to=builder.nav_path(
|
||||
self.flight.departure.position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
@@ -77,7 +80,9 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
nav_from=[],
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self) -> FerryFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> FerryFlightPlan:
|
||||
return FerryFlightPlan(self.flight, self.layout())
|
||||
|
||||
@@ -8,11 +8,10 @@ generating the waypoints for the mission.
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from abc import ABC
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from functools import cached_property
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
|
||||
|
||||
from game.typeguard import self_type_guard
|
||||
@@ -20,10 +19,9 @@ from game.utils import Distance, Speed, meters
|
||||
from .planningerror import PlanningError
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ..starttype import StartType
|
||||
from ..traveltime import GroundSpeed, TravelTime
|
||||
from ..traveltime import GroundSpeed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import FuelConsumption
|
||||
from game.theater import ControlPoint
|
||||
from ..flight import Flight
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
@@ -32,24 +30,20 @@ if TYPE_CHECKING:
|
||||
from .loiter import LoiterFlightPlan
|
||||
from .patrolling import PatrollingFlightPlan
|
||||
|
||||
INGRESS_TYPES = {
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_ESCORT,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
FlightWaypointType.INGRESS_DEAD,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class Layout(ABC):
|
||||
departure: FlightWaypoint
|
||||
custom_waypoints: list[FlightWaypoint]
|
||||
|
||||
@property
|
||||
def waypoints(self) -> list[FlightWaypoint]:
|
||||
"""A list of all waypoints in the flight plan, in order."""
|
||||
return list(self.iter_waypoints())
|
||||
|
||||
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
|
||||
return False
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
"""Iterates over all waypoints in the flight plan, in order."""
|
||||
raise NotImplementedError
|
||||
@@ -62,6 +56,12 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
def __init__(self, flight: Flight, layout: LayoutT) -> None:
|
||||
self.flight = flight
|
||||
self.layout = layout
|
||||
self.tot_offset = self.default_tot_offset()
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
if "tot_offset" not in state:
|
||||
state["tot_offset"] = self.default_tot_offset()
|
||||
self.__dict__.update(state)
|
||||
|
||||
@property
|
||||
def package(self) -> Package:
|
||||
@@ -105,6 +105,19 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
#
|
||||
# Plus, it's a loiter point so there's no reason to hurry.
|
||||
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.
|
||||
# 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
|
||||
@@ -149,42 +162,9 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def tot(self) -> timedelta:
|
||||
def tot(self) -> datetime:
|
||||
return self.package.time_over_target + self.tot_offset
|
||||
|
||||
@cached_property
|
||||
def bingo_fuel(self) -> int:
|
||||
"""Bingo fuel value for the FlightPlan"""
|
||||
if (fuel := self.flight.unit_type.fuel_consumption) is not None:
|
||||
return self._bingo_estimate(fuel)
|
||||
return self._legacy_bingo_estimate()
|
||||
|
||||
def _bingo_estimate(self, fuel: FuelConsumption) -> int:
|
||||
distance_to_arrival = self.max_distance_from(self.flight.arrival)
|
||||
fuel_consumed = fuel.cruise * distance_to_arrival.nautical_miles
|
||||
bingo = fuel_consumed + fuel.min_safe
|
||||
return math.ceil(bingo / 100) * 100
|
||||
|
||||
def _legacy_bingo_estimate(self) -> int:
|
||||
distance_to_arrival = self.max_distance_from(self.flight.arrival)
|
||||
|
||||
bingo = 1000.0 # Minimum Emergency Fuel
|
||||
bingo += 500 # Visual Traffic
|
||||
bingo += 15 * distance_to_arrival.nautical_miles
|
||||
|
||||
# TODO: Per aircraft tweaks.
|
||||
|
||||
if self.flight.divert is not None:
|
||||
max_divert_distance = self.max_distance_from(self.flight.divert)
|
||||
bingo += 10 * max_divert_distance.nautical_miles
|
||||
|
||||
return round(bingo / 100) * 100
|
||||
|
||||
@cached_property
|
||||
def joker_fuel(self) -> int:
|
||||
"""Joker fuel value for the FlightPlan"""
|
||||
return self.bingo_fuel + 1000
|
||||
|
||||
def max_distance_from(self, cp: ControlPoint) -> Distance:
|
||||
"""Returns the farthest waypoint of the flight plan from a ControlPoint.
|
||||
:arg cp The ControlPoint to measure distance from.
|
||||
@@ -195,8 +175,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
[meters(cp.position.distance_to_point(w.position)) for w in self.waypoints]
|
||||
)
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
def default_tot_offset(self) -> timedelta:
|
||||
"""This flight's offset from the package's TOT.
|
||||
|
||||
Positive values represent later TOTs. An offset of -2 minutes is used
|
||||
@@ -214,20 +193,37 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
)
|
||||
|
||||
for previous_waypoint, waypoint in self.edges(until=destination):
|
||||
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
|
||||
return total
|
||||
total += self.total_time_between_waypoints(previous_waypoint, waypoint)
|
||||
|
||||
# Trim microseconds. Our simulation tick rate is 1 second, so anything that
|
||||
# takes 100.1 or 100.9 seconds will take 100 seconds. DCS doesn't handle
|
||||
# sub-second resolution for tasks anyway, nor are they interesting from a
|
||||
# mission planning perspective, so there's little value to keeping them in the
|
||||
# model.
|
||||
return timedelta(seconds=math.floor(total.total_seconds()))
|
||||
|
||||
def total_time_between_waypoints(
|
||||
self, a: FlightWaypoint, b: FlightWaypoint
|
||||
) -> timedelta:
|
||||
"""Returns the total time spent between a and b.
|
||||
|
||||
The total time between waypoints differs from the travel time in that it may
|
||||
include additional time for actions such as loitering.
|
||||
"""
|
||||
return self.travel_time_between_waypoints(a, b)
|
||||
|
||||
def travel_time_between_waypoints(
|
||||
self, a: FlightWaypoint, b: FlightWaypoint
|
||||
) -> timedelta:
|
||||
return TravelTime.between_points(
|
||||
a.position, b.position, self.speed_between_waypoints(a, b)
|
||||
)
|
||||
error_factor = 1.05
|
||||
speed = self.speed_between_waypoints(a, b)
|
||||
distance = meters(a.position.distance_to_point(b.position))
|
||||
return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
raise NotImplementedError
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
raise NotImplementedError
|
||||
|
||||
def request_escort_at(self) -> FlightWaypoint | None:
|
||||
@@ -250,34 +246,24 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
if waypoint == end:
|
||||
return
|
||||
|
||||
def takeoff_time(self) -> timedelta:
|
||||
def takeoff_time(self) -> datetime:
|
||||
return self.tot - self._travel_time_to_waypoint(self.tot_waypoint)
|
||||
|
||||
def startup_time(self) -> timedelta:
|
||||
start_time = (
|
||||
self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops()
|
||||
def minimum_duration_from_start_to_tot(self) -> timedelta:
|
||||
return (
|
||||
self._travel_time_to_waypoint(self.tot_waypoint)
|
||||
+ self.estimate_startup()
|
||||
+ self.estimate_ground_ops()
|
||||
+ self.estimate_takeoff_time()
|
||||
)
|
||||
|
||||
# In case FP math has given us some barely below zero time, round to
|
||||
# zero.
|
||||
if math.isclose(start_time.total_seconds(), 0):
|
||||
start_time = timedelta()
|
||||
|
||||
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
|
||||
# and they're not interesting from a mission planning perspective so we
|
||||
# don't want them in the UI.
|
||||
#
|
||||
# Round down so *barely* above zero start times are just zero.
|
||||
start_time = timedelta(seconds=math.floor(start_time.total_seconds()))
|
||||
|
||||
# Feature request #1309: Carrier planes should start at +1s
|
||||
# This is a workaround to a DCS problem: some AI planes spawn on
|
||||
# the 'sixpack' when start_time is zero and cause a deadlock.
|
||||
# Workaround: force the start_time to 1 second for these planes.
|
||||
if self.flight.from_cp.is_fleet and start_time.total_seconds() == 0:
|
||||
start_time = timedelta(seconds=1)
|
||||
|
||||
return start_time
|
||||
def startup_time(self) -> datetime:
|
||||
return (
|
||||
self.takeoff_time()
|
||||
- self.estimate_startup()
|
||||
- self.estimate_ground_ops()
|
||||
- self.estimate_takeoff_time()
|
||||
)
|
||||
|
||||
def estimate_startup(self) -> timedelta:
|
||||
if self.flight.start_type is StartType.COLD:
|
||||
@@ -291,13 +277,32 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
def estimate_ground_ops(self) -> timedelta:
|
||||
if self.flight.start_type in {StartType.RUNWAY, StartType.IN_FLIGHT}:
|
||||
return timedelta()
|
||||
if self.flight.from_cp.is_fleet:
|
||||
if self.flight.departure.is_fleet or self.flight.departure.is_fob:
|
||||
return timedelta(minutes=2)
|
||||
else:
|
||||
return timedelta(minutes=8)
|
||||
|
||||
def estimate_takeoff_time(self) -> timedelta:
|
||||
if self.flight.departure.is_offmap:
|
||||
return timedelta()
|
||||
return timedelta(seconds=30)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def is_airassault(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
"""The time that the mission is first on-station."""
|
||||
...
|
||||
|
||||
@property
|
||||
def is_custom(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
"""The time that the mission is complete and the flight RTBs."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from .ocarunway import OcaRunwayFlightPlan
|
||||
from .packagerefueling import PackageRefuelingFlightPlan
|
||||
from .planningerror import PlanningError
|
||||
from .sead import SeadFlightPlan
|
||||
from .seadsweep import SeadSweepFlightPlan
|
||||
from .strike import StrikeFlightPlan
|
||||
from .sweep import SweepFlightPlan
|
||||
from .tarcap import TarCapFlightPlan
|
||||
@@ -26,12 +27,13 @@ from .theaterrefueling import TheaterRefuelingFlightPlan
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato import Flight
|
||||
from game.theater import FrontLine
|
||||
|
||||
|
||||
class FlightPlanBuilderTypes:
|
||||
@staticmethod
|
||||
def for_flight(flight: Flight) -> Type[IBuilder[Any, Any]]:
|
||||
from game.theater import FrontLine
|
||||
|
||||
if flight.flight_type is FlightType.REFUELING:
|
||||
if flight.package.target.is_friendly(flight.squadron.player) or isinstance(
|
||||
flight.package.target, FrontLine
|
||||
@@ -50,6 +52,7 @@ class FlightPlanBuilderTypes:
|
||||
FlightType.OCA_RUNWAY: OcaRunwayFlightPlan.builder_type(),
|
||||
FlightType.SEAD: SeadFlightPlan.builder_type(),
|
||||
FlightType.SEAD_ESCORT: EscortFlightPlan.builder_type(),
|
||||
FlightType.SEAD_SWEEP: SeadSweepFlightPlan.builder_type(),
|
||||
FlightType.STRIKE: StrikeFlightPlan.builder_type(),
|
||||
FlightType.SWEEP: SweepFlightPlan.builder_type(),
|
||||
FlightType.TARCAP: TarCapFlightPlan.builder_type(),
|
||||
|
||||
@@ -2,27 +2,32 @@ from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from functools import cached_property
|
||||
from typing import Any, TYPE_CHECKING, TypeGuard
|
||||
from typing import Any, TYPE_CHECKING, TypeGuard, Optional
|
||||
|
||||
from game.typeguard import self_type_guard
|
||||
from game.utils import Speed
|
||||
from .flightplan import FlightPlan
|
||||
from .loiter import LoiterFlightPlan, LoiterLayout
|
||||
from ..traveltime import GroundSpeed, TravelTime
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class FormationLayout(LoiterLayout, ABC):
|
||||
nav_to: list[FlightWaypoint]
|
||||
join: FlightWaypoint
|
||||
split: FlightWaypoint
|
||||
refuel: FlightWaypoint
|
||||
nav_from: list[FlightWaypoint]
|
||||
refuel: Optional[FlightWaypoint]
|
||||
|
||||
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
|
||||
if waypoint == self.refuel:
|
||||
self.refuel = None
|
||||
return True
|
||||
elif super().delete_waypoint(waypoint):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||
@@ -59,10 +64,7 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||
return min(speeds)
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
|
||||
if b in self.package_speed_waypoints:
|
||||
# Should be impossible, as any package with at least one
|
||||
# FormationFlightPlan flight needs a formation speed.
|
||||
assert self.package.formation_speed is not None
|
||||
if self.package.formation_speed and b in self.package_speed_waypoints:
|
||||
return self.package.formation_speed
|
||||
return super().speed_between_waypoints(a, b)
|
||||
|
||||
@@ -73,15 +75,15 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def join_time(self) -> timedelta:
|
||||
def join_time(self) -> datetime:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def split_time(self) -> timedelta:
|
||||
def split_time(self) -> datetime:
|
||||
...
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.join:
|
||||
return self.join_time
|
||||
elif waypoint == self.layout.split:
|
||||
@@ -89,15 +91,23 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||
return None
|
||||
|
||||
@property
|
||||
def push_time(self) -> timedelta:
|
||||
return self.join_time - TravelTime.between_points(
|
||||
self.layout.hold.position,
|
||||
self.layout.join.position,
|
||||
GroundSpeed.for_flight(self.flight, self.layout.hold.alt),
|
||||
def push_time(self) -> datetime:
|
||||
hold2join_time = (
|
||||
self.travel_time_between_waypoints(
|
||||
self.layout.hold,
|
||||
self.layout.join,
|
||||
)
|
||||
if self.layout.hold
|
||||
else timedelta(0)
|
||||
)
|
||||
return self.join_time - hold2join_time
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.split_time
|
||||
|
||||
@self_type_guard
|
||||
|
||||
@@ -3,14 +3,15 @@ from __future__ import annotations
|
||||
from abc import ABC
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from dcs import Point
|
||||
|
||||
from game.flightplan import HoldZoneGeometry
|
||||
from game.theater import MissionTarget
|
||||
from game.utils import Speed, meters
|
||||
from game.utils import Speed, meters, nautical_miles
|
||||
from .flightplan import FlightPlan
|
||||
from .formation import FormationFlightPlan, FormationLayout
|
||||
from .ibuilder import IBuilder
|
||||
@@ -25,10 +26,6 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
@property
|
||||
def lead_time(self) -> timedelta:
|
||||
return timedelta()
|
||||
|
||||
@property
|
||||
def package_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
return {
|
||||
@@ -50,13 +47,6 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.targets[0]
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
try:
|
||||
return -self.lead_time
|
||||
except AttributeError:
|
||||
return timedelta()
|
||||
|
||||
@property
|
||||
def target_area_waypoint(self) -> FlightWaypoint:
|
||||
return FlightWaypoint(
|
||||
@@ -91,18 +81,18 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
return total
|
||||
|
||||
@property
|
||||
def join_time(self) -> timedelta:
|
||||
travel_time = self.travel_time_between_waypoints(
|
||||
def join_time(self) -> datetime:
|
||||
travel_time = self.total_time_between_waypoints(
|
||||
self.layout.join, self.layout.ingress
|
||||
)
|
||||
return self.ingress_time - travel_time
|
||||
|
||||
@property
|
||||
def split_time(self) -> timedelta:
|
||||
travel_time_ingress = self.travel_time_between_waypoints(
|
||||
def split_time(self) -> datetime:
|
||||
travel_time_ingress = self.total_time_between_waypoints(
|
||||
self.layout.ingress, self.target_area_waypoint
|
||||
)
|
||||
travel_time_egress = self.travel_time_between_waypoints(
|
||||
travel_time_egress = self.total_time_between_waypoints(
|
||||
self.target_area_waypoint, self.layout.split
|
||||
)
|
||||
minutes_at_target = 0.75 * len(self.layout.targets)
|
||||
@@ -115,32 +105,49 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
)
|
||||
|
||||
@property
|
||||
def ingress_time(self) -> timedelta:
|
||||
def ingress_time(self) -> datetime:
|
||||
tot = self.tot
|
||||
travel_time = self.travel_time_between_waypoints(
|
||||
travel_time = self.total_time_between_waypoints(
|
||||
self.layout.ingress, self.target_area_waypoint
|
||||
)
|
||||
return tot - travel_time
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
@property
|
||||
def initial_time(self) -> datetime:
|
||||
tot = self.tot
|
||||
travel_time = self.travel_time_between_waypoints(
|
||||
self.layout.initial, self.target_area_waypoint
|
||||
)
|
||||
return tot - travel_time
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.ingress:
|
||||
return self.ingress_time
|
||||
elif waypoint == self.layout.initial:
|
||||
return self.initial_time
|
||||
elif waypoint in self.layout.targets:
|
||||
return self.tot
|
||||
return super().tot_for_waypoint(waypoint)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class FormationAttackLayout(FormationLayout):
|
||||
ingress: FlightWaypoint
|
||||
targets: list[FlightWaypoint]
|
||||
initial: Optional[FlightWaypoint] = None
|
||||
lineup: Optional[FlightWaypoint] = None
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield self.hold
|
||||
if self.hold:
|
||||
yield self.hold
|
||||
yield from self.nav_to
|
||||
yield self.join
|
||||
if self.lineup:
|
||||
yield self.lineup
|
||||
yield self.ingress
|
||||
if self.initial is not None:
|
||||
yield self.initial
|
||||
yield from self.targets
|
||||
yield self.split
|
||||
if self.refuel is not None:
|
||||
@@ -150,6 +157,7 @@ class FormationAttackLayout(FormationLayout):
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[FormationAttackLayout])
|
||||
@@ -163,7 +171,7 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
targets: list[StrikeTarget] | None = None,
|
||||
) -> FormationAttackLayout:
|
||||
assert self.package.waypoints is not None
|
||||
builder = WaypointBuilder(self.flight, self.coalition, targets)
|
||||
builder = WaypointBuilder(self.flight, targets)
|
||||
|
||||
target_waypoints: list[FlightWaypoint] = []
|
||||
if targets is not None:
|
||||
@@ -178,36 +186,83 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
)
|
||||
)
|
||||
|
||||
hold = builder.hold(self._hold_point())
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
refuel = None
|
||||
if self.package.waypoints.refuel is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
hold = None
|
||||
if not self.flight.is_helo:
|
||||
hold = builder.hold(self._hold_point())
|
||||
join_pos = self.package.waypoints.join
|
||||
if self.flight.is_helo:
|
||||
join_pos = self.package.waypoints.ingress
|
||||
join = builder.join(join_pos)
|
||||
split = builder.split(self._get_split())
|
||||
refuel = self._build_refuel(builder)
|
||||
|
||||
ingress = builder.ingress(
|
||||
ingress_type, self.package.waypoints.ingress, self.package.target
|
||||
)
|
||||
|
||||
initial = None
|
||||
if ingress_type == FlightWaypointType.INGRESS_SEAD:
|
||||
initial = builder.sead_search(self.package.target)
|
||||
elif ingress_type == FlightWaypointType.INGRESS_SEAD_SWEEP:
|
||||
initial = builder.sead_sweep(self.package.target)
|
||||
|
||||
lineup = None
|
||||
if self.flight.flight_type == FlightType.STRIKE:
|
||||
hdg = self.package.target.position.heading_between_point(ingress.position)
|
||||
pos = ingress.position.point_from_heading(hdg, nautical_miles(10).meters)
|
||||
lineup = builder.nav(pos, builder.get_combat_altitude)
|
||||
|
||||
is_helo = self.flight.is_helo
|
||||
ingress_egress_altitude = builder.get_combat_altitude
|
||||
use_agl_ingress_egress = is_helo
|
||||
|
||||
return FormationAttackLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
hold=hold,
|
||||
nav_to=builder.nav_path(
|
||||
hold.position, join.position, self.doctrine.ingress_altitude
|
||||
hold.position if hold else self.flight.departure.position,
|
||||
join.position if join else ingress.position,
|
||||
ingress_egress_altitude,
|
||||
use_agl_ingress_egress,
|
||||
),
|
||||
join=join,
|
||||
ingress=builder.ingress(
|
||||
ingress_type, self.package.waypoints.ingress, self.package.target
|
||||
),
|
||||
lineup=lineup,
|
||||
ingress=ingress,
|
||||
initial=initial,
|
||||
targets=target_waypoints,
|
||||
split=split,
|
||||
refuel=refuel,
|
||||
nav_from=builder.nav_path(
|
||||
split.position,
|
||||
refuel.position if refuel else split.position,
|
||||
self.flight.arrival.position,
|
||||
self.doctrine.ingress_altitude,
|
||||
ingress_egress_altitude,
|
||||
use_agl_ingress_egress,
|
||||
),
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def _build_refuel(self, builder: WaypointBuilder) -> Optional[FlightWaypoint]:
|
||||
refuel: Optional[FlightWaypoint] = None
|
||||
can_plan = self.flight.coalition.air_wing.can_auto_plan(FlightType.REFUELING)
|
||||
if not self.flight.is_helo and can_plan and self.package.waypoints:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
return refuel
|
||||
|
||||
@property
|
||||
def primary_flight_is_air_assault(self) -> bool:
|
||||
if self.flight is self.package.primary_flight:
|
||||
# Can't call self.package.primary_flight.flight_plan here
|
||||
# because the flight-plan wasn't created yet.
|
||||
# Calling the fligh_plan property would result in infinite recursion
|
||||
return self.flight.flight_type == FlightType.AIR_ASSAULT
|
||||
else:
|
||||
assert self.package.primary_flight is not None
|
||||
fp = self.package.primary_flight.flight_plan
|
||||
return fp.is_airassault
|
||||
|
||||
@staticmethod
|
||||
def target_waypoint(
|
||||
flight: Flight, builder: WaypointBuilder, target: StrikeTarget
|
||||
@@ -216,7 +271,7 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
return builder.bai_group(target)
|
||||
elif flight.flight_type == FlightType.DEAD:
|
||||
return builder.dead_point(target)
|
||||
elif flight.flight_type == FlightType.SEAD:
|
||||
elif flight.flight_type in {FlightType.SEAD, FlightType.SEAD_SWEEP}:
|
||||
return builder.sead_point(target)
|
||||
else:
|
||||
return builder.strike_point(target)
|
||||
@@ -243,3 +298,13 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
return HoldZoneGeometry(
|
||||
target, origin, ip, join, self.coalition, self.theater
|
||||
).find_best_hold_point()
|
||||
|
||||
def _get_split(self) -> Point:
|
||||
assert self.package.waypoints is not None
|
||||
assert self.package.primary_flight is not None
|
||||
split_pos = (
|
||||
self.package.primary_flight.arrival.position
|
||||
if self.package.primary_flight.is_helo
|
||||
else self.package.waypoints.split
|
||||
)
|
||||
return split_pos
|
||||
|
||||
@@ -25,6 +25,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
||||
def __init__(self, flight: Flight) -> None:
|
||||
self.flight = flight
|
||||
self._flight_plan: FlightPlanT | None = None
|
||||
self.settings = self.flight.coalition.game.settings
|
||||
|
||||
def get_or_build(self) -> FlightPlanT:
|
||||
if self._flight_plan is None:
|
||||
@@ -32,10 +33,10 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
||||
assert self._flight_plan is not None
|
||||
return self._flight_plan
|
||||
|
||||
def regenerate(self) -> None:
|
||||
def regenerate(self, dump_debug_info: bool = False) -> None:
|
||||
try:
|
||||
self._generate_package_waypoints_if_needed()
|
||||
self._flight_plan = self.build()
|
||||
self._generate_package_waypoints_if_needed(dump_debug_info)
|
||||
self._flight_plan = self.build(dump_debug_info)
|
||||
except NavMeshError as ex:
|
||||
color = "blue" if self.flight.squadron.player else "red"
|
||||
raise PlanningError(
|
||||
@@ -43,10 +44,15 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
||||
f"{self.flight.departure} to {self.package.target}"
|
||||
) from ex
|
||||
|
||||
def _generate_package_waypoints_if_needed(self) -> None:
|
||||
if self.package.waypoints is None:
|
||||
def _generate_package_waypoints_if_needed(self, dump_debug_info: bool) -> None:
|
||||
# Package waypoints are only valid for offensive missions. Skip this if the
|
||||
# target is friendly.
|
||||
if self.package.target.is_friendly(self.is_player):
|
||||
return
|
||||
|
||||
if self.package.waypoints is None or dump_debug_info:
|
||||
self.package.waypoints = PackageWaypoints.create(
|
||||
self.package, self.coalition
|
||||
self.package, self.coalition, dump_debug_info
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -54,11 +60,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
||||
return self.flight.departure.theater
|
||||
|
||||
@abstractmethod
|
||||
def layout(self) -> LayoutT:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def build(self) -> FlightPlanT:
|
||||
def build(self, dump_debug_info: bool = False) -> FlightPlanT:
|
||||
...
|
||||
|
||||
@property
|
||||
|
||||
@@ -2,8 +2,9 @@ from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, TYPE_CHECKING, TypeGuard
|
||||
from typing import Optional
|
||||
|
||||
from game.typeguard import self_type_guard
|
||||
from .flightplan import FlightPlan
|
||||
@@ -13,9 +14,9 @@ if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class LoiterLayout(StandardLayout, ABC):
|
||||
hold: FlightWaypoint
|
||||
hold: Optional[FlightWaypoint]
|
||||
|
||||
|
||||
class LoiterFlightPlan(StandardFlightPlan[Any], ABC):
|
||||
@@ -25,18 +26,18 @@ class LoiterFlightPlan(StandardFlightPlan[Any], ABC):
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def push_time(self) -> timedelta:
|
||||
def push_time(self) -> datetime:
|
||||
...
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.hold:
|
||||
return self.push_time
|
||||
return None
|
||||
|
||||
def travel_time_between_waypoints(
|
||||
def total_time_between_waypoints(
|
||||
self, a: FlightWaypoint, b: FlightWaypoint
|
||||
) -> timedelta:
|
||||
travel_time = super().travel_time_between_waypoints(a, b)
|
||||
travel_time = super().total_time_between_waypoints(a, b)
|
||||
if a != self.layout.hold:
|
||||
return travel_time
|
||||
return travel_time + self.hold_duration
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Type
|
||||
|
||||
from game.theater import Airfield
|
||||
from game.theater import Airfield, Fob
|
||||
from .formationattack import (
|
||||
FormationAttackBuilder,
|
||||
FormationAttackFlightPlan,
|
||||
@@ -23,7 +23,7 @@ class Builder(FormationAttackBuilder[OcaAircraftFlightPlan, FormationAttackLayou
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, Airfield):
|
||||
if not isinstance(location, Airfield) and not isinstance(location, Fob):
|
||||
logging.exception(
|
||||
f"Invalid Objective Location for OCA/Aircraft flight "
|
||||
f"{self.flight=} at {location=}."
|
||||
@@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[OcaAircraftFlightPlan, FormationAttackLayou
|
||||
|
||||
return self._build(FlightWaypointType.INGRESS_OCA_AIRCRAFT)
|
||||
|
||||
def build(self) -> OcaAircraftFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> OcaAircraftFlightPlan:
|
||||
return OcaAircraftFlightPlan(self.flight, self.layout())
|
||||
|
||||
@@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[OcaRunwayFlightPlan, FormationAttackLayout]
|
||||
|
||||
return self._build(FlightWaypointType.INGRESS_OCA_RUNWAY)
|
||||
|
||||
def build(self) -> OcaRunwayFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> OcaRunwayFlightPlan:
|
||||
return OcaRunwayFlightPlan(self.flight, self.layout())
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Type
|
||||
|
||||
from dcs import Point
|
||||
|
||||
from game.utils import Distance, Heading, feet, meters
|
||||
from game.utils import Distance, Heading, meters
|
||||
from .ibuilder import IBuilder
|
||||
from .patrolling import PatrollingLayout
|
||||
from .refuelingflightplan import RefuelingFlightPlan
|
||||
@@ -39,7 +39,7 @@ class PackageRefuelingFlightPlan(RefuelingFlightPlan):
|
||||
)
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
def patrol_start_time(self) -> datetime:
|
||||
altitude = self.flight.unit_type.patrol_altitude
|
||||
|
||||
if altitude is None:
|
||||
@@ -59,10 +59,10 @@ class PackageRefuelingFlightPlan(RefuelingFlightPlan):
|
||||
"REFUEL", FlightWaypointType.REFUEL, refuel, altitude
|
||||
)
|
||||
|
||||
delay_target_to_split: timedelta = self.travel_time_between_waypoints(
|
||||
delay_target_to_split: timedelta = self.total_time_between_waypoints(
|
||||
self.target_area_waypoint(), split_waypoint
|
||||
)
|
||||
delay_split_to_refuel: timedelta = self.travel_time_between_waypoints(
|
||||
delay_split_to_refuel: timedelta = self.total_time_between_waypoints(
|
||||
split_waypoint, refuel_waypoint
|
||||
)
|
||||
|
||||
@@ -96,13 +96,9 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
|
||||
home_heading.degrees, racetrack_half_distance
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
builder = WaypointBuilder(self.flight)
|
||||
|
||||
tanker_type = self.flight.unit_type
|
||||
if tanker_type.patrol_altitude is not None:
|
||||
altitude = tanker_type.patrol_altitude
|
||||
else:
|
||||
altitude = feet(21000)
|
||||
altitude = builder.get_patrol_altitude
|
||||
|
||||
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
|
||||
|
||||
@@ -119,7 +115,8 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self) -> PackageRefuelingFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> PackageRefuelingFlightPlan:
|
||||
return PackageRefuelingFlightPlan(self.flight, self.layout())
|
||||
|
||||
@@ -3,24 +3,23 @@ from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar
|
||||
|
||||
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
|
||||
from game.typeguard import self_type_guard
|
||||
from game.utils import Distance, Speed
|
||||
from .uizonedisplay import UiZone, UiZoneDisplay
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
from .flightplan import FlightPlan
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class PatrollingLayout(StandardLayout):
|
||||
nav_to: list[FlightWaypoint]
|
||||
patrol_start: FlightWaypoint
|
||||
patrol_end: FlightWaypoint
|
||||
nav_from: list[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
@@ -32,12 +31,13 @@ class PatrollingLayout(StandardLayout):
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
LayoutT = TypeVar("LayoutT", bound=PatrollingLayout)
|
||||
|
||||
|
||||
class PatrollingFlightPlan(StandardFlightPlan[LayoutT], ABC):
|
||||
class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def patrol_duration(self) -> timedelta:
|
||||
@@ -60,22 +60,22 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], ABC):
|
||||
"""
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
def patrol_start_time(self) -> datetime:
|
||||
return self.tot
|
||||
|
||||
@property
|
||||
def patrol_end_time(self) -> timedelta:
|
||||
def patrol_end_time(self) -> datetime:
|
||||
# TODO: This is currently wrong for CAS.
|
||||
# CAS missions end when they're winchester or bingo. We need to
|
||||
# configure push tasks for the escorts rather than relying on timing.
|
||||
return self.patrol_start_time + self.patrol_duration
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.patrol_start:
|
||||
return self.patrol_start_time
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.patrol_end:
|
||||
return self.patrol_end_time
|
||||
return None
|
||||
@@ -89,7 +89,11 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], ABC):
|
||||
return self.layout.patrol_start
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return self.patrol_start_time
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.patrol_end_time
|
||||
|
||||
@self_type_guard
|
||||
@@ -97,3 +101,9 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], ABC):
|
||||
self, flight_plan: FlightPlan[Any]
|
||||
) -> TypeGuard[PatrollingFlightPlan[Any]]:
|
||||
return True
|
||||
|
||||
def ui_zone(self) -> UiZone:
|
||||
return UiZone(
|
||||
[self.layout.patrol_start.position, self.layout.patrol_end.position],
|
||||
self.engagement_distance,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.utils import feet
|
||||
@@ -15,19 +15,19 @@ if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class RtbLayout(StandardLayout):
|
||||
abort_location: FlightWaypoint
|
||||
nav_to_destination: list[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield self.abort_location
|
||||
yield from self.nav_to_destination
|
||||
yield from self.nav_to
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
|
||||
@@ -43,15 +43,19 @@ class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.abort_location
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return timedelta()
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.tot
|
||||
|
||||
|
||||
class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
||||
@@ -62,13 +66,13 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
||||
current_position = self.flight.state.estimate_position()
|
||||
current_altitude, altitude_reference = self.flight.state.estimate_altitude()
|
||||
|
||||
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
|
||||
builder = WaypointBuilder(self.flight)
|
||||
altitude_is_agl = self.flight.is_helo
|
||||
altitude = (
|
||||
feet(1500)
|
||||
feet(self.coalition.game.settings.heli_cruise_alt_agl)
|
||||
if altitude_is_agl
|
||||
else self.flight.unit_type.preferred_patrol_altitude
|
||||
else builder.get_patrol_altitude
|
||||
)
|
||||
builder = WaypointBuilder(self.flight, self.flight.coalition)
|
||||
abort_point = builder.nav(
|
||||
current_position, current_altitude, altitude_reference == "RADIO"
|
||||
)
|
||||
@@ -78,7 +82,7 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
||||
return RtbLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
abort_location=abort_point,
|
||||
nav_to_destination=builder.nav_path(
|
||||
nav_to=builder.nav_path(
|
||||
current_position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
@@ -87,7 +91,9 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
nav_from=[],
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self) -> RtbFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> RtbFlightPlan:
|
||||
return RtbFlightPlan(self.flight, self.layout())
|
||||
|
||||
@@ -16,14 +16,13 @@ class SeadFlightPlan(FormationAttackFlightPlan):
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def lead_time(self) -> timedelta:
|
||||
return timedelta(minutes=1)
|
||||
def default_tot_offset(self) -> timedelta:
|
||||
return -timedelta(minutes=1)
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[SeadFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
return self._build(FlightWaypointType.INGRESS_SEAD)
|
||||
|
||||
def build(self) -> SeadFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> SeadFlightPlan:
|
||||
return SeadFlightPlan(self.flight, self.layout())
|
||||
|
||||
28
game/ato/flightplans/seadsweep.py
Normal file
28
game/ato/flightplans/seadsweep.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Type
|
||||
|
||||
from .formationattack import (
|
||||
FormationAttackBuilder,
|
||||
FormationAttackFlightPlan,
|
||||
FormationAttackLayout,
|
||||
)
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
|
||||
|
||||
class SeadSweepFlightPlan(FormationAttackFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
def default_tot_offset(self) -> timedelta:
|
||||
return -timedelta(minutes=2)
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[SeadSweepFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
return self._build(FlightWaypointType.INGRESS_SEAD_SWEEP)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> SeadSweepFlightPlan:
|
||||
return SeadSweepFlightPlan(self.flight, self.layout())
|
||||
@@ -1,20 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
from typing import TYPE_CHECKING, TypeVar, Optional
|
||||
|
||||
from game.ato.flightplans.flightplan import FlightPlan, Layout
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ...utils import feet
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class StandardLayout(Layout, ABC):
|
||||
arrival: FlightWaypoint
|
||||
divert: FlightWaypoint | None
|
||||
bullseye: FlightWaypoint
|
||||
nav_to: list[FlightWaypoint]
|
||||
nav_from: list[FlightWaypoint]
|
||||
|
||||
def add_waypoint(
|
||||
self, wpt: FlightWaypoint, next_wpt: Optional[FlightWaypoint]
|
||||
) -> bool:
|
||||
new_wpt = self.get_midpoint(wpt, next_wpt)
|
||||
if wpt.waypoint_type in [FlightWaypointType.TAKEOFF, FlightWaypointType.LOITER]:
|
||||
self.nav_to.insert(0, new_wpt)
|
||||
return True
|
||||
elif wpt.waypoint_type in [
|
||||
FlightWaypointType.SPLIT,
|
||||
FlightWaypointType.REFUEL,
|
||||
FlightWaypointType.PATROL,
|
||||
FlightWaypointType.EGRESS,
|
||||
]:
|
||||
self.nav_from.insert(0, new_wpt)
|
||||
return True
|
||||
elif wpt.waypoint_type is FlightWaypointType.NAV:
|
||||
if wpt in self.nav_to:
|
||||
index = self.nav_to.index(wpt) + 1
|
||||
self.nav_to.insert(index, new_wpt)
|
||||
return True
|
||||
elif wpt in self.nav_from:
|
||||
index = self.nav_from.index(wpt) + 1
|
||||
self.nav_from.insert(index, new_wpt)
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_midpoint(
|
||||
wpt: FlightWaypoint, next_wpt: Optional[FlightWaypoint]
|
||||
) -> FlightWaypoint:
|
||||
new_pos = deepcopy(wpt.position)
|
||||
next_alt = feet(20000)
|
||||
if next_wpt:
|
||||
new_pos = wpt.position.lerp(next_wpt.position, 0.5)
|
||||
next_alt = next_wpt.alt
|
||||
new_wpt = WaypointBuilder.nav(new_pos, max(wpt.alt, next_alt))
|
||||
return new_wpt
|
||||
|
||||
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
|
||||
if waypoint is self.divert:
|
||||
self.divert = None
|
||||
return True
|
||||
elif waypoint in self.nav_to:
|
||||
self.nav_to.remove(waypoint)
|
||||
return True
|
||||
elif waypoint in self.nav_from:
|
||||
self.nav_from.remove(waypoint)
|
||||
return True
|
||||
elif waypoint in self.custom_waypoints:
|
||||
self.custom_waypoints.remove(waypoint)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
LayoutT = TypeVar("LayoutT", bound=StandardLayout)
|
||||
|
||||
@@ -11,6 +11,7 @@ from .formationattack import (
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from .waypointbuilder import StrikeTarget
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ...theater.theatergroup import SceneryUnit
|
||||
|
||||
|
||||
class StrikeFlightPlan(FormationAttackFlightPlan):
|
||||
@@ -28,9 +29,12 @@ class Builder(FormationAttackBuilder[StrikeFlightPlan, FormationAttackLayout]):
|
||||
|
||||
targets: list[StrikeTarget] = []
|
||||
for idx, unit in enumerate(location.strike_targets):
|
||||
targets.append(StrikeTarget(f"{unit.type.id} #{idx}", unit))
|
||||
name = unit.type.id
|
||||
if isinstance(unit, SceneryUnit):
|
||||
name = unit.name
|
||||
targets.append(StrikeTarget(f"{name} #{idx}", unit))
|
||||
|
||||
return self._build(FlightWaypointType.INGRESS_STRIKE, targets)
|
||||
|
||||
def build(self) -> StrikeFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> StrikeFlightPlan:
|
||||
return StrikeFlightPlan(self.flight, self.layout())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Iterator, TYPE_CHECKING, Type
|
||||
|
||||
from dcs import Point
|
||||
@@ -10,41 +10,35 @@ from game.utils import Heading
|
||||
from .ibuilder import IBuilder
|
||||
from .loiter import LoiterFlightPlan, LoiterLayout
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ..traveltime import GroundSpeed, TravelTime
|
||||
from ...flightplan import HoldZoneGeometry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class SweepLayout(LoiterLayout):
|
||||
nav_to: list[FlightWaypoint]
|
||||
sweep_start: FlightWaypoint
|
||||
sweep_end: FlightWaypoint
|
||||
refuel: FlightWaypoint | None
|
||||
nav_from: list[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield self.hold
|
||||
if self.hold:
|
||||
yield self.hold
|
||||
yield from self.nav_to
|
||||
yield self.sweep_start
|
||||
yield self.sweep_end
|
||||
if self.refuel is not None:
|
||||
yield self.refuel
|
||||
yield from self.nav_from
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
class SweepFlightPlan(LoiterFlightPlan):
|
||||
@property
|
||||
def lead_time(self) -> timedelta:
|
||||
return timedelta(minutes=5)
|
||||
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
@@ -57,42 +51,45 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.sweep_end
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
return -self.lead_time
|
||||
def default_tot_offset(self) -> timedelta:
|
||||
return -timedelta(minutes=5)
|
||||
|
||||
@property
|
||||
def sweep_start_time(self) -> timedelta:
|
||||
travel_time = self.travel_time_between_waypoints(
|
||||
def sweep_start_time(self) -> datetime:
|
||||
travel_time = self.total_time_between_waypoints(
|
||||
self.layout.sweep_start, self.layout.sweep_end
|
||||
)
|
||||
return self.sweep_end_time - travel_time
|
||||
|
||||
@property
|
||||
def sweep_end_time(self) -> timedelta:
|
||||
def sweep_end_time(self) -> datetime:
|
||||
return self.tot
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.sweep_start:
|
||||
return self.sweep_start_time
|
||||
if waypoint == self.layout.sweep_end:
|
||||
return self.sweep_end_time
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.hold:
|
||||
return self.push_time
|
||||
return None
|
||||
|
||||
@property
|
||||
def push_time(self) -> timedelta:
|
||||
return self.sweep_end_time - TravelTime.between_points(
|
||||
self.layout.hold.position,
|
||||
self.layout.sweep_end.position,
|
||||
GroundSpeed.for_flight(self.flight, self.layout.hold.alt),
|
||||
def push_time(self) -> datetime:
|
||||
return self.sweep_end_time - self.travel_time_between_waypoints(
|
||||
self.layout.hold,
|
||||
self.layout.sweep_end,
|
||||
)
|
||||
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
@property
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.sweep_end_time
|
||||
|
||||
|
||||
@@ -107,33 +104,28 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
|
||||
heading.degrees, -self.doctrine.sweep_distance.meters
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
|
||||
builder = WaypointBuilder(self.flight)
|
||||
altitude = builder.get_patrol_altitude
|
||||
|
||||
start, end = builder.sweep(start_pos, target, altitude)
|
||||
|
||||
hold = builder.hold(self._hold_point())
|
||||
|
||||
refuel = None
|
||||
|
||||
if self.package.waypoints is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
|
||||
return SweepLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
hold=hold,
|
||||
nav_to=builder.nav_path(
|
||||
hold.position, start.position, self.doctrine.ingress_altitude
|
||||
),
|
||||
nav_to=builder.nav_path(hold.position, start.position, altitude),
|
||||
nav_from=builder.nav_path(
|
||||
end.position,
|
||||
self.flight.arrival.position,
|
||||
self.doctrine.ingress_altitude,
|
||||
altitude,
|
||||
),
|
||||
sweep_start=start,
|
||||
sweep_end=end,
|
||||
refuel=refuel,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def _hold_point(self) -> Point:
|
||||
@@ -146,5 +138,5 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
|
||||
target, origin, ip, join, self.coalition, self.theater
|
||||
).find_best_hold_point()
|
||||
|
||||
def build(self) -> SweepFlightPlan:
|
||||
def build(self, dump_debug_info: bool = False) -> SweepFlightPlan:
|
||||
return SweepFlightPlan(self.flight, self.layout())
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user