mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
1168 Commits
1.06
...
develop_2_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f65637f6b | ||
|
|
cceb3da693 | ||
|
|
a357bf3c08 | ||
|
|
3f251c38d8 | ||
|
|
01702f046e | ||
|
|
d394d01ea8 | ||
|
|
70d982b0ed | ||
|
|
c3b028ef4b | ||
|
|
206d09f7f8 | ||
|
|
87248fec53 | ||
|
|
b7634a8ac3 | ||
|
|
434755a620 | ||
|
|
3eb2529b0b | ||
|
|
e6e4cca076 | ||
|
|
8d57bbc777 | ||
|
|
483db564f9 | ||
|
|
63d5862319 | ||
|
|
365b379798 | ||
|
|
fd473f0a46 | ||
|
|
a816877d08 | ||
|
|
2d56ae1cb6 | ||
|
|
216adcc35a | ||
|
|
8d485d5fa2 | ||
|
|
fa549fcf94 | ||
|
|
b7d160631a | ||
|
|
d05897edcb | ||
|
|
1d98432c57 | ||
|
|
21cd764f66 | ||
|
|
dfc31dfd5c | ||
|
|
2e067aada6 | ||
|
|
78cd60f3df | ||
|
|
96c401e1b9 | ||
|
|
fad132dcca | ||
|
|
696710bf41 | ||
|
|
f8735927bf | ||
|
|
16cfc4e945 | ||
|
|
3987274764 | ||
|
|
73a97f9c2a | ||
|
|
169fba9ab8 | ||
|
|
0b902e19ee | ||
|
|
1f43fbe16e | ||
|
|
f29cb99530 | ||
|
|
9e32ea7413 | ||
|
|
95fd4cab05 | ||
|
|
c4d08fa7b7 | ||
|
|
cec28351e7 | ||
|
|
9620ac7e7e | ||
|
|
c0bfdbf4bb | ||
|
|
efb544a303 | ||
|
|
adfc4b7244 | ||
|
|
7a5ce98569 | ||
|
|
1fcceb0901 | ||
|
|
22c552053f | ||
|
|
818c679d4f | ||
|
|
3ff36c45aa | ||
|
|
623d461b06 | ||
|
|
7535013848 | ||
|
|
a63bac8826 | ||
|
|
d2c831c4ee | ||
|
|
840b5ce071 | ||
|
|
be6abc0025 | ||
|
|
ef585c59dd | ||
|
|
16d9c1ccad | ||
|
|
9a9ef78583 | ||
|
|
fe7ee5b610 | ||
|
|
680804040a | ||
|
|
0d4fe73daa | ||
|
|
407190c6c5 | ||
|
|
73998dbde0 | ||
|
|
8827f7df34 | ||
|
|
4c394a9e2d | ||
|
|
5946fc7404 | ||
|
|
5b8ecb2c14 | ||
|
|
61253e4d4d | ||
|
|
2c0ca5803f | ||
|
|
690f3d0f13 | ||
|
|
42c259bc58 | ||
|
|
11426a0713 | ||
|
|
e6af1b8645 | ||
|
|
103f18191d | ||
|
|
fb312236a2 | ||
|
|
c850c0095d | ||
|
|
58481268f7 | ||
|
|
11604671f8 | ||
|
|
e8feded4c3 | ||
|
|
18f9b38d25 | ||
|
|
676eea3ccc | ||
|
|
ee113d080e | ||
|
|
8bc69415a7 | ||
|
|
3979ee57ff | ||
|
|
c2ee169d16 | ||
|
|
56b51c85bb | ||
|
|
853ee5aac4 | ||
|
|
4cf406aefa | ||
|
|
1abb341cb6 | ||
|
|
44dce9598c | ||
|
|
aefc8685a1 | ||
|
|
26761342f5 | ||
|
|
ca777bcebb | ||
|
|
5742075ff2 | ||
|
|
c7a6ec9691 | ||
|
|
e0153cfa6a | ||
|
|
3fd5e1bae7 | ||
|
|
d9511a7edd | ||
|
|
040db055fd | ||
|
|
b9f8cfd10d | ||
|
|
ff46556927 | ||
|
|
d1815a3d6e | ||
|
|
fdfa4827ab | ||
|
|
5d579ccef9 | ||
|
|
4145d5578e | ||
|
|
43eb041bb8 | ||
|
|
0b8ac8fc47 | ||
|
|
de3ba5908f | ||
|
|
bbb6251aa9 | ||
|
|
6f71d92a7b | ||
|
|
a8b59cc567 | ||
|
|
f4d3660eac | ||
|
|
1f165835c6 | ||
|
|
9087f3487d | ||
|
|
4ca92ea22d | ||
|
|
e2682d633f | ||
|
|
e6cb1b5970 | ||
|
|
de2d548139 | ||
|
|
4d1a0b85e4 | ||
|
|
5cfbd8c3ad | ||
|
|
95f72be8eb | ||
|
|
95c4dfa52f | ||
|
|
b72a2f4a5f | ||
|
|
844f8595d1 | ||
|
|
1c9d9be667 | ||
|
|
2a02a743a4 | ||
|
|
968d9365d6 | ||
|
|
cdb16cc591 | ||
|
|
86bc41c15c | ||
|
|
ac05c7cfaa | ||
|
|
9c07fe5963 | ||
|
|
ed05f995b5 | ||
|
|
85491dca20 | ||
|
|
465399f803 | ||
|
|
3550c8a8f6 | ||
|
|
739c0f8f52 | ||
|
|
49aa79c612 | ||
|
|
cdde75b517 | ||
|
|
dde74af6b5 | ||
|
|
eff9c77c9a | ||
|
|
5ba633c8a1 | ||
|
|
ab67a38ca5 | ||
|
|
08f0c9d30a | ||
|
|
9d747a9f9b | ||
|
|
31ca121498 | ||
|
|
44b5f5acf1 | ||
|
|
eb4878dfc4 | ||
|
|
3dc7dc3d1a | ||
|
|
6a6133e5cd | ||
|
|
65c85d7f0b | ||
|
|
d519dfa5da | ||
|
|
73ea83bbdd | ||
|
|
235a5ec538 | ||
|
|
f81a3d03c0 | ||
|
|
6878b57fba | ||
|
|
0143e5641f | ||
|
|
5adc92c601 | ||
|
|
0b2fbddbc5 | ||
|
|
28035bf02b | ||
|
|
6c9a9de3f3 | ||
|
|
62139fc4eb | ||
|
|
88b9ed29ba | ||
|
|
d94c57afd6 | ||
|
|
b6421646ff | ||
|
|
611b6fc272 | ||
|
|
9cdbef9faf | ||
|
|
b34de70fc7 | ||
|
|
f5047fc0cc | ||
|
|
258c34e61d | ||
|
|
f365487fd6 | ||
|
|
8f65b7ee7c | ||
|
|
9397f1f39c | ||
|
|
f03121af5a | ||
|
|
239b9f8234 | ||
|
|
1620c602cf | ||
|
|
fa01303460 | ||
|
|
1e5bd916d9 | ||
|
|
62d89239fc | ||
|
|
91bee4e6c2 | ||
|
|
a465dde32f | ||
|
|
254dd5f70f | ||
|
|
1a70ed5121 | ||
|
|
16d5a550ce | ||
|
|
794cc43a41 | ||
|
|
8583bbf893 | ||
|
|
63d510f2ea | ||
|
|
2c6b26003b | ||
|
|
fdaf3bc30f | ||
|
|
816d9696b5 | ||
|
|
bdaa6a294a | ||
|
|
a6b15b9529 | ||
|
|
878529e8f8 | ||
|
|
2cb37b5bd8 | ||
|
|
de95cfc981 | ||
|
|
0477247cf2 | ||
|
|
e0319a4047 | ||
|
|
839f163ac5 | ||
|
|
ba9ad4c371 | ||
|
|
bdcf7c1828 | ||
|
|
821e9fc114 | ||
|
|
334d21897b | ||
|
|
b405c3ab32 | ||
|
|
3cabb1e02d | ||
|
|
ec7f8f5710 | ||
|
|
93f0627c5e | ||
|
|
3d8c2d689e | ||
|
|
e1572c09ff | ||
|
|
910af12fb9 | ||
|
|
acbd45341f | ||
|
|
901c89371c | ||
|
|
743534bdda | ||
|
|
98bb1a9f65 | ||
|
|
bfc602f22f | ||
|
|
dd2b61edf3 | ||
|
|
0fd58135fd | ||
|
|
4672252242 | ||
|
|
a0c61bf73a | ||
|
|
d6c19a8aff | ||
|
|
b6a933e264 | ||
|
|
0191eca9dc | ||
|
|
f962fd55bc | ||
|
|
33fa719e8d | ||
|
|
adb9352905 | ||
|
|
58574c67df | ||
|
|
04d3ba4c47 | ||
|
|
0f1d2b8685 | ||
|
|
c06a855113 | ||
|
|
e9bfd58ee1 | ||
|
|
5f02febb6c | ||
|
|
15db12fb21 | ||
|
|
c3fca6696d | ||
|
|
dd4c37cde3 | ||
|
|
aa7ffdabb0 | ||
|
|
85f6616185 | ||
|
|
c8955bdca7 | ||
|
|
669a8c9e64 | ||
|
|
b4d0eb0b99 | ||
|
|
035bebaab8 | ||
|
|
b20200318b | ||
|
|
9caf83cda9 | ||
|
|
493f9df4e2 | ||
|
|
cc61893bca | ||
|
|
3ba2fb76aa | ||
|
|
e024da277b | ||
|
|
bc1e793ce6 | ||
|
|
aa7eacc043 | ||
|
|
dd7b9f1790 | ||
|
|
d6b94345d9 | ||
|
|
aa1ac56ec3 | ||
|
|
fd969020af | ||
|
|
95f486870d | ||
|
|
f6d049da3c | ||
|
|
69f15824ca | ||
|
|
8c70d1ab79 | ||
|
|
58fd651a0b | ||
|
|
177b505cb7 | ||
|
|
8ac5dbe22a | ||
|
|
b744238fb8 | ||
|
|
57b7402753 | ||
|
|
dcaa390d24 | ||
|
|
59010f6949 | ||
|
|
6a91fad10a | ||
|
|
f03029417d | ||
|
|
f5aa342602 | ||
|
|
53582ba539 | ||
|
|
44c976948d | ||
|
|
41d5020467 | ||
|
|
1bd26005f2 | ||
|
|
b1840ce2ca | ||
|
|
769246c55d | ||
|
|
24394d4d00 | ||
|
|
cab5825b72 | ||
|
|
84beb2dfe5 | ||
|
|
f8ac39fb82 | ||
|
|
eb69d01067 | ||
|
|
1c4f255c7f | ||
|
|
4125f6ec06 | ||
|
|
5023e0d30f | ||
|
|
f65595c626 | ||
|
|
916d1eec96 | ||
|
|
c2d615315e | ||
|
|
aa96ce7134 | ||
|
|
01f83e8451 | ||
|
|
ed92e9afb9 | ||
|
|
064890c0a2 | ||
|
|
8617f48fc2 | ||
|
|
2269cf0f08 | ||
|
|
3d41eb1ab4 | ||
|
|
cace523aa8 | ||
|
|
002f55dc04 | ||
|
|
49b6951ac3 | ||
|
|
7aa17e5ad6 | ||
|
|
9db41270f3 | ||
|
|
e4852c74ab | ||
|
|
613f84aa3c | ||
|
|
2814876976 | ||
|
|
69bf3999aa | ||
|
|
2fa3b26119 | ||
|
|
5a027c552e | ||
|
|
9efecf9514 | ||
|
|
8b87c43869 | ||
|
|
f7fec834e6 | ||
|
|
de43a1215c | ||
|
|
8dc531bb7f | ||
|
|
da2584d7ee | ||
|
|
373924a959 | ||
|
|
f75032bd79 | ||
|
|
4203dc5d41 | ||
|
|
9fe1f8ff90 | ||
|
|
bc825f760d | ||
|
|
191199d9de | ||
|
|
883a66a792 | ||
|
|
411e71b9a2 | ||
|
|
5463505787 | ||
|
|
ec6fc076de | ||
|
|
5a245bf362 | ||
|
|
d95912322c | ||
|
|
9c58e73b39 | ||
|
|
3c4ccd7d57 | ||
|
|
d22943d755 | ||
|
|
974b6590d8 | ||
|
|
d414c00b74 | ||
|
|
7b79d183eb | ||
|
|
edd56cb407 | ||
|
|
c777204f50 | ||
|
|
55f12f20c1 | ||
|
|
1ac062653d | ||
|
|
b0a176a22c | ||
|
|
f4b07cb518 | ||
|
|
a0ff78a810 | ||
|
|
f22391855b | ||
|
|
1fa18447e1 | ||
|
|
2d8c8c63c9 | ||
|
|
31d5e3151b | ||
|
|
c77bfe9da2 | ||
|
|
9a7dfc55e3 | ||
|
|
63bc3bd46e | ||
|
|
b5e5a3b2da | ||
|
|
7abe32be5c | ||
|
|
f0279a6866 | ||
|
|
5ce942c9a0 | ||
|
|
59d6cc7625 | ||
|
|
4abf806837 | ||
|
|
6bfb8cf2fd | ||
|
|
1d7f1082ea | ||
|
|
1c4aec83cb | ||
|
|
e537396fec | ||
|
|
944748a0ac | ||
|
|
023925d741 | ||
|
|
93db1254ec | ||
|
|
e0725ff139 | ||
|
|
9e96aee89f | ||
|
|
db6b660270 | ||
|
|
1808e5bccf | ||
|
|
5f1601a2da | ||
|
|
60ce6658ad | ||
|
|
e664652cc5 | ||
|
|
ca48a42701 | ||
|
|
00ea8ac4e1 | ||
|
|
de5238e89a | ||
|
|
3bb1327a65 | ||
|
|
71f77dd8fb | ||
|
|
9101dae38a | ||
|
|
1f18bf2bd8 | ||
|
|
41445c3092 | ||
|
|
2a3bf9821b | ||
|
|
819d775282 | ||
|
|
efbc6fe3ae | ||
|
|
262ba6c113 | ||
|
|
c41ecb6735 | ||
|
|
f5c32c6b98 | ||
|
|
a1886e37f8 | ||
|
|
f040804d02 | ||
|
|
e27625556c | ||
|
|
b13711ddef | ||
|
|
6ce82be46b | ||
|
|
56a5864600 | ||
|
|
07cbaa3e70 | ||
|
|
2aecea88b0 | ||
|
|
582c43fb6c | ||
|
|
cc7c2cc707 | ||
|
|
8b717c4f4c | ||
|
|
aa309af015 | ||
|
|
1e041b6249 | ||
|
|
1f240b02f4 | ||
|
|
6317f376b7 | ||
|
|
5ecf9aeed8 | ||
|
|
db36a76c2c | ||
|
|
3df8fb5fe9 | ||
|
|
7dd3367203 | ||
|
|
e72c82521a | ||
|
|
aca415db23 | ||
|
|
5ba2e8a7a1 | ||
|
|
07d4b126f5 | ||
|
|
98b2d8b3b9 | ||
|
|
f8ef5db5a3 | ||
|
|
6e14ec3227 | ||
|
|
028292a023 | ||
|
|
a38f2e36a2 | ||
|
|
a44cbe5972 | ||
|
|
2066a2e9bc | ||
|
|
f381bf85a4 | ||
|
|
44dcdcc8bb | ||
|
|
01220800f3 | ||
|
|
8ecb4cdcf4 | ||
|
|
8402d108c0 | ||
|
|
75af2d468e | ||
|
|
72ce37f008 | ||
|
|
e48e884286 | ||
|
|
f9d5c1f8de | ||
|
|
0873dcab0a | ||
|
|
a1343c2849 | ||
|
|
f732cc54d0 | ||
|
|
473a7d5fa4 | ||
|
|
8054a0b62f | ||
|
|
7f9cba5d37 | ||
|
|
5807fbf896 | ||
|
|
2c1dc6a18d | ||
|
|
f68e6387e6 | ||
|
|
f032001bee | ||
|
|
3bae591c04 | ||
|
|
18a1f0af94 | ||
|
|
afbd4a4716 | ||
|
|
a98da14c6f | ||
|
|
a2a70213a7 | ||
|
|
8b0f877041 | ||
|
|
bf7ad4cad2 | ||
|
|
66b659c0af | ||
|
|
8709ea948f | ||
|
|
7236c10403 | ||
|
|
dde703ec41 | ||
|
|
aa2e9b123c | ||
|
|
a3c06ce6e0 | ||
|
|
0e1dfb8ccb | ||
|
|
8a4a81a008 | ||
|
|
ff083942e8 | ||
|
|
737e04d09e | ||
|
|
0fe59efd72 | ||
|
|
ddb50e6254 | ||
|
|
72e6ae4186 | ||
|
|
4d510f643a | ||
|
|
66f607b5e6 | ||
|
|
1a125c62e7 | ||
|
|
84da44a27b | ||
|
|
5e35efcaef | ||
|
|
83f221c5e3 | ||
|
|
e86a10e9ba | ||
|
|
7eea328706 | ||
|
|
aa9dcec0ad | ||
|
|
e9bad2c7eb | ||
|
|
ce257a31bb | ||
|
|
c96b5cf4d7 | ||
|
|
fb40e9273d | ||
|
|
42c9af102b | ||
|
|
b7dff59542 | ||
|
|
266927aa9a | ||
|
|
0eee5747af | ||
|
|
80f2b7a1db | ||
|
|
dcaa8d4e96 | ||
|
|
0e2a449553 | ||
|
|
a70e035e02 | ||
|
|
4019da8ba9 | ||
|
|
5042ac1789 | ||
|
|
4031c9b978 | ||
|
|
65dd9bc286 | ||
|
|
e210dcb4df | ||
|
|
52a379229e | ||
|
|
93b2e91c10 | ||
|
|
b8afb01c46 | ||
|
|
59e7665b65 | ||
|
|
8a8a835a32 | ||
|
|
6ceab69656 | ||
|
|
9e98f05be0 | ||
|
|
5da1db91fd | ||
|
|
f0d58acd62 | ||
|
|
722ec00076 | ||
|
|
2db740e1ad | ||
|
|
70cd0e8c31 | ||
|
|
848c92ec25 | ||
|
|
98946d0a63 | ||
|
|
e0a39104b1 | ||
|
|
a4f66298a4 | ||
|
|
993bf50012 | ||
|
|
51bfc9a59b | ||
|
|
837795e87a | ||
|
|
d4820b2435 | ||
|
|
180537cd48 | ||
|
|
8bc77bbf18 | ||
|
|
474b606524 | ||
|
|
8a7e43ef42 | ||
|
|
7b5b486f0e | ||
|
|
ebedc02a0a | ||
|
|
ad42a3d956 | ||
|
|
98d75bc721 | ||
|
|
14fe68eb54 | ||
|
|
b6938c14ca | ||
|
|
3c96c1d5b1 | ||
|
|
2969ce2d56 | ||
|
|
bca92f5ee7 | ||
|
|
1c1982952e | ||
|
|
4b74b5a13d | ||
|
|
0fc00fac38 | ||
|
|
4446a7f060 | ||
|
|
9d31c478d3 | ||
|
|
b31e186d1d | ||
|
|
d02a3a0d3f | ||
|
|
a9e65cc83d | ||
|
|
010d505f04 | ||
|
|
95b9a3e1aa | ||
|
|
d051859371 | ||
|
|
af596c58c3 | ||
|
|
b4e3067718 | ||
|
|
40f8ae1cde | ||
|
|
f5f45a098e | ||
|
|
d429804c1f | ||
|
|
f5f770f401 | ||
|
|
91d6ed0ee7 | ||
|
|
06858b26c7 | ||
|
|
7e60a43f53 | ||
|
|
e7e82dcd0b | ||
|
|
66af6be063 | ||
|
|
69e1d2779d | ||
|
|
001752a81e | ||
|
|
1000041bce | ||
|
|
d50e791c30 | ||
|
|
7817d59989 | ||
|
|
139c4c1dd8 | ||
|
|
75bb6941d3 | ||
|
|
e92fb38271 | ||
|
|
e4eeef8f99 | ||
|
|
21c355bc9f | ||
|
|
04c878f57c | ||
|
|
ef23ce58d1 | ||
|
|
d213fa1b91 | ||
|
|
4b2804427e | ||
|
|
d707a59a71 | ||
|
|
923358b364 | ||
|
|
1d0c0ac19c | ||
|
|
7c97ecddac | ||
|
|
bcae51cc92 | ||
|
|
18896a69cf | ||
|
|
4c310d268d | ||
|
|
70babd9c32 | ||
|
|
15d0edce2e | ||
|
|
d7ab1774ac | ||
|
|
42d9607b0d | ||
|
|
bf82474fd7 | ||
|
|
64211275cc | ||
|
|
cf8060f7ff | ||
|
|
cee1d01d9a | ||
|
|
536d763a8e | ||
|
|
6d27b6ce41 | ||
|
|
0d05655e94 | ||
|
|
9cf697d72c | ||
|
|
f0f818c524 | ||
|
|
d2a3448819 | ||
|
|
1a25b15bce | ||
|
|
83808a63ed | ||
|
|
515d1d0dee | ||
|
|
2d47df96b6 | ||
|
|
ad4d71316f | ||
|
|
40932c9e84 | ||
|
|
59bee5f23e | ||
|
|
e6f00febea | ||
|
|
6ea694bc7e | ||
|
|
8f7a8edde4 | ||
|
|
11c7107152 | ||
|
|
d18a9f376f | ||
|
|
6ba95d8c70 | ||
|
|
1c00418d64 | ||
|
|
602e7fb530 | ||
|
|
e6a0a1d4a4 | ||
|
|
136cf143bd | ||
|
|
5afa2a23f6 | ||
|
|
513c81b508 | ||
|
|
45af66e000 | ||
|
|
b38f6c39e4 | ||
|
|
036874fbf0 | ||
|
|
9f3b49a7f5 | ||
|
|
9c35156db9 | ||
|
|
ab2edc6e59 | ||
|
|
9af278217c | ||
|
|
13dbc9c0fe | ||
|
|
14f7fbe794 | ||
|
|
a60e6aa860 | ||
|
|
220e72322c | ||
|
|
1a100dc54b | ||
|
|
04b95c986d | ||
|
|
8f6bb90945 | ||
|
|
50e78bd069 | ||
|
|
ce60e1a8f8 | ||
|
|
fb7ed19f7a | ||
|
|
970888b5ca | ||
|
|
8c934654ba | ||
|
|
ad3dc70cfd | ||
|
|
ae5949bc7f | ||
|
|
c7c563e68c | ||
|
|
3dac0af6c4 | ||
|
|
f9ce6966bb | ||
|
|
f61e23153b | ||
|
|
ff4f008a63 | ||
|
|
cbcf8a0a90 | ||
|
|
647e62059f | ||
|
|
6e3ef24e3a | ||
|
|
2559b27a6f | ||
|
|
421e2508d4 | ||
|
|
c9f8a93813 | ||
|
|
126849cf9a | ||
|
|
c08768f648 | ||
|
|
2c07257bf6 | ||
|
|
f797bbb97f | ||
|
|
a7f3b6e0dc | ||
|
|
60732c33c0 | ||
|
|
65b77e241f | ||
|
|
a167b95cec | ||
|
|
283cfd1ce9 | ||
|
|
e16db60d0f | ||
|
|
6a3b5bbe1d | ||
|
|
339c3f506c | ||
|
|
9fade70092 | ||
|
|
e18d84ae5e | ||
|
|
fa76e31640 | ||
|
|
2fd4fa25f7 | ||
|
|
c27d8e3b16 | ||
|
|
0841c52a75 | ||
|
|
669bff13c7 | ||
|
|
01ea4fa7a6 | ||
|
|
ef024b5118 | ||
|
|
a96a107ef9 | ||
|
|
8d3ab2be5d | ||
|
|
6ed407f656 | ||
|
|
6b3625f0ea | ||
|
|
db8e7f0474 | ||
|
|
d6398630df | ||
|
|
464bfccfb6 | ||
|
|
aa16da837d | ||
|
|
ce4478803a | ||
|
|
114239fe8e | ||
|
|
67d96061da | ||
|
|
2d5ad16399 | ||
|
|
4c7f79c6f8 | ||
|
|
2a50768db1 | ||
|
|
bff33e6992 | ||
|
|
448057a0b9 | ||
|
|
0a47669b14 | ||
|
|
b5893ce521 | ||
|
|
23537211b0 | ||
|
|
39f25db439 | ||
|
|
a551067c72 | ||
|
|
23f4df766c | ||
|
|
7354a34f1a | ||
|
|
2de48b3918 | ||
|
|
e823a7a7b0 | ||
|
|
ffc1f9d48e | ||
|
|
bb19bfb925 | ||
|
|
b3a2464249 | ||
|
|
09718e73a3 | ||
|
|
d5f20377ea | ||
|
|
f703d620c2 | ||
|
|
5ac680b37d | ||
|
|
9bf57e99fb | ||
|
|
b787f7cb11 | ||
|
|
7b35965dbf | ||
|
|
1618c1e677 | ||
|
|
0a5ce108b7 | ||
|
|
99c180441d | ||
|
|
79a7b0557c | ||
|
|
91cf192d2e | ||
|
|
66d7435ed6 | ||
|
|
19ea75b281 | ||
|
|
f0350b7045 | ||
|
|
9d4d3d0523 | ||
|
|
a76962e206 | ||
|
|
4060039440 | ||
|
|
c3a3a428a4 | ||
|
|
171e23bd09 | ||
|
|
6f4b7e0f1a | ||
|
|
ccf2cd3425 | ||
|
|
6ee444efd7 | ||
|
|
3e5be909a2 | ||
|
|
dac78f8f09 | ||
|
|
6b91e1b03c | ||
|
|
772295fc04 | ||
|
|
a4e93276b8 | ||
|
|
456a82acaa | ||
|
|
f2fb2cb363 | ||
|
|
2a7e5eecd7 | ||
|
|
79502f56a0 | ||
|
|
12dacfe2d2 | ||
|
|
d46337d694 | ||
|
|
f897cf745f | ||
|
|
cfa4f7da2e | ||
|
|
b21272cfab | ||
|
|
8ce0520101 | ||
|
|
4c17e1fd33 | ||
|
|
d5fb1f62f5 | ||
|
|
b34ede3795 | ||
|
|
4989d84693 | ||
|
|
2f210ab59f | ||
|
|
3ffea901f1 | ||
|
|
5e2e6520ce | ||
|
|
a8679c8eef | ||
|
|
469e842e96 | ||
|
|
cd945c625f | ||
|
|
1346192b75 | ||
|
|
45bebdd94e | ||
|
|
d0bbb025d3 | ||
|
|
8d0c53ef69 | ||
|
|
a59c2dfb10 | ||
|
|
9c7689f9b5 | ||
|
|
c580979fee | ||
|
|
e8e7bc95ea | ||
|
|
839e3e0833 | ||
|
|
4f451fab2f | ||
|
|
7d0413f41d | ||
|
|
bbac78195d | ||
|
|
20fef86b84 | ||
|
|
9581a8f1f4 | ||
|
|
58b4c36b6c | ||
|
|
15aaa5d9a1 | ||
|
|
b38332d061 | ||
|
|
4cfbbb6756 | ||
|
|
dec7db9e69 | ||
|
|
274be3bcfc | ||
|
|
d8a668ce60 | ||
|
|
a845ed1998 | ||
|
|
53bd147de2 | ||
|
|
4248b518a2 | ||
|
|
8f65a88d8b | ||
|
|
4d4a640f34 | ||
|
|
af9ead5937 | ||
|
|
ae3518f450 | ||
|
|
04e77a97f2 | ||
|
|
0ff3ce98e0 | ||
|
|
df2659787c | ||
|
|
d994673e91 | ||
|
|
dce781ef0e | ||
|
|
b0e2c73024 | ||
|
|
bda28f81cc | ||
|
|
d8dc3d48b1 | ||
|
|
5fdfa6339d | ||
|
|
9a799190ef | ||
|
|
8d32ba5b8e | ||
|
|
85a7f89ba9 | ||
|
|
17f5378326 | ||
|
|
9d67741310 | ||
|
|
ac6a106c6a | ||
|
|
d775b8baa0 | ||
|
|
2dc1b3ec43 | ||
|
|
d3d5160861 | ||
|
|
cdf8c3b6e5 | ||
|
|
29b1bbab9d | ||
|
|
c5d055c19b | ||
|
|
8f7b51a3df | ||
|
|
44e8cc810f | ||
|
|
b994878465 | ||
|
|
8d5d703cbe | ||
|
|
1c1936d8f8 | ||
|
|
7890fd5cc7 | ||
|
|
4bdf11eb79 | ||
|
|
81f4c7303f | ||
|
|
0641907ea0 | ||
|
|
ff0f32fcf5 | ||
|
|
768049ca36 | ||
|
|
88c8fe7cca | ||
|
|
5e7facfeb6 | ||
|
|
6e43467ef6 | ||
|
|
cd4ef8ae32 | ||
|
|
93a4463f22 | ||
|
|
56cf6bdaa4 | ||
|
|
fe02df27a2 | ||
|
|
a60ab68287 | ||
|
|
83e46ddc97 | ||
|
|
9c89ad7c72 | ||
|
|
0518abdedc | ||
|
|
93bcd07c7f | ||
|
|
5f1f4f8d81 | ||
|
|
e78120d9c6 | ||
|
|
cc5986d435 | ||
|
|
5192306b06 | ||
|
|
826935eb7d | ||
|
|
cd41bcf45c | ||
|
|
601375d06f | ||
|
|
c708abafc8 | ||
|
|
90d588353c | ||
|
|
2d4287df2a | ||
|
|
fde3a988b7 | ||
|
|
397164e667 | ||
|
|
a7824039e3 | ||
|
|
bb9247e821 | ||
|
|
4bc04ec031 | ||
|
|
8fd91e6c5c | ||
|
|
ee137d086a | ||
|
|
fcd81850cb | ||
|
|
aa4b07d024 | ||
|
|
16a096d288 | ||
|
|
b219b2a71b | ||
|
|
ce70242c35 | ||
|
|
dec01e2611 | ||
|
|
4373d89661 | ||
|
|
c73290eebb | ||
|
|
a022f9c2e1 | ||
|
|
5adb25a695 | ||
|
|
08c2972cf9 | ||
|
|
8132c7e676 | ||
|
|
8c68c9f703 | ||
|
|
000b6142fd | ||
|
|
c203ded1cd | ||
|
|
64c5c39b2a | ||
|
|
a8d2a1e371 | ||
|
|
9e5846b24a | ||
|
|
836ff9122c | ||
|
|
75d836358b | ||
|
|
bb11e7f90c | ||
|
|
cf6a71ab86 | ||
|
|
7f7288937d | ||
|
|
a38f9c2183 | ||
|
|
7ee880cadc | ||
|
|
9ae34d474b | ||
|
|
2817e2f2c8 | ||
|
|
02886a09d3 | ||
|
|
0b9d827ad6 | ||
|
|
ab3ea84d70 | ||
|
|
03a1c44659 | ||
|
|
53364444fd | ||
|
|
94040e8551 | ||
|
|
34d46ee28e | ||
|
|
59986d74f4 | ||
|
|
8afdf5ef65 | ||
|
|
fc64e57495 | ||
|
|
6dec5ea8f8 | ||
|
|
3f2aafcd28 | ||
|
|
7a3ee6b1a9 | ||
|
|
f7d9c62afb | ||
|
|
1a26a7f346 | ||
|
|
f6ff4405f8 | ||
|
|
14ceb0abc6 | ||
|
|
c9a3af6065 | ||
|
|
4e4a7fe84e | ||
|
|
989d25e2e2 | ||
|
|
f57e453d8d | ||
|
|
fcfa785ae9 | ||
|
|
3de688da29 | ||
|
|
5884d9d120 | ||
|
|
074ea5c719 | ||
|
|
e794446a54 | ||
|
|
1320a67e1f | ||
|
|
7c45177650 | ||
|
|
6b2d6ab57f | ||
|
|
04add8ebb5 | ||
|
|
ad8138fb04 | ||
|
|
6598e034c1 | ||
|
|
bba51c6a23 | ||
|
|
7d524300e5 | ||
|
|
a25a0031ff | ||
|
|
98b899c9c7 | ||
|
|
4424425dd4 | ||
|
|
85de3a09ea | ||
|
|
e82db1fecd | ||
|
|
76638b549f | ||
|
|
2936df6a02 | ||
|
|
27e3cf8ac5 | ||
|
|
adf1f8db8c | ||
|
|
9f319ab99a | ||
|
|
48a40f2511 | ||
|
|
7fbc75b375 | ||
|
|
65a54acd4f | ||
|
|
de96552f78 | ||
|
|
fee959940a | ||
|
|
48748bbc39 | ||
|
|
827138fc6a | ||
|
|
b7ee98dcd6 | ||
|
|
fa99df3ce7 | ||
|
|
d2ea6ed2fd | ||
|
|
d8d17e5c18 | ||
|
|
0519438292 | ||
|
|
09b8ff6b93 | ||
|
|
bd66dcb39e | ||
|
|
93504eaf7a | ||
|
|
9e116f481e | ||
|
|
bd95258176 | ||
|
|
a7e202bbc8 | ||
|
|
3f5f4f4bb1 | ||
|
|
4365db0fae | ||
|
|
82bb608fd3 | ||
|
|
ba0b3adf71 | ||
|
|
707e1f8b67 | ||
|
|
7f97e894a3 | ||
|
|
19118f7b84 | ||
|
|
63c46d1b21 | ||
|
|
4c3ff906ff | ||
|
|
6b77e1cce5 | ||
|
|
fdd8f102e6 | ||
|
|
2d0c195e46 | ||
|
|
f5a5fb765b | ||
|
|
f698cb66b8 | ||
|
|
fc51b16e4a | ||
|
|
0365d7a900 | ||
|
|
7a14376765 | ||
|
|
2167953b87 | ||
|
|
17352bfcf7 | ||
|
|
d4577aa7a6 | ||
|
|
e955c10170 | ||
|
|
9d4b6183b0 | ||
|
|
3c9bffb557 | ||
|
|
d4d7b546e1 | ||
|
|
9f0c17115e | ||
|
|
f6fdd3d12a | ||
|
|
2401da2b24 | ||
|
|
50ef8fef24 | ||
|
|
079248bfeb | ||
|
|
650ee9666d | ||
|
|
18bcc1bce7 | ||
|
|
73b4ec47b9 | ||
|
|
70331d913d | ||
|
|
9c72c9a063 | ||
|
|
af8ae09434 | ||
|
|
e8a8364ac2 | ||
|
|
0e7d49488c | ||
|
|
405c6659b9 | ||
|
|
7ca435337f | ||
|
|
024b665dd9 | ||
|
|
a397296624 | ||
|
|
adb9e38ff4 | ||
|
|
c4dc432be1 | ||
|
|
49795993f1 | ||
|
|
ea6b2ab2dc | ||
|
|
44d04d3ba6 | ||
|
|
577c171d48 | ||
|
|
c5edf7a581 | ||
|
|
ac67ec86b1 | ||
|
|
291f538645 | ||
|
|
817a3a5e15 | ||
|
|
0b87e192ce | ||
|
|
fc83ca0de6 | ||
|
|
c621e822dc | ||
|
|
1776452964 | ||
|
|
4e5e0a4a7a | ||
|
|
bb32f47b8c | ||
|
|
6b5f77c415 | ||
|
|
9a73c78705 | ||
|
|
8246c8e94b | ||
|
|
89e8ef65ea | ||
|
|
2428d39308 | ||
|
|
dc91f5004e | ||
|
|
b66bf4cc36 | ||
|
|
67910bce61 | ||
|
|
2adacdba02 | ||
|
|
4652e7d188 | ||
|
|
a30d4a4514 | ||
|
|
a17a9399ef | ||
|
|
ba6d10cd9e | ||
|
|
b1576e8f15 | ||
|
|
55de28105e | ||
|
|
40e3d59432 | ||
|
|
665f022111 | ||
|
|
309c10c4cb | ||
|
|
658120b8d9 | ||
|
|
fbd01fbfdb | ||
|
|
09135adadc | ||
|
|
0c7a36cef6 | ||
|
|
abf6fe69bd | ||
|
|
b3318583ed | ||
|
|
288fe20def | ||
|
|
96ee14bf08 | ||
|
|
00b9ba0f32 | ||
|
|
2a63f7b187 | ||
|
|
c655d97109 | ||
|
|
fd26700867 | ||
|
|
df5d9782e7 | ||
|
|
61af07bd79 | ||
|
|
0e008ac87b | ||
|
|
28eab870d4 | ||
|
|
86aedaf631 | ||
|
|
e242851260 | ||
|
|
c884fb7262 | ||
|
|
b6104fbdaf | ||
|
|
9305f1d483 | ||
|
|
048cd22d85 | ||
|
|
e1ea9664dc | ||
|
|
72d3863f8a | ||
|
|
49de53f1c9 | ||
|
|
b17e34351c | ||
|
|
84b1df75c2 | ||
|
|
89f122c325 | ||
|
|
e9103acb07 | ||
|
|
c7eae7e97a | ||
|
|
f5851d09f5 | ||
|
|
ba8112280d | ||
|
|
4cc373595c | ||
|
|
36aa4edb05 | ||
|
|
26840373ed | ||
|
|
4c5a2650d1 | ||
|
|
b99cdfb5c3 | ||
|
|
6ec14e744e | ||
|
|
310db66c22 | ||
|
|
c7c2b9a248 | ||
|
|
b697a8b40a | ||
|
|
c152b49b88 | ||
|
|
2356fc2bbf | ||
|
|
f7e2c8921c | ||
|
|
604352f8df | ||
|
|
fbbe56f954 | ||
|
|
7842c69ebb | ||
|
|
e1d50f1f27 | ||
|
|
9d0997624b | ||
|
|
355cd3e0e4 | ||
|
|
af5cd57094 | ||
|
|
8f85101cec | ||
|
|
97be483624 | ||
|
|
1ff5721912 | ||
|
|
80cbc663bf | ||
|
|
b3e729af0d | ||
|
|
3ed864021d | ||
|
|
8bc269fa99 | ||
|
|
c656c0f7e4 | ||
|
|
858e5d2d04 | ||
|
|
903a8db46f | ||
|
|
5aa731853d | ||
|
|
540096178c | ||
|
|
ed2a611197 | ||
|
|
1c61b0b5a2 | ||
|
|
82f7e5d0c4 | ||
|
|
ecb2c86dc4 | ||
|
|
5cbbc3b1ab | ||
|
|
e0b2e178f9 | ||
|
|
390974ba0f | ||
|
|
91379ff7d9 | ||
|
|
1fe9e56997 | ||
|
|
e10b853712 | ||
|
|
7d70862e72 | ||
|
|
90bdcec7ff | ||
|
|
63da350223 | ||
|
|
854f31cb7a | ||
|
|
eba6daf6c8 | ||
|
|
911d57b415 | ||
|
|
d91e0344a7 | ||
|
|
e9d7ee51f3 | ||
|
|
9053408e13 | ||
|
|
8f4094ee98 | ||
|
|
933e064079 | ||
|
|
274e08dd8b | ||
|
|
05c968edc2 | ||
|
|
270820de0b | ||
|
|
e049a97bec | ||
|
|
e2306ba0f3 | ||
|
|
6d0f488672 | ||
|
|
397f9a58cb | ||
|
|
4fc766a524 | ||
|
|
3f8b5c6c00 | ||
|
|
ce43be0d67 | ||
|
|
ff08888385 | ||
|
|
6b96410ea4 | ||
|
|
f21bd10f09 | ||
|
|
07b35f8ee1 | ||
|
|
251435ae0b | ||
|
|
b81bf90319 | ||
|
|
d6b1b8665d | ||
|
|
64bd3e6a52 | ||
|
|
520a0f91fd | ||
|
|
0015667829 | ||
|
|
35a7da2816 | ||
|
|
5bbf3fc49f | ||
|
|
7a8dfeb819 | ||
|
|
e28a24c875 | ||
|
|
823c6a6137 | ||
|
|
2cbe63f162 | ||
|
|
93d0746d3e | ||
|
|
8cb7c7378f | ||
|
|
3500c85e8d | ||
|
|
c699567c73 | ||
|
|
056c397e68 | ||
|
|
8431c7745d | ||
|
|
8df4607e50 | ||
|
|
edf9efddf9 | ||
|
|
03fc17fae6 | ||
|
|
6fb342a42c | ||
|
|
262347f8c8 | ||
|
|
1176b92073 | ||
|
|
afb084ebf8 | ||
|
|
12853feec3 | ||
|
|
d31876b65e | ||
|
|
ca521e7e51 | ||
|
|
f21c515d5c | ||
|
|
fa5259d1f2 | ||
|
|
7a5361c057 | ||
|
|
40bfb6fa88 | ||
|
|
61a237d1ae | ||
|
|
cf7276b528 | ||
|
|
67f69d0a7f | ||
|
|
a918914431 | ||
|
|
49f2c00d76 | ||
|
|
d284305323 | ||
|
|
c32ac8577c | ||
|
|
4ba1dd87e8 | ||
|
|
e0d82da6cb | ||
|
|
2179e4af47 | ||
|
|
0d5530f5ea | ||
|
|
a528249062 | ||
|
|
75d734d6e4 | ||
|
|
1d73affa08 | ||
|
|
834ee30c86 | ||
|
|
d236b8a94d | ||
|
|
a132cba7ef | ||
|
|
86706231e0 | ||
|
|
5eb921948a | ||
|
|
fd54a5f86c | ||
|
|
1b2ad5b419 | ||
|
|
14cd54668e | ||
|
|
2047089b64 | ||
|
|
ee924ef36e | ||
|
|
f9c1dd980b | ||
|
|
74c1861240 | ||
|
|
e8226782c1 | ||
|
|
a592cf3a05 | ||
|
|
0b1cb0d770 | ||
|
|
a4aa1cff3a | ||
|
|
6a02d2ffb6 | ||
|
|
7458181e90 | ||
|
|
ec28cdc936 | ||
|
|
9dbc9a8a56 | ||
|
|
73d4a2d414 | ||
|
|
e93ad8b800 | ||
|
|
d48985ca6d | ||
|
|
5f7d717b63 | ||
|
|
e266698e68 | ||
|
|
e8098e795c | ||
|
|
8d69724272 | ||
|
|
683114f916 | ||
|
|
3b454470f9 | ||
|
|
f40f83bb09 | ||
|
|
932bec2f84 | ||
|
|
820820eb92 | ||
|
|
6f5835a2b8 | ||
|
|
07128bb5e6 | ||
|
|
b302372d4c | ||
|
|
cad7d2c735 | ||
|
|
e4c3f8bce2 | ||
|
|
1fbf4e292a | ||
|
|
62f5b2d06d | ||
|
|
3831658162 | ||
|
|
b2545e4de0 | ||
|
|
26e43f5e54 | ||
|
|
44ba5c32c6 | ||
|
|
ce7d3a89c0 | ||
|
|
c74b1205df | ||
|
|
58dd16219b | ||
|
|
e0e1d0238f | ||
|
|
71e22bdb21 | ||
|
|
3c2025ab44 | ||
|
|
d314e22970 | ||
|
|
ff7181b648 | ||
|
|
4cbd30fdbc | ||
|
|
4ce7480df8 | ||
|
|
d2aede593b | ||
|
|
32fb5ad0e2 | ||
|
|
fa55ae1fcc | ||
|
|
90fbe77682 | ||
|
|
26a7609875 | ||
|
|
77a07f6628 | ||
|
|
42c8b2f989 | ||
|
|
3861926ff7 |
58
.github/workflows/build.yml
vendored
Normal file
58
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Build
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install environment
|
||||
run: |
|
||||
python -m venv ./venv
|
||||
|
||||
- name: Install dependencies
|
||||
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
|
||||
|
||||
- name: mypy game
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy game
|
||||
|
||||
- name: mypy gen
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy gen
|
||||
|
||||
- name: mypy theater
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy theater
|
||||
|
||||
- name: update build number
|
||||
run: |
|
||||
[IO.File]::WriteAllLines($pwd.path + "\resources\buildnumber", $env:GITHUB_RUN_NUMBER)
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
$env:PYTHONPATH=".;./pydcs"
|
||||
pyinstaller pyinstaller.spec
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dcs_liberation
|
||||
path: dist/
|
||||
126
.github/workflows/release.yml
vendored
Normal file
126
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
name: Release Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [ '*' ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install environment
|
||||
run: |
|
||||
python -m venv ./venv
|
||||
|
||||
- name: Install dependencies
|
||||
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
|
||||
|
||||
- name: Finalize version
|
||||
run: |
|
||||
New-Item -ItemType file resources\final
|
||||
|
||||
- name: mypy game
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy game
|
||||
|
||||
- name: mypy gen
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy gen
|
||||
|
||||
- name: mypy theater
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy theater
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
$env:PYTHONPATH=".;./pydcs"
|
||||
pyinstaller pyinstaller.spec
|
||||
|
||||
- name: Create Installer
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref }}
|
||||
run: |
|
||||
$version = ($env:TAG_NAME -split "/") | Select-Object -Last 1
|
||||
(Get-Content .\installer\dcs_liberation.iss) -replace "{{version}}",$version | Out-File .\build\installer.iss
|
||||
cd .\installer
|
||||
iscc.exe ..\build\installer.iss
|
||||
cd ..
|
||||
Copy-Item .\changelog.md .\dist
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dcs_liberation
|
||||
path: dist/
|
||||
|
||||
release:
|
||||
needs: [ build ]
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dcs_liberation
|
||||
|
||||
- name: "Get Version"
|
||||
id: version
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref }}
|
||||
run: |
|
||||
Get-ChildItem -Recurse -Depth 1
|
||||
$version = ($env:TAG_NAME -split "/") | Select-Object -Last 1
|
||||
$prerelease = ("2.1.1-alpha3" -match '[^\.\d]').ToString().ToLower()
|
||||
Write-Host $version
|
||||
Write-Host $prerelease
|
||||
Write-Output "::set-output name=number::$version"
|
||||
Write-Output "::set-output name=prerelease::$prerelease"
|
||||
$changelog = Get-Content .\changelog.md
|
||||
$last_change = ($changelog | Select-String -Pattern "^#\s" | Select-Object -Skip 1 -First 1).LineNumber - 2
|
||||
($changelog | Select-Object -First $last_change) -join "`n" | Out-File .\releasenotes.md
|
||||
Compress-Archive -Path .\dcs_liberation -DestinationPath "dcs_liberation.$version.zip" -Compression Optimal
|
||||
|
||||
- uses: actions/create-release@v1
|
||||
id: create_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
body_path: releasenotes.md
|
||||
draft: false
|
||||
prerelease: ${{ steps.version.outputs.prerelease }}
|
||||
|
||||
- uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./dcs_liberation.exe
|
||||
asset_name: dcs_liberation.${{ steps.version.outputs.number }}.exe
|
||||
asset_content_type: application/exe
|
||||
|
||||
- uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./dcs_liberation.${{ steps.version.outputs.number }}.zip
|
||||
asset_name: dcs_liberation.${{ steps.version.outputs.number }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
24
.gitignore
vendored
24
.gitignore
vendored
@@ -1,12 +1,22 @@
|
||||
*.pyc
|
||||
__pycache__
|
||||
build/*
|
||||
build/**
|
||||
resources/payloads/*.lua
|
||||
venv
|
||||
logs.txt
|
||||
.DS_Store
|
||||
dist/**
|
||||
a.py
|
||||
resources/tools/a.miz
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
.idea/misc.xml
|
||||
.idea/*.iml
|
||||
.idea/
|
||||
|
||||
/kneeboards
|
||||
/liberation_preferences.json
|
||||
/state.json
|
||||
|
||||
logs/
|
||||
|
||||
qt_ui/logs/liberation.log
|
||||
|
||||
*.psd
|
||||
|
||||
5
.gitmodules
vendored
5
.gitmodules
vendored
@@ -1,3 +1,4 @@
|
||||
[submodule "submodules/dcs"]
|
||||
path = submodules/dcs
|
||||
[submodule "pydcs"]
|
||||
path = pydcs
|
||||
url = https://github.com/pydcs/dcs
|
||||
branch = master
|
||||
|
||||
10
.idea/inspectionProfiles/Project_Default.xml
generated
10
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,10 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/dcs_pmcliberation.iml" filepath="$PROJECT_DIR$/.idea/dcs_pmcliberation.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
11
.idea/vcs.xml
generated
11
.idea/vcs.xml
generated
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitSharedSettings">
|
||||
<option name="FORCE_PUSH_PROHIBITED_PATTERNS">
|
||||
<list />
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
30
.vscode/launch.json
vendored
Normal file
30
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Main",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "qt_ui\\main.py",
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"PYTHONPATH": ".;./pydcs"
|
||||
},
|
||||
"preLaunchTask": "Prepare Environment"
|
||||
},
|
||||
{
|
||||
"name": "Python: Make Release",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "resources\\tools\\mkrelease.py",
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"PYTHONPATH": ".;./pydcs"
|
||||
},
|
||||
"preLaunchTask": "Prepare Environment"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
.vscode/tasks.json
vendored
Normal file
35
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Prepare Environment",
|
||||
"type": "shell",
|
||||
"isBackground": false,
|
||||
"problemMatcher": [],
|
||||
"command": "powershell",
|
||||
"args": [
|
||||
"-Command",
|
||||
"& {if (-not (Test-Path ${workspaceFolder}\\venv)) { python -m venv ${workspaceFolder}\\venv; . ${workspaceFolder}\\venv\\scripts\\activate.ps1; pip install -r requirements.txt; Copy-Item ${workspaceFolder}\\venv\\Lib\\site-packages\\shiboken2\\shiboken2.abi3.dll ${workspaceFolder}\\venv\\Lib\\site-packages\\PySide2 } }",
|
||||
],
|
||||
"group": "build",
|
||||
"options": {
|
||||
"env": {
|
||||
"PYTHONPATH": ".;./pydcs"
|
||||
}
|
||||
},
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "never",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": true,
|
||||
"clear": false
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "folderOpen"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
40
README.md
40
README.md
@@ -1,6 +1,40 @@
|
||||
WIP [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) single-player liberation dynamic campaign.
|
||||

|
||||
|
||||
Inspired by *ARMA Liberation* mission.
|
||||
[](https://www.paypal.com/paypalme/KhopaDCSL)
|
||||
[](https://patreon.com/khopa)
|
||||
|
||||
Uses [pydcs](http://github.com/pydcs/dcs) for mission generation.
|
||||
[](https://github.com/Khopa/dcs_liberation/releases)
|
||||
|
||||
[](https://discord.gg/bKrtrkJ)
|
||||
|
||||
[](https://github.com/Khopa/dcs_liberation)
|
||||
[](https://github.com/Khopa/dcs_liberation/issues)
|
||||

|
||||
|
||||
## About DCS Liberation
|
||||
DCS Liberation 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.
|
||||
|
||||

|
||||
|
||||
## Downloads
|
||||
|
||||
Latest release is available here : https://github.com/Khopa/dcs_liberation/releases
|
||||
|
||||
## Resources
|
||||
|
||||
Tutorials, contributors and developer's guides are available in the project's [Wiki](https://github.com/Khopa/dcs_liberation/wiki/)
|
||||
|
||||
## Special Thanks
|
||||
|
||||
First, a big thanks to shdwp, for starting the original DCS Liberation project.
|
||||
|
||||
Then, DCS Liberation uses [pydcs](http://github.com/pydcs/dcs) for mission generation, and nothing would be possible without this.
|
||||
It also uses the popular [Mist](https://github.com/mrSkortch/MissionScriptingTools) lua framework for mission scripting.
|
||||
|
||||
Excellent lua scripts DCS Liberation uses as plugins:
|
||||
|
||||
* For the JTAC feature, DCS Liberation embeds Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
|
||||
* Walder's [Skynet-IADS](https://github.com/walder/Skynet-IADS) is used for Integrated Air Defense System.
|
||||
|
||||
Please also show some support to these projects !
|
||||
|
||||
65
__init__.py
65
__init__.py
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import dcs
|
||||
|
||||
import theater.caucasus
|
||||
import theater.persiangulf
|
||||
import theater.nevada
|
||||
|
||||
import ui.window
|
||||
import ui.mainmenu
|
||||
import ui.newgamemenu
|
||||
import ui.corruptedsavemenu
|
||||
|
||||
from game.game import Game
|
||||
from theater import start_generator
|
||||
from userdata import persistency
|
||||
|
||||
dcs.planes.FlyingType.payload_dirs.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), "resources\\payloads"))
|
||||
|
||||
|
||||
def proceed_to_main_menu(game: Game):
|
||||
m = ui.mainmenu.MainMenu(w, None, game)
|
||||
m.display()
|
||||
|
||||
|
||||
w = ui.window.Window()
|
||||
try:
|
||||
game = persistency.restore_game()
|
||||
if not game:
|
||||
new_game_menu = None # type: NewGameMenu
|
||||
|
||||
def start_new_game(player_name: str, enemy_name: str, terrain: str, sams: bool, midgame: bool, multiplier: float):
|
||||
if terrain == "persiangulf":
|
||||
conflicttheater = theater.persiangulf.PersianGulfTheater()
|
||||
elif terrain == "nevada":
|
||||
conflicttheater = theater.nevada.NevadaTheater()
|
||||
else:
|
||||
conflicttheater = theater.caucasus.CaucasusTheater()
|
||||
|
||||
if midgame:
|
||||
for i in range(0, int(len(conflicttheater.controlpoints) / 2)):
|
||||
conflicttheater.controlpoints[i].captured = True
|
||||
|
||||
start_generator.generate_initial(conflicttheater, enemy_name, sams, multiplier)
|
||||
game = Game(player_name=player_name,
|
||||
enemy_name=enemy_name,
|
||||
theater=conflicttheater)
|
||||
game.budget = int(game.budget * multiplier)
|
||||
game.settings.multiplier = multiplier
|
||||
game.settings.sams = sams
|
||||
|
||||
if midgame:
|
||||
game.budget = game.budget * 4 * len(list(conflicttheater.conflicts()))
|
||||
|
||||
proceed_to_main_menu(game)
|
||||
|
||||
new_game_menu = ui.newgamemenu.NewGameMenu(w, start_new_game)
|
||||
new_game_menu.display()
|
||||
else:
|
||||
proceed_to_main_menu(game)
|
||||
except Exception as e:
|
||||
ui.corruptedsavemenu.CorruptedSaveMenu(w).display()
|
||||
|
||||
w.run()
|
||||
|
||||
338
changelog.md
Normal file
338
changelog.md
Normal file
@@ -0,0 +1,338 @@
|
||||
# 2.2.1
|
||||
|
||||
# Features/Improvements
|
||||
* **[Factions]** Added factions : Georgia 2008, USN 1985, France 2005 Frenchpack by HerrTom
|
||||
* **[Factions]** Added map Persian Gulf full by Plob
|
||||
* **[Flight Planner]** Player flights with start delays under ten minutes will spawn immediately.
|
||||
* **[UI]** Mission start screen now informs players about delayed flights.
|
||||
* **[Units]** Added support for F-14A-135-GR
|
||||
* **[Modding]** Possible to setup liveries overrides in factions definition files
|
||||
|
||||
## Fixes :
|
||||
* **[Flight Planner]** Hold, join, and split points are planned cautiously near enemy airfields. Ascend/descend points are no longer planned.
|
||||
* **[Flight Planner]** Custom waypoints are usable again. Not that in most cases custom flight plans will revert to the 2.1 flight planning behavior.
|
||||
* **[Flight Planner]** Fixed UI bug that made it possible to create empty flights which would throw an error.
|
||||
* **[Flight Planner]** Player flights from carriers will now be delayed correctly according to the player's settings.
|
||||
* **[Misc]** Spitfire variant with clipped wings was not seen as flyable by DCS Liberation (hence could not be setup as client/player slot)
|
||||
* **[Misc]** Updated Syria terrain parking slots database, the out-of-date database could end up generating aircraft in wrong slots (We are still experiencing issues with somes airbases, such as Khalkhalah though)
|
||||
|
||||
# 2.2.0
|
||||
|
||||
## Features/Improvements :
|
||||
* **[Campaign Generator]** Added early warning radar generation
|
||||
* **[Campaign Generator]** Added scud launcher sites
|
||||
* **[Cheat Menu]** Added ability to capture base from mission planner
|
||||
* **[Cheat Menu]** Added ability to show red ATO
|
||||
* **[Factions]** Added WW2 factions that do not depend on WW2 asset pack
|
||||
* **[Factions]** Cold War / Middle eastern factions will use Flak sites
|
||||
* **[Flight Planner]** Flight planner overhaul, with package and TOT system
|
||||
* **[Flight Planner]** Pick runways and ascent/descent based on headwind
|
||||
* **[Map]** Added polygon debug mode display
|
||||
* **[Map]** Highlight the selected flight path on the map
|
||||
* **[Map]** Improved SAM display settings
|
||||
* **[Map]** Improved flight plan display settings
|
||||
* **[Map]** Caucasus and The Channel map use a new system to generate SAM and strike target location to reduce probability of targets generated in the middle of a forests
|
||||
* **[Misc]** Flexible Dedicated Hosting Options for Mission Files via environment variables
|
||||
* **[Moddability]** Custom campaigns can be designed through json files
|
||||
* **[Moddability]** LUA plugins can now be injected into Liberation missions.
|
||||
* **[Moddability]** Optional Skynet IADS lua plugin now included
|
||||
* **[New Game]** Starting budget can be freely selected
|
||||
* **[New Game]** Exanded information for faction and campaign selection in the new game wizard
|
||||
* **[UI]** Add double and right click actions to many UI elements.
|
||||
* **[UI]** Add polygon drawing mode for map background
|
||||
* **[UI]** Added a warning if you press takeoff with no player enabled flights
|
||||
* **[UI]** Packages and flights now visible in the main window sidebar
|
||||
* **[Units/Factions]** Added bombers to some coalitions
|
||||
* **[Units/Factions]** Added support for SU-57 mod by Cubanace
|
||||
* **[Units]** Added Freya EWR sites to german WW2 factions
|
||||
* **[Units]** Added support for many bombers (B-52H, B-1B, Tu-22, Tu-142)
|
||||
* **[Units]** Added support for new P-47 variants
|
||||
|
||||
## Fixes :
|
||||
* **[Campaign Generator]** Big airbases could end up without any airbase defense.
|
||||
* **[Campaign generator]** Ship group and offshore buildings should not be generated on land anymore
|
||||
* **[Flight Planner]** Fix waypoint alitudes for helicopters
|
||||
* **[Flight Planner]** Fixed CAS aircraft wandering away from frontline
|
||||
* **[Maps]** Incirlik airbase was missing exclusions zones, so SAMS could end up being generated on the runway
|
||||
* **[Mission Generator]** Fixed player/client confusion when a flight had only one player slot.
|
||||
* **[Radios]** Fix A-10C radio
|
||||
* **[UI]** Many missing unit icons were added
|
||||
* **[UI]** Missing TER weapons in custom payload now selectable.
|
||||
|
||||
# 2.1.5
|
||||
|
||||
## Features/Improvements :
|
||||
* **[Units/Factions]** Enabled EPLRS for ground units that supports it (so they appear on A-10C II TAD and Helmet)
|
||||
|
||||
## Fixes :
|
||||
* **[UI]** Fixed an issue that prevent saving after aborting a mission
|
||||
* **[Mission Generator]** Fixed aircraft landing point type being wrong
|
||||
|
||||
# 2.1.4
|
||||
|
||||
## Fixes :
|
||||
* **[UI]** Fixed an issue that prevented generating the mission (take off button no working) on old savegames.
|
||||
|
||||
## Features/Improvements :
|
||||
* **[Units/Factions]** Added A-10C_2 to USA 2005 and Bluefor modern factions
|
||||
* **[UI]** Limit number of aircraft that can be bought to the number of available parking slots.
|
||||
* **[Mission Generator]** Use inline loading of the JSON.lua library, and save to either %LIBERATION_EXPORT_DIR%, or to DCS working directory
|
||||
|
||||
## Changes :
|
||||
* **[Units/Factions]** Bluefor generic factions will now use the new "Combined Joint Task Forces Blue" country in the generated mission instead of "USA"
|
||||
|
||||
## Fixes :
|
||||
* **[UI]** Fixed icon for Viggen
|
||||
* **[UI]** Added icons for some ground units
|
||||
* **[Misc]** Fixed issue with Chinese characters in pydcs preventing generating the mission. (Take Off button not working) (thanks to spark135246)
|
||||
* **[Misc]** Fixed an error causing with ATC frequency preventing generating the mission. (Take Off button not working) (thanks to danalbert)
|
||||
|
||||
# 2.1.2
|
||||
|
||||
## Fixes :
|
||||
* **[Mission Generator]** Fix mission generation issues with radio frequencies (Thanks to contributors davidp57 and danalbert)
|
||||
* **[Mission Generator]** AI should now properly plan flights for Tornados
|
||||
|
||||
# 2.1.1
|
||||
|
||||
## Features/Improvements :
|
||||
* **[Other]** Added an installer option (thanks to contributor parithon)
|
||||
* **[Kneeboards]** Generate mission kneeboards for player flights. Kneeboards include
|
||||
airfield/carrier information (ATC frequencies, ILS, TACAN, and runway
|
||||
assignments), assigned radio channels, waypoint lists, and AWACS/JTAC/tanker
|
||||
information. (Thanks to contributor danalbert)
|
||||
* **[Radios]** Allocate separate intra-flight channels for most aircraft to reduce global
|
||||
chatter. (Thanks to contributor danalbert)
|
||||
* **[Radios]** Configure radio channel presets for most aircraft. Currently supported are:
|
||||
* AJS37
|
||||
* AV-8B
|
||||
* F-14B
|
||||
* F-16C
|
||||
* F/A-18C
|
||||
* JF-17
|
||||
* M-2000C (Thanks to contributor danalbert)
|
||||
* **[Base Menu]** Added possibility to repair destroyed SAM and base defenses units for the player (Click on a SAM site to fix it)
|
||||
* **[Base Menu]** Added possibility to buy/sell/replace SAM units
|
||||
* **[Map]** Added recon images for buildings on strike targets, click on a Strike target to get detailled informations
|
||||
* **[Units/Factions]** Added F-16C to USA 1990
|
||||
* **[Units/Factions]** Added MQ-9 Reaper as CAS unit for USA 2005
|
||||
* **[Units/Factions]** Added Mig-21, Mig-23, SA-342L to Syria 2011
|
||||
* **[Cheat Menu]** Added buttons to remove money
|
||||
|
||||
## Fixed issues :
|
||||
* **[UI/UX]** Spelling issues (Thanks to contributor steveveepee)
|
||||
* **[Campaign Generator]** LHA was placed on land in Syrian Civil War campaign
|
||||
* **[Campaign Generator]** Fixed inverted configuration for Syria full map
|
||||
* **[Campaign Generator]** Syria "Inherent Resolve" campaign, added Incirlik Air Base
|
||||
* **[Mission Generator]** AH-1W was not used by AI to generate CAS mission by default
|
||||
* **[Mission Generator]** Fixed F-16C targeting pod not being added to payload
|
||||
* **[Mission Generator]** AH-64A and AH-64D payloads fix.
|
||||
* **[Units/Factions]** China will use KJ-2000 as awacs instead of A-50
|
||||
|
||||
# 2.1.0
|
||||
|
||||
## Features/Improvements :
|
||||
|
||||
* **[Campaign Generator]** Added Syria map
|
||||
* **[Campaign Generator]** Added 5 campaigns for the Syria map
|
||||
* **[Campaign Generator]** Added 2 small scale campaign for Persian Gulf map
|
||||
* **[Units/Factions]** Added factions for Syria map : Syria 2011, Arab Armies 1982, 1973, 1968, 1948, Israel 1982, 1973, 1948
|
||||
* **[Base Menu]** Budget is visible in recruitment menu. (Thanks to Github contributor root0fall)
|
||||
* **[Misc]** Added error message in mission when the state file can not be written
|
||||
* **[Units/Factions]** China, Pakistan, UAE will now use the new WingLoong drone as JTAC instead of the MQ-9 Reaper
|
||||
* **[Units/Factions]** Minor changes to Syria 2011 and Turkey 2005 factions
|
||||
* **[UI]** Version number is shown in about dialog
|
||||
|
||||
## Fixed issues :
|
||||
|
||||
* **[Mission Generator]** Caucasus terrain improvement on exclusions zone (added forests between Vaziani and Beslan to exclusion zones)
|
||||
* **[Mission Generator]** The first unit of every base defenses group could not be controlled with Combined Arms.
|
||||
* **[Mission Generator]** Reduced generated helicopter altitude for CAS missions
|
||||
* **[Mission Generator]** F-16C default CAS payload was asymmetric, fixed.
|
||||
* **[Mission Generator]** AH-1W couldn't be bought, and added default payloads.
|
||||
* **[UI/UX]** Fixed Mi-28N missing thumbnail
|
||||
* **[UI/UX]** Fixed list of flights not refreshing when changing the mission departure (T+).
|
||||
|
||||
# 2.0.11
|
||||
|
||||
## Features/Improvements :
|
||||
|
||||
* **[Units/Factions]** Added Mig-31, Su-30, Mi-24V, Mi-28N to Russia 2010 faction.
|
||||
* **[Units/Factions]** Added F-15E to USA 2005 and USA 1990 factions.
|
||||
* **[Mission Generator]** Added a parameter to choose whether the JTACs should use smoke markers or not
|
||||
|
||||
## Fixed issues :
|
||||
|
||||
* **[Units/Factions]** Fixed big performance issue in new release UI that occurred only when running the .exe
|
||||
* **[Units/Factions]** Fixed mission generation not working with Libya faction
|
||||
* **[Units/Factions]** Fixed OH-58D not being used by AI
|
||||
* **[Units/Factions]** Typo in UK 1990 name (fixed by bwRavencl)
|
||||
* **[Units/Factions]** Fixed Tanker Tacan channel not being the same as the briefing one. (Sorry)
|
||||
* **[Mission Generator]** Neutral airbases services will now be disabled. (Not possible to refuel or re-arm there)
|
||||
* **[Mission Generator]** AI will be configured to limit afterburner usage
|
||||
* **[Mission Generator]** JTAC will not use laser codes above 1688 anymore
|
||||
* **[Mission Generator]** JTAC units were misconfigured and would not be invisible/immortal.
|
||||
* **[Mission Generator]** Increased JTAC status message duration to 25s, so you have more time to enter coordinates;
|
||||
* **[Mission Generator]** Destroyed units carcass will not appear on airfields to avoid having a destroyed vehicle blocking a runway or taxiway.
|
||||
|
||||
|
||||
# 2.0.10
|
||||
|
||||
## Features/Improvements :
|
||||
* **[Misc]** Now possible to save game in a different file, and to open DCS Liberation savegame files. (You are not restricted to a single save file anymore)
|
||||
* **[UI/UX]** New dark UI Theme and default theme improvement by Deus
|
||||
* **[UI/UX]** New "satellite" map backgrounds
|
||||
* **[UX]** Base menu is opened with a single mouse click
|
||||
* **[Units/Factions/Mods]** Added Community A-4E-C support for faction Bluefor Cold War
|
||||
* **[Units/Factions/Mods]** Added MB-339PAN support for faction Bluefor Cold War
|
||||
* **[Units/Factions/Mods]** Added Rafale AI mod support
|
||||
* **[Units/Factions/Mods]** Added faction "France Modded" with units from frenchpack v3.5 mod
|
||||
* **[Units/Factions/Mods]** Added faction "Insurgent modded" with Insurgent units from frenchpack v3.5 mod (Toyota truck)
|
||||
* **[Units/Factions/Mods]** Added factions Canada 2005, Australia 2005, Japan 2005, USA Aggressors, PMC
|
||||
* **[New Game Wizard]** Added the list of required mods for modded factions.
|
||||
* **[New Game Wizard]** No more RED vs BLUE opposing faction restrictions.
|
||||
* **[New Game Wizard]** New campaign generation settings added : No aircraft carrier, no lha, no navy, invert map starting positions.
|
||||
* **[Mission Generator]** Artillery units will start firing mission after a random delay. It should reduces lag spikes induced by artillery strikes by spreading them out.
|
||||
* **[Mission Generator]** Ground units will retreat after taking too much casualties. Artillery units will retreat if engaged.
|
||||
* **[Mission Generator]** The briefing will now contain the carrier ATC frequency
|
||||
* **[Mission Generator]** The briefing contains a small situation update.
|
||||
* **[Mission Generator]** Previously destroyed units are visible in the mission. (And added a performance settings to disable this behaviour)
|
||||
* **[Mission Generator]*c* Basic JTAC on Frontlines
|
||||
* **[Campaign Generator]** Added Tarawa in caucasus campaigns
|
||||
* **[Campaign Generator]** Tuned the various existing campaign parameters
|
||||
* **[Campaign Generator]** Added small campaign : "Russia" on Caucasus Theater
|
||||
|
||||
## Fixed issues :
|
||||
* **[Mission Generator]** Carrier will sail into the wind, not in the same direction
|
||||
* **[Mission Generator]** Carrier cold start was not working (flight was starting warm even when cold was selected)
|
||||
* **[Mission Generator]** Carrier group ships are more spread out
|
||||
* **[Mission Generator]** Fixed wrong radio frequency for german WW2 warbirds
|
||||
* **[Mission Generator]** Fixed FW-190A8 spawning with bomb rack for CAP missions
|
||||
* **[Mission Generator]** Fixed A-20G spawning with no payload
|
||||
* **[Mission Generator]** Fixed Su-33 spawning too heavy to take off from carrier
|
||||
* **[Mission Generator]** Fixed Harrier AV-8B spawning too heavy to take off from tarawa
|
||||
* **[Mission Generator]** Base defense units were not controllable with Combined Arms
|
||||
* **[Mission Generator]** Tanker speed was too low
|
||||
* **[Mission Generator]** Tanker TACAN settings were wrong
|
||||
* **[Mission Generator]** AI aircraft should start datalink ON (EPLRS)
|
||||
* **[Mission Generator]** Base defense units should not spawn on runway and or taxyway. (The chance for this to happen should now be really really low)
|
||||
* **[Mission Generator]** Fixed all flights starting "In flight" after playing a few missions (parking slot reset issue)
|
||||
* **[Mission Script/Performance]** Mission lua script will not listen to weapons fired event anymore and register every fired weapons. This should improve performance especially in WW2 scenarios or when rocket artillery is firing.
|
||||
* **[Campaign Generator]** Carrier name will now not appear for faction who do not have carriers
|
||||
* **[Campaign Generator]** SA-10 sites will now have a tracking radar.
|
||||
* **[Units/Factions]** Remove JF-17 from USA 2005 faction
|
||||
* **[Units/Factions]** Remove AJS-37 from Russia 2010
|
||||
* **[Units/Factions]** Removed Oliver Hazard Perry from cold war factions (too powerful sam system for the era)
|
||||
* **[Bug]** On the persian gulf full map campaign, the two carriers were sharing the same id, this was causing a lot of bugs
|
||||
* **[Performance]** Tuned the culling setting so that you cannot run into situation where no friendly or enemy AI flights are generated
|
||||
* **[Other]** Application doesn't gracefully exit.
|
||||
* **[Other]** Other minor fixes, and multiples factions small changes
|
||||
|
||||
# 2.0 RC 9
|
||||
|
||||
## Features/Improvements :
|
||||
* **[UI/UX]** New icons from contributor Deus
|
||||
|
||||
## Fixed issues :
|
||||
* **[Mission Generator]** Carrier TACAN was wrongfully set up as an A/A TACAN
|
||||
* **[Campaign Generator]** Fixed issue with Russian navy group generator causing a random crash on campaign creation.
|
||||
|
||||
# 2.0 RC 8
|
||||
|
||||
## Fixed issues :
|
||||
* **[Mission Generator]** Frequency for P-47D-30 changed to 124Mhz (Generated mission with 251Mhz would not work)
|
||||
* **[Mission Generator]** Reduced the maximum number of uboat per generated group
|
||||
* **[Mission Generator]** Fixed an issue with the WW2 LST groups (superposed units).
|
||||
* **[UI]** Fixed issue with the zoom
|
||||
|
||||
# 2.0 RC 7
|
||||
|
||||
## Features/Improvements :
|
||||
|
||||
* **[Units/Factions]** Added P-47D-30 for factions allies_1944
|
||||
* **[Units/Factions]** Added factions : Bluefor Coldwar, Germany 1944 Easy
|
||||
|
||||
* **[Campaign/Map]** Added a campaign in the Channel map
|
||||
* **[Campaign/Map]** Changed the Normandy campaign map
|
||||
* **[Campaign/Map]** Added new campaign Normandy Small
|
||||
|
||||
* **[Mission Generator]** AI Flight generator has been reworked
|
||||
* **[Mission Generator]** Add PP points for JF-17 on STRIKE missions
|
||||
* **[Mission Generator]** Add ST point for F-14B on STRIKE missions
|
||||
* **[Mission Generator]** Flights with client slots will never be delayed
|
||||
* **[Mission Generator]** AI units can start from parking (With a new setting in Settings Window to disable it)
|
||||
* **[Mission Generator]** Tacan for carrier will only be in Mode X from now
|
||||
* **[Mission Generator]** RTB waypoints for autogenerated flights
|
||||
|
||||
* **[Flight Planner]** Added CAS mission generator
|
||||
* **[Flight Planner]** Added CAP mission generator
|
||||
* **[Flight Planner]** Added SEAD mission generator
|
||||
* **[Flight Planner]** Added STRIKE mission generator
|
||||
* **[Flight Planner]** Added buttons to add autogenerated waypoints (ASCEND, DESCEND, RTB)
|
||||
* **[Flight Planner]** Improved waypoint list
|
||||
* **[Flight Planner]** WW2 factions uses different parameters for flight planning.
|
||||
|
||||
* **[Settings]** Added settings to disallow external views
|
||||
* **[Settings]** Added settings to choose F10 Map mode (All, Allies only, Player only, Fog of War, Map Only)
|
||||
* **[Settings]** Added settings to choose whether to auto-generate objective marks on the F10 map
|
||||
|
||||
* **[Info Panel]** Added information about destroyed buildings in info panel
|
||||
* **[Info Panel]** Added information about destroyed units at SAM site in info panel
|
||||
* **[Debriefing]** Added information about units destroyed outside the frontline in the debriefing window
|
||||
* **[Debriefing]** Added destroyed buildings in the debriefing window
|
||||
|
||||
* **[Map]** Tooltip now contains the list of building for Strike targets on the map
|
||||
* **[Map]** Added "Oil derrick" building
|
||||
* **[Map]** Added "ww2 bunker" building (WW2)
|
||||
* **[Map]** Added "ally camp" building (WW2)
|
||||
* **[Map]** Added "V1 Site" (WW2)
|
||||
|
||||
* **[Misc]** Made it possible to setup DCS Saved Games directory and DCS installation directory manually at first start
|
||||
* **[Misc]** Added culling performance settings
|
||||
|
||||
## Fixed issues :
|
||||
|
||||
* **[Units/Factions]** Replaced S3-B Tanker by KC130 for most factions (More fuel)
|
||||
* **[Units/Factions]** WW2 factions will not have offshore oil station and other modern buildings generated. No more third-reich operated offshore stations will spawn on normandy's coast.
|
||||
* **[Units/Factions]** Aircraft carrier will try to move in the wind direction
|
||||
* **[Units/Factions]** Missing icons added for some aircraft
|
||||
|
||||
* **[Mission Generator]** When playing as RED the activation trigger would not be properly generated
|
||||
* **[Mission Generator]** FW-190A8 is now properly considered as a flyable aircraft
|
||||
* **[Mission Generator]** Changed "strike" payload for Su-24M that was ineffective
|
||||
* **[Mission Generator]** Changed "strike" payload for JF-17 to use LS-6 bombs instead of GBU
|
||||
* **[Mission Generator]** Change power station template. (Buildings could end up superposed).
|
||||
|
||||
* **[Maps/Campaign]** Now using Vasiani airbase instead of Soganlung airport in Caucasus campaigns (more parking slot)
|
||||
* **[Info Panel]** Message displayed on base capture event stated that the enemy captured an airbase, while it was the player who captured it.
|
||||
* **[Map View]** Graphical glitch on map when one building of an objective was destroyed, but not the others
|
||||
* **[Mission Planner]** The list of flights was not updated on departure time change.
|
||||
|
||||
|
||||
# 2.0 RC 6
|
||||
|
||||
Saves file from RC5 are not compatible with the new version.
|
||||
Sorry :(
|
||||
|
||||
## Features/Improvements :
|
||||
* **[Units/Factions]** Supercarrier support (You have to go to settings to enable it, if you have the supercarrier module)
|
||||
* **[Units/Factions]** Added 'Modern Bluefor' factions, containing all most popular DCS flyable units
|
||||
* **[Units/Factions]** Factions US 2005 / 1990 will now sometimes have Arleigh Burke class ships instead of Perry as carrier escorts
|
||||
* **[Units/Factions]** Added support for newest WW2 Units
|
||||
* **[Campaign logic]** When a base is captured, refill the "base defenses" group with units for the new owner.
|
||||
* **[Mission Generator]** Carrier ICLS channel will now be configured (check your briefing)
|
||||
* **[Mission Generator]** SAM units will spawn on RED Alarm state
|
||||
* **[Mission Generator]** AI Flight planner now creates its own STRIKE flights
|
||||
* **[Mission Generator]** AI units assigned to Strike flight will now actually engage the buildings they have been assigned.
|
||||
* **[Mission Generator]** Added performance settings to allow disabling : smoke, artillery strike, moving units, infantry, SAM Red alert mode.
|
||||
* **[Mission Generator]** Using Late Activation & Trigger in attempt to improve performance & reduce stutter (Previously they were spawned through 'ETA' feature)
|
||||
* **[UX]** : Improved flight selection behaviour in the Mission Planning Window
|
||||
|
||||
## Fixed issues :
|
||||
* **[Mission Generator]** Payloads were not correctly assigned in the release version.
|
||||
* **[Mission Generator]** Game generation does not work when "no night mission" settings was selected and the current time was "day"
|
||||
* **[Mission Generator]** Game generation does not work when the player selected faction has no AWACS
|
||||
* **[Mission Generator]** Planned flights will spawn even if their home base has been captured or is being contested by enemy ground units.
|
||||
* **[Campaign Generator]** Base defenses would not be generated on Normandy map and in some rare cases on others maps as well
|
||||
* **[Mission Planning]** CAS waypoints created from the "Predefined waypoint selector" would not be at the exact location of the frontline
|
||||
* **[Naming]** CAP mission flown from airbase are not named BARCAP anymore (CAP from carrier is still named BARCAP)
|
||||
@@ -1,2 +1,3 @@
|
||||
from .game import Game
|
||||
from . import db
|
||||
from . import db
|
||||
from .version import VERSION
|
||||
|
||||
21
game/data/aaa_db.py
Normal file
21
game/data/aaa_db.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
AAA_UNITS = [
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
AirDefence.AAA_ZU_23_Closed,
|
||||
AirDefence.AAA_ZU_23_Emplacement,
|
||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Insurgent_Closed,
|
||||
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Insurgent,
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
AirDefence.AAA_Flak_38,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
AirDefence.AAA_8_8cm_Flak_37,
|
||||
AirDefence.AAA_Flak_Vierling_38,
|
||||
AirDefence.AAA_Kdo_G_40,
|
||||
AirDefence.AAA_8_8cm_Flak_41,
|
||||
AirDefence.AAA_Bofors_40mm
|
||||
]
|
||||
17
game/data/building_data.py
Normal file
17
game/data/building_data.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import inspect
|
||||
import dcs
|
||||
|
||||
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick']
|
||||
|
||||
WW2_FREE = ['fuel', 'factory', 'ware']
|
||||
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp']
|
||||
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp']
|
||||
|
||||
FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2',
|
||||
'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5',
|
||||
'Haystack 1', 'Haystack 2', 'Haystack 3', 'Haystack 4', 'Hemmkurvenvenhindernis',
|
||||
'Log posts 1', 'Log posts 2', 'Log posts 3', 'Log ramps 1', 'Log ramps 2', 'Log ramps 3',
|
||||
'Belgian Gate', 'Container white']
|
||||
|
||||
FORTIFICATION_UNITS = [c for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)]
|
||||
FORTIFICATION_UNITS_ID = [c.id for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)]
|
||||
55
game/data/cap_capabilities_db.py
Normal file
55
game/data/cap_capabilities_db.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from dcs.planes import (
|
||||
Bf_109K_4,
|
||||
C_101CC,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
I_16,
|
||||
L_39ZA,
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW
|
||||
)
|
||||
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
|
||||
"""
|
||||
This list contains the aircraft that do not use the guns as the last resort weapons, but as a main weapon
|
||||
They'll RTB when they don't have gun ammo left
|
||||
"""
|
||||
GUNFIGHTERS = [
|
||||
|
||||
# Cold War
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
F_86F_Sabre,
|
||||
A_4E_C,
|
||||
F_5E_3,
|
||||
|
||||
# Trainers
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
|
||||
# WW2
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
I_16,
|
||||
|
||||
]
|
||||
119
game/data/doctrine.py
Normal file
119
game/data/doctrine.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
|
||||
from game.utils import nm_to_meter, feet_to_meter
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Doctrine:
|
||||
cas: bool
|
||||
cap: bool
|
||||
sead: bool
|
||||
strike: bool
|
||||
antiship: bool
|
||||
|
||||
strike_max_range: int
|
||||
sead_max_range: int
|
||||
|
||||
rendezvous_altitude: int
|
||||
hold_distance: int
|
||||
push_distance: int
|
||||
join_distance: int
|
||||
split_distance: int
|
||||
ingress_egress_distance: int
|
||||
ingress_altitude: int
|
||||
egress_altitude: int
|
||||
|
||||
min_patrol_altitude: int
|
||||
max_patrol_altitude: int
|
||||
pattern_altitude: int
|
||||
|
||||
cap_duration: timedelta
|
||||
cap_min_track_length: int
|
||||
cap_max_track_length: int
|
||||
cap_min_distance_from_cp: int
|
||||
cap_max_distance_from_cp: int
|
||||
|
||||
cas_duration: timedelta
|
||||
|
||||
|
||||
MODERN_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
cas=True,
|
||||
sead=True,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
strike_max_range=1500000,
|
||||
sead_max_range=1500000,
|
||||
rendezvous_altitude=feet_to_meter(25000),
|
||||
hold_distance=nm_to_meter(15),
|
||||
push_distance=nm_to_meter(20),
|
||||
join_distance=nm_to_meter(20),
|
||||
split_distance=nm_to_meter(20),
|
||||
ingress_egress_distance=nm_to_meter(45),
|
||||
ingress_altitude=feet_to_meter(20000),
|
||||
egress_altitude=feet_to_meter(20000),
|
||||
min_patrol_altitude=feet_to_meter(15000),
|
||||
max_patrol_altitude=feet_to_meter(33000),
|
||||
pattern_altitude=feet_to_meter(5000),
|
||||
cap_duration=timedelta(minutes=30),
|
||||
cap_min_track_length=nm_to_meter(15),
|
||||
cap_max_track_length=nm_to_meter(40),
|
||||
cap_min_distance_from_cp=nm_to_meter(10),
|
||||
cap_max_distance_from_cp=nm_to_meter(40),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
)
|
||||
|
||||
COLDWAR_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
cas=True,
|
||||
sead=True,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
strike_max_range=1500000,
|
||||
sead_max_range=1500000,
|
||||
rendezvous_altitude=feet_to_meter(22000),
|
||||
hold_distance=nm_to_meter(10),
|
||||
push_distance=nm_to_meter(10),
|
||||
join_distance=nm_to_meter(10),
|
||||
split_distance=nm_to_meter(10),
|
||||
ingress_egress_distance=nm_to_meter(30),
|
||||
ingress_altitude=feet_to_meter(18000),
|
||||
egress_altitude=feet_to_meter(18000),
|
||||
min_patrol_altitude=feet_to_meter(10000),
|
||||
max_patrol_altitude=feet_to_meter(24000),
|
||||
pattern_altitude=feet_to_meter(5000),
|
||||
cap_duration=timedelta(minutes=30),
|
||||
cap_min_track_length=nm_to_meter(12),
|
||||
cap_max_track_length=nm_to_meter(24),
|
||||
cap_min_distance_from_cp=nm_to_meter(8),
|
||||
cap_max_distance_from_cp=nm_to_meter(25),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
)
|
||||
|
||||
WWII_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
cas=True,
|
||||
sead=False,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
strike_max_range=1500000,
|
||||
sead_max_range=1500000,
|
||||
hold_distance=nm_to_meter(5),
|
||||
push_distance=nm_to_meter(5),
|
||||
join_distance=nm_to_meter(5),
|
||||
split_distance=nm_to_meter(5),
|
||||
rendezvous_altitude=feet_to_meter(10000),
|
||||
ingress_egress_distance=nm_to_meter(7),
|
||||
ingress_altitude=feet_to_meter(8000),
|
||||
egress_altitude=feet_to_meter(8000),
|
||||
min_patrol_altitude=feet_to_meter(4000),
|
||||
max_patrol_altitude=feet_to_meter(15000),
|
||||
pattern_altitude=feet_to_meter(5000),
|
||||
cap_duration=timedelta(minutes=30),
|
||||
cap_min_track_length=nm_to_meter(8),
|
||||
cap_max_track_length=nm_to_meter(18),
|
||||
cap_min_distance_from_cp=nm_to_meter(0),
|
||||
cap_max_distance_from_cp=nm_to_meter(5),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
)
|
||||
74
game/data/radar_db.py
Normal file
74
game/data/radar_db.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from dcs.ships import (
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
CG_1164_Moskva,
|
||||
CVN_70_Carl_Vinson,
|
||||
CVN_71_Theodore_Roosevelt,
|
||||
CVN_72_Abraham_Lincoln,
|
||||
CVN_73_George_Washington,
|
||||
CVN_74_John_C__Stennis,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
CV_1143_5_Admiral_Kuznetsov_2017,
|
||||
FFG_11540_Neustrashimy,
|
||||
FFL_1124_4_Grisha,
|
||||
FF_1135M_Rezky,
|
||||
FSG_1241_1MP_Molniya,
|
||||
LHA_1_Tarawa,
|
||||
Oliver_Hazzard_Perry_class,
|
||||
Ticonderoga_class,
|
||||
Type_052B_Destroyer,
|
||||
Type_052C_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
)
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
UNITS_WITH_RADAR = [
|
||||
|
||||
# Radars
|
||||
AirDefence.SAM_SA_15_Tor_9A331,
|
||||
AirDefence.SAM_SA_11_Buk_CC_9S470M1,
|
||||
AirDefence.SAM_Patriot_AMG_AN_MRC_137,
|
||||
AirDefence.SAM_Patriot_ECS_AN_MSQ_104,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
AirDefence.EWR_1L13,
|
||||
AirDefence.SAM_SA_6_Kub_STR_9S91,
|
||||
AirDefence.SAM_SA_10_S_300PS_TR_30N6,
|
||||
AirDefence.SAM_SA_10_S_300PS_SR_5N66M,
|
||||
AirDefence.EWR_55G6,
|
||||
AirDefence.SAM_SA_10_S_300PS_SR_64H6E,
|
||||
AirDefence.SAM_SA_11_Buk_SR_9S18M1,
|
||||
AirDefence.CP_9S80M1_Sborka,
|
||||
AirDefence.SAM_Hawk_TR_AN_MPQ_46,
|
||||
AirDefence.SAM_Hawk_SR_AN_MPQ_50,
|
||||
AirDefence.SAM_Patriot_STR_AN_MPQ_53,
|
||||
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
|
||||
AirDefence.SAM_SR_P_19,
|
||||
AirDefence.SAM_Roland_EWR,
|
||||
AirDefence.SAM_SA_3_S_125_TR_SNR,
|
||||
AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song,
|
||||
AirDefence.HQ_7_Self_Propelled_STR,
|
||||
|
||||
# Ships
|
||||
CVN_70_Carl_Vinson,
|
||||
Oliver_Hazzard_Perry_class,
|
||||
Ticonderoga_class,
|
||||
FFL_1124_4_Grisha,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
FSG_1241_1MP_Molniya,
|
||||
CG_1164_Moskva,
|
||||
FFG_11540_Neustrashimy,
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
FF_1135M_Rezky,
|
||||
CV_1143_5_Admiral_Kuznetsov_2017,
|
||||
CVN_74_John_C__Stennis,
|
||||
CVN_71_Theodore_Roosevelt,
|
||||
CVN_72_Abraham_Lincoln,
|
||||
CVN_73_George_Washington,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
LHA_1_Tarawa,
|
||||
Type_052B_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
Type_052C_Destroyer
|
||||
]
|
||||
1385
game/db.py
1385
game/db.py
File diff suppressed because it is too large
Load Diff
213
game/debriefing.py
Normal file
213
game/debriefing.py
Normal file
@@ -0,0 +1,213 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import typing
|
||||
|
||||
from game import db
|
||||
|
||||
DEBRIEFING_LOG_EXTENSION = "log"
|
||||
|
||||
class DebriefingDeadUnitInfo:
|
||||
country_id = -1
|
||||
player_unit = False
|
||||
type = None
|
||||
|
||||
def __init__(self, country_id, player_unit , type):
|
||||
self.country_id = country_id
|
||||
self.player_unit = player_unit
|
||||
self.type = type
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.country_id) + " " + str(self.player_unit) + " " + str(self.type)
|
||||
|
||||
class Debriefing:
|
||||
def __init__(self, state_data, game):
|
||||
self.state_data = state_data
|
||||
self.killed_aircrafts = state_data["killed_aircrafts"]
|
||||
self.killed_ground_units = state_data["killed_ground_units"]
|
||||
self.weapons_fired = state_data["weapons_fired"]
|
||||
self.mission_ended = state_data["mission_ended"]
|
||||
self.destroyed_units = state_data["destroyed_objects_positions"]
|
||||
|
||||
self.__destroyed_units = []
|
||||
logging.info("--------------------------------")
|
||||
logging.info("Starting Debriefing preprocessing")
|
||||
logging.info("--------------------------------")
|
||||
logging.info(self.base_capture_events)
|
||||
logging.info(self.killed_aircrafts)
|
||||
logging.info(self.killed_ground_units)
|
||||
logging.info(self.weapons_fired)
|
||||
logging.info(self.mission_ended)
|
||||
logging.info(self.destroyed_units)
|
||||
logging.info("--------------------------------")
|
||||
|
||||
self.player_country_id = db.country_id_from_name(game.player_country)
|
||||
self.enemy_country_id = db.country_id_from_name(game.enemy_country)
|
||||
|
||||
self.dead_aircraft = []
|
||||
self.dead_units = []
|
||||
self.dead_aaa_groups = []
|
||||
self.dead_buildings = []
|
||||
|
||||
for aircraft in self.killed_aircrafts:
|
||||
try:
|
||||
country = int(aircraft.split("|")[1])
|
||||
type = db.unit_type_from_name(aircraft.split("|")[4])
|
||||
player_unit = (country == self.player_country_id)
|
||||
aircraft = DebriefingDeadUnitInfo(country, player_unit, type)
|
||||
if type is not None:
|
||||
self.dead_aircraft.append(aircraft)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
|
||||
for unit in self.killed_ground_units:
|
||||
try:
|
||||
country = int(unit.split("|")[1])
|
||||
type = db.unit_type_from_name(unit.split("|")[4])
|
||||
player_unit = (country == self.player_country_id)
|
||||
unit = DebriefingDeadUnitInfo(country, player_unit, type)
|
||||
if type is not None:
|
||||
self.dead_units.append(unit)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
|
||||
for unit in self.killed_ground_units:
|
||||
for cp in game.theater.controlpoints:
|
||||
|
||||
logging.info(cp.name)
|
||||
logging.info(cp.captured)
|
||||
|
||||
if cp.captured:
|
||||
country = self.player_country_id
|
||||
else:
|
||||
country = self.enemy_country_id
|
||||
player_unit = (country == self.player_country_id)
|
||||
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
logging.info(unit)
|
||||
logging.info(ground_object.group_name)
|
||||
if ground_object.is_same_group(unit):
|
||||
unit = DebriefingDeadUnitInfo(country, player_unit, ground_object.dcs_identifier)
|
||||
self.dead_buildings.append(unit)
|
||||
elif ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]:
|
||||
for g in ground_object.groups:
|
||||
for u in g.units:
|
||||
if u.name == unit:
|
||||
unit = DebriefingDeadUnitInfo(country, player_unit, db.unit_type_from_name(u.type))
|
||||
self.dead_units.append(unit)
|
||||
|
||||
self.player_dead_aircraft = [a for a in self.dead_aircraft if a.country_id == self.player_country_id]
|
||||
self.enemy_dead_aircraft = [a for a in self.dead_aircraft if a.country_id == self.enemy_country_id]
|
||||
self.player_dead_units = [a for a in self.dead_units if a.country_id == self.player_country_id]
|
||||
self.enemy_dead_units = [a for a in self.dead_units if a.country_id == self.enemy_country_id]
|
||||
self.player_dead_buildings = [a for a in self.dead_buildings if a.country_id == self.player_country_id]
|
||||
self.enemy_dead_buildings = [a for a in self.dead_buildings if a.country_id == self.enemy_country_id]
|
||||
|
||||
logging.info(self.player_dead_aircraft)
|
||||
logging.info(self.enemy_dead_aircraft)
|
||||
logging.info(self.player_dead_units)
|
||||
logging.info(self.enemy_dead_units)
|
||||
|
||||
self.player_dead_aircraft_dict = {}
|
||||
for a in self.player_dead_aircraft:
|
||||
if a.type in self.player_dead_aircraft_dict.keys():
|
||||
self.player_dead_aircraft_dict[a.type] = self.player_dead_aircraft_dict[a.type] + 1
|
||||
else:
|
||||
self.player_dead_aircraft_dict[a.type] = 1
|
||||
|
||||
self.enemy_dead_aircraft_dict = {}
|
||||
for a in self.enemy_dead_aircraft:
|
||||
if a.type in self.enemy_dead_aircraft_dict.keys():
|
||||
self.enemy_dead_aircraft_dict[a.type] = self.enemy_dead_aircraft_dict[a.type] + 1
|
||||
else:
|
||||
self.enemy_dead_aircraft_dict[a.type] = 1
|
||||
|
||||
self.player_dead_units_dict = {}
|
||||
for a in self.player_dead_units:
|
||||
if a.type in self.player_dead_units_dict.keys():
|
||||
self.player_dead_units_dict[a.type] = self.player_dead_units_dict[a.type] + 1
|
||||
else:
|
||||
self.player_dead_units_dict[a.type] = 1
|
||||
|
||||
self.enemy_dead_units_dict = {}
|
||||
for a in self.enemy_dead_units:
|
||||
if a.type in self.enemy_dead_units_dict.keys():
|
||||
self.enemy_dead_units_dict[a.type] = self.enemy_dead_units_dict[a.type] + 1
|
||||
else:
|
||||
self.enemy_dead_units_dict[a.type] = 1
|
||||
|
||||
self.player_dead_buildings_dict = {}
|
||||
for a in self.player_dead_buildings:
|
||||
if a.type in self.player_dead_buildings_dict.keys():
|
||||
self.player_dead_buildings_dict[a.type] = self.player_dead_buildings_dict[a.type] + 1
|
||||
else:
|
||||
self.player_dead_buildings_dict[a.type] = 1
|
||||
|
||||
self.enemy_dead_buildings_dict = {}
|
||||
for a in self.enemy_dead_buildings:
|
||||
if a.type in self.enemy_dead_buildings_dict.keys():
|
||||
self.enemy_dead_buildings_dict[a.type] = self.enemy_dead_buildings_dict[a.type] + 1
|
||||
else:
|
||||
self.enemy_dead_buildings_dict[a.type] = 1
|
||||
|
||||
logging.info("--------------------------------")
|
||||
logging.info("Debriefing pre process results :")
|
||||
logging.info("--------------------------------")
|
||||
logging.info(self.player_dead_aircraft_dict)
|
||||
logging.info(self.enemy_dead_aircraft_dict)
|
||||
logging.info(self.player_dead_units_dict)
|
||||
logging.info(self.enemy_dead_units_dict)
|
||||
logging.info(self.player_dead_buildings_dict)
|
||||
logging.info(self.enemy_dead_buildings_dict)
|
||||
|
||||
@property
|
||||
def base_capture_events(self):
|
||||
"""Keeps only the last instance of a base capture event for each base ID"""
|
||||
reversed_captures = [i for i in self.state_data["base_capture_events"][::-1]]
|
||||
last_base_cap_indexes = []
|
||||
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
|
||||
if base in [x[1] for x in last_base_cap_indexes]:
|
||||
continue
|
||||
else:
|
||||
last_base_cap_indexes.append((idx, base))
|
||||
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
|
||||
|
||||
|
||||
class PollDebriefingFileThread(threading.Thread):
|
||||
"""Thread class with a stop() method. The thread itself has to check
|
||||
regularly for the stopped() condition."""
|
||||
|
||||
def __init__(self, callback: typing.Callable, game):
|
||||
super(PollDebriefingFileThread, self).__init__()
|
||||
self._stop_event = threading.Event()
|
||||
self.callback = callback
|
||||
self.game = game
|
||||
|
||||
def stop(self):
|
||||
self._stop_event.set()
|
||||
|
||||
def stopped(self):
|
||||
return self._stop_event.is_set()
|
||||
|
||||
def run(self):
|
||||
if os.path.isfile("state.json"):
|
||||
last_modified = os.path.getmtime("state.json")
|
||||
else:
|
||||
last_modified = 0
|
||||
while not self.stopped():
|
||||
if os.path.isfile("state.json") and os.path.getmtime("state.json") > last_modified:
|
||||
with open("state.json", "r") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
debriefing = Debriefing(json_data, self.game)
|
||||
self.callback(debriefing)
|
||||
break
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def wait_for_debriefing(callback: typing.Callable, game)->PollDebriefingFileThread:
|
||||
thread = PollDebriefingFileThread(callback, game)
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
@@ -1,8 +1,2 @@
|
||||
from .event import *
|
||||
from .groundintercept import *
|
||||
from .intercept import *
|
||||
from .capture import *
|
||||
from .navalintercept import *
|
||||
from .antiaastrike import *
|
||||
from .groundattack import *
|
||||
from .infantrytransport import *
|
||||
from .frontlineattack import *
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import math
|
||||
import random
|
||||
|
||||
from dcs.task import *
|
||||
|
||||
from game import *
|
||||
from game.event import *
|
||||
from game.operation.antiaastrike import AntiAAStrikeOperation
|
||||
from userdata.debriefing import Debriefing
|
||||
|
||||
|
||||
class AntiAAStrikeEvent(Event):
|
||||
TARGET_AMOUNT_MAX = 2
|
||||
STRENGTH_INFLUENCE = 0.3
|
||||
SUCCESS_TARGETS_HIT_PERCENTAGE = 0.5
|
||||
|
||||
targets = None # type: db.ArmorDict
|
||||
|
||||
def __str__(self):
|
||||
return "Anti-AA strike from {} at {}".format(self.from_cp, self.to_cp)
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing):
|
||||
total_targets = sum(self.targets.values())
|
||||
destroyed_targets = 0
|
||||
for unit, count in debriefing.destroyed_units[self.defender_name].items():
|
||||
if unit in self.targets:
|
||||
destroyed_targets += count
|
||||
|
||||
if self.from_cp.captured:
|
||||
return math.ceil(float(destroyed_targets) / total_targets) >= self.SUCCESS_TARGETS_HIT_PERCENTAGE
|
||||
else:
|
||||
return math.ceil(float(destroyed_targets) / total_targets) < self.SUCCESS_TARGETS_HIT_PERCENTAGE
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
super(AntiAAStrikeEvent, self).commit(debriefing)
|
||||
|
||||
if self.from_cp.captured:
|
||||
if self.is_successfull(debriefing):
|
||||
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
else:
|
||||
self.to_cp.base.affect_strength(+self.STRENGTH_INFLUENCE)
|
||||
else:
|
||||
if self.is_successfull(debriefing):
|
||||
self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
else:
|
||||
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
|
||||
def skip(self):
|
||||
if self.to_cp.captured:
|
||||
self.to_cp.base.affect_strength(-0.1)
|
||||
|
||||
def player_attacking(self, strikegroup: db.PlaneDict, clients: db.PlaneDict):
|
||||
self.targets = self.to_cp.base.assemble_aa(count=self.to_cp.base.total_aa)
|
||||
|
||||
op = AntiAAStrikeOperation(game=self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker_clients=clients,
|
||||
defender_clients={},
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp)
|
||||
op.setup(target=self.targets,
|
||||
strikegroup=strikegroup,
|
||||
interceptors={})
|
||||
|
||||
self.operation = op
|
||||
|
||||
def player_defending(self, interceptors: db.PlaneDict, clients: db.PlaneDict):
|
||||
self.targets = self.to_cp.base.assemble_aa()
|
||||
|
||||
op = AntiAAStrikeOperation(
|
||||
self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker_clients={},
|
||||
defender_clients=clients,
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp
|
||||
)
|
||||
|
||||
strikegroup = self.from_cp.base.scramble_cas(self.game.settings.multiplier)
|
||||
op.setup(target=self.targets,
|
||||
strikegroup=strikegroup,
|
||||
interceptors=interceptors)
|
||||
|
||||
self.operation = op
|
||||
@@ -1,89 +0,0 @@
|
||||
import math
|
||||
import random
|
||||
|
||||
from dcs.task import *
|
||||
|
||||
from game import db
|
||||
from game.operation.capture import CaptureOperation
|
||||
from userdata.debriefing import Debriefing
|
||||
|
||||
from .event import Event
|
||||
|
||||
|
||||
class CaptureEvent(Event):
|
||||
silent = True
|
||||
BONUS_BASE = 15
|
||||
STRENGTH_RECOVERY = 0.35
|
||||
|
||||
def __str__(self):
|
||||
return "Attack from {} to {}".format(self.from_cp, self.to_cp)
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing):
|
||||
alive_attackers = sum([v for k, v in debriefing.alive_units[self.attacker_name].items() if db.unit_task(k) == PinpointStrike])
|
||||
alive_defenders = sum([v for k, v in debriefing.alive_units[self.defender_name].items() if db.unit_task(k) == PinpointStrike])
|
||||
attackers_success = alive_attackers >= alive_defenders
|
||||
if self.from_cp.captured:
|
||||
return attackers_success
|
||||
else:
|
||||
return not attackers_success
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
super(CaptureEvent, self).commit(debriefing)
|
||||
if self.is_successfull(debriefing):
|
||||
if self.from_cp.captured:
|
||||
self.to_cp.captured = True
|
||||
self.to_cp.base.filter_units(db.UNIT_BY_COUNTRY[self.attacker_name])
|
||||
|
||||
self.to_cp.base.affect_strength(+self.STRENGTH_RECOVERY)
|
||||
else:
|
||||
if not self.from_cp.captured:
|
||||
self.to_cp.captured = False
|
||||
self.to_cp.base.affect_strength(+self.STRENGTH_RECOVERY)
|
||||
|
||||
def skip(self):
|
||||
if self.to_cp.captured:
|
||||
self.to_cp.captured = False
|
||||
|
||||
def player_defending(self, interceptors: db.PlaneDict, clients: db.PlaneDict):
|
||||
cas = self.from_cp.base.scramble_cas(self.game.settings.multiplier)
|
||||
escort = self.from_cp.base.scramble_sweep(self.game.settings.multiplier)
|
||||
attackers = self.from_cp.base.armor
|
||||
|
||||
op = CaptureOperation(game=self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker_clients={},
|
||||
defender_clients=clients,
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp)
|
||||
|
||||
op.setup(cas=cas,
|
||||
escort=escort,
|
||||
attack=attackers,
|
||||
intercept=interceptors,
|
||||
defense=self.to_cp.base.armor,
|
||||
aa=self.to_cp.base.aa)
|
||||
|
||||
self.operation = op
|
||||
|
||||
def player_attacking(self, cas: db.PlaneDict, escort: db.PlaneDict, armor: db.ArmorDict, clients: db.PlaneDict):
|
||||
op = CaptureOperation(game=self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker_clients=clients,
|
||||
defender_clients={},
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp)
|
||||
|
||||
defenders = self.to_cp.base.scramble_sweep(self.game.settings.multiplier)
|
||||
defenders.update(self.to_cp.base.scramble_cas(self.game.settings.multiplier))
|
||||
|
||||
op.setup(cas=cas,
|
||||
escort=escort,
|
||||
attack=armor,
|
||||
intercept=defenders,
|
||||
defense=self.to_cp.base.armor,
|
||||
aa=self.to_cp.base.assemble_aa())
|
||||
|
||||
self.operation = op
|
||||
|
||||
@@ -1,46 +1,86 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, List, Optional, Type, TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import Task
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game import *
|
||||
from theater import *
|
||||
from gen.environmentgen import EnvironmentSettings
|
||||
from game import db, persistency
|
||||
from game.debriefing import Debriefing
|
||||
from game.infos.information import Information
|
||||
from game.operation.operation import Operation
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from theater import ControlPoint
|
||||
|
||||
from userdata.debriefing import Debriefing
|
||||
from userdata import persistency
|
||||
if TYPE_CHECKING:
|
||||
from ..game import Game
|
||||
|
||||
DIFFICULTY_LOG_BASE = 1.1
|
||||
EVENT_DEPARTURE_MAX_DISTANCE = 340000
|
||||
|
||||
|
||||
MINOR_DEFEAT_INFLUENCE = 0.1
|
||||
DEFEAT_INFLUENCE = 0.3
|
||||
STRONG_DEFEAT_INFLUENCE = 0.5
|
||||
|
||||
|
||||
class Event:
|
||||
silent = False
|
||||
informational = False
|
||||
is_awacs_enabled = False
|
||||
ca_slots = 0
|
||||
|
||||
game = None # type: Game
|
||||
location = None # type: Point
|
||||
from_cp = None # type: ControlPoint
|
||||
to_cp = None # type: ControlPoint
|
||||
|
||||
operation = None # type: Operation
|
||||
difficulty = 1 # type: int
|
||||
game = None # type: Game
|
||||
environment_settings = None # type: EnvironmentSettings
|
||||
BONUS_BASE = 5
|
||||
|
||||
def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game):
|
||||
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
|
||||
self.game = game
|
||||
self.departure_cp: Optional[ControlPoint] = None
|
||||
self.from_cp = from_cp
|
||||
self.to_cp = target_cp
|
||||
self.location = location
|
||||
self.attacker_name = attacker_name
|
||||
self.defender_name = defender_name
|
||||
self.to_cp = to_cp
|
||||
self.from_cp = from_cp
|
||||
self.game = game
|
||||
|
||||
@property
|
||||
def is_player_attacking(self) -> bool:
|
||||
return self.attacker_name == self.game.player
|
||||
return self.attacker_name == self.game.player_name
|
||||
|
||||
@property
|
||||
def enemy_cp(self) -> ControlPoint:
|
||||
if self.attacker_name == self.game.player:
|
||||
def enemy_cp(self) -> Optional[ControlPoint]:
|
||||
if self.attacker_name == self.game.player_name:
|
||||
return self.to_cp
|
||||
else:
|
||||
return self.from_cp
|
||||
return self.departure_cp
|
||||
|
||||
@property
|
||||
def threat_description(self) -> str:
|
||||
return ""
|
||||
def tasks(self) -> List[Type[Task]]:
|
||||
return []
|
||||
|
||||
@property
|
||||
def global_cp_available(self) -> bool:
|
||||
return False
|
||||
|
||||
def is_departure_available_from(self, cp: ControlPoint) -> bool:
|
||||
if not cp.captured:
|
||||
return False
|
||||
|
||||
if self.location.distance_to_point(cp.position) > EVENT_DEPARTURE_MAX_DISTANCE:
|
||||
return False
|
||||
|
||||
if cp.is_global and not self.global_cp_available:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def bonus(self) -> int:
|
||||
return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE)
|
||||
@@ -50,52 +90,293 @@ class Event:
|
||||
|
||||
def generate(self):
|
||||
self.operation.is_awacs_enabled = self.is_awacs_enabled
|
||||
self.operation.ca_slots = self.ca_slots
|
||||
|
||||
self.operation.prepare(self.game.theater.terrain, is_quick=False)
|
||||
self.operation.generate()
|
||||
self.operation.mission.save(persistency.mission_path_for("liberation_nextturn.miz"))
|
||||
self.operation.current_mission.save(persistency.mission_path_for("liberation_nextturn.miz"))
|
||||
self.environment_settings = self.operation.environment_settings
|
||||
|
||||
def generate_quick(self):
|
||||
self.operation.is_awacs_enabled = self.is_awacs_enabled
|
||||
self.operation.environment_settings = self.environment_settings
|
||||
|
||||
self.operation.prepare(self.game.theater.terrain, is_quick=True)
|
||||
self.operation.generate()
|
||||
self.operation.mission.save(persistency.mission_path_for("liberation_nextturn_quick.miz"))
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
for country, losses in debriefing.destroyed_units.items():
|
||||
if country == self.attacker_name:
|
||||
cp = self.from_cp
|
||||
else:
|
||||
cp = self.to_cp
|
||||
|
||||
cp.base.commit_losses(losses)
|
||||
logging.info("Commiting mission results")
|
||||
|
||||
# ------------------------------
|
||||
# Destroyed aircrafts
|
||||
cp_map = {cp.id: cp for cp in self.game.theater.controlpoints}
|
||||
for destroyed_aircraft in debriefing.killed_aircrafts:
|
||||
try:
|
||||
cpid = int(destroyed_aircraft.split("|")[3])
|
||||
type = db.unit_type_from_name(destroyed_aircraft.split("|")[4])
|
||||
if cpid in cp_map.keys():
|
||||
cp = cp_map[cpid]
|
||||
if type in cp.base.aircraft.keys():
|
||||
logging.info("Aircraft destroyed : " + str(type))
|
||||
cp.base.aircraft[type] = max(0, cp.base.aircraft[type]-1)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# ------------------------------
|
||||
# Destroyed ground units
|
||||
killed_unit_count_by_cp = {cp.id: 0 for cp in self.game.theater.controlpoints}
|
||||
cp_map = {cp.id: cp for cp in self.game.theater.controlpoints}
|
||||
for killed_ground_unit in debriefing.killed_ground_units:
|
||||
try:
|
||||
cpid = int(killed_ground_unit.split("|")[3])
|
||||
type = db.unit_type_from_name(killed_ground_unit.split("|")[4])
|
||||
if cpid in cp_map.keys():
|
||||
killed_unit_count_by_cp[cpid] = killed_unit_count_by_cp[cpid] + 1
|
||||
cp = cp_map[cpid]
|
||||
if type in cp.base.armor.keys():
|
||||
logging.info("Ground unit destroyed : " + str(type))
|
||||
cp.base.armor[type] = max(0, cp.base.armor[type] - 1)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# ------------------------------
|
||||
# Static ground objects
|
||||
for destroyed_ground_unit_name in debriefing.killed_ground_units:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if not cp.ground_objects:
|
||||
continue
|
||||
|
||||
# -- Static ground objects
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
|
||||
if (
|
||||
(ground_object.group_name == destroyed_ground_unit_name)
|
||||
or
|
||||
(ground_object.is_same_group(destroyed_ground_unit_name))
|
||||
):
|
||||
logging.info("cp {} killing ground object {}".format(cp, ground_object.group_name))
|
||||
cp.ground_objects[i].is_dead = True
|
||||
|
||||
info = Information("Building destroyed",
|
||||
ground_object.dcs_identifier + " has been destroyed at location " + ground_object.obj_name,
|
||||
self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
|
||||
|
||||
# -- AA Site groups
|
||||
destroyed_units = 0
|
||||
info = Information("Units destroyed at " + ground_object.obj_name,
|
||||
"",
|
||||
self.game.turn)
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
if ground_object.dcs_identifier in ["AA", "CARRIER", "LHA", "EWR"]:
|
||||
for g in ground_object.groups:
|
||||
if not hasattr(g, "units_losts"):
|
||||
g.units_losts = []
|
||||
for u in g.units:
|
||||
if u.name == destroyed_ground_unit_name:
|
||||
g.units.remove(u)
|
||||
g.units_losts.append(u)
|
||||
destroyed_units = destroyed_units + 1
|
||||
info.text = u.type
|
||||
ucount = sum([len(g.units) for g in ground_object.groups])
|
||||
if ucount == 0:
|
||||
ground_object.is_dead = True
|
||||
if destroyed_units > 0:
|
||||
self.game.informations.append(info)
|
||||
|
||||
# ------------------------------
|
||||
# Captured bases
|
||||
#if self.game.player_country in db.BLUEFOR_FACTIONS:
|
||||
coalition = 2 # Value in DCS mission event for BLUE
|
||||
#else:
|
||||
# coalition = 1 # Value in DCS mission event for RED
|
||||
|
||||
for captured in debriefing.base_capture_events:
|
||||
try:
|
||||
id = int(captured.split("||")[0])
|
||||
new_owner_coalition = int(captured.split("||")[1])
|
||||
|
||||
captured_cps = []
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.id == id:
|
||||
|
||||
if cp.captured and new_owner_coalition != coalition:
|
||||
for_player = False
|
||||
info = Information(cp.name + " lost !", "The ennemy took control of " + cp.name + "\nShame on us !", self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
captured_cps.append(cp)
|
||||
elif not(cp.captured) and new_owner_coalition == coalition:
|
||||
for_player = True
|
||||
info = Information(cp.name + " captured !", "We took control of " + cp.name + "! Great job !", self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
captured_cps.append(cp)
|
||||
else:
|
||||
continue
|
||||
|
||||
cp.capture(self.game, for_player)
|
||||
|
||||
for cp in captured_cps:
|
||||
logging.info("Will run redeploy for " + cp.name)
|
||||
self.redeploy_units(cp)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# Destroyed units carcass
|
||||
# -------------------------
|
||||
for destroyed_unit in debriefing.destroyed_units:
|
||||
self.game.add_destroyed_units(destroyed_unit)
|
||||
|
||||
# -----------------------------------
|
||||
# Compute damage to bases
|
||||
for cp in self.game.theater.player_points():
|
||||
enemy_cps = [e for e in cp.connected_points if not e.captured]
|
||||
for enemy_cp in enemy_cps:
|
||||
print("Compute frontline progression for : " + cp.name + " to " + enemy_cp.name)
|
||||
|
||||
delta = 0.0
|
||||
player_won = True
|
||||
ally_casualties = killed_unit_count_by_cp[cp.id]
|
||||
enemy_casualties = killed_unit_count_by_cp[enemy_cp.id]
|
||||
ally_units_alive = cp.base.total_armor
|
||||
enemy_units_alive = enemy_cp.base.total_armor
|
||||
|
||||
print(ally_units_alive)
|
||||
print(enemy_units_alive)
|
||||
print(ally_casualties)
|
||||
print(enemy_casualties)
|
||||
|
||||
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
|
||||
|
||||
player_aggresive = cp.stances[enemy_cp.id] in [CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]
|
||||
|
||||
if ally_units_alive == 0:
|
||||
player_won = False
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
elif enemy_units_alive == 0:
|
||||
player_won = True
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
elif cp.stances[enemy_cp.id] == CombatStance.RETREAT:
|
||||
player_won = False
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
else:
|
||||
if enemy_casualties > ally_casualties:
|
||||
player_won = True
|
||||
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
else:
|
||||
if ratio > 3:
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
elif ratio < 1.5:
|
||||
delta = MINOR_DEFEAT_INFLUENCE
|
||||
else:
|
||||
delta = DEFEAT_INFLUENCE
|
||||
elif ally_casualties > enemy_casualties:
|
||||
|
||||
if ally_units_alive > 2*enemy_units_alive and player_aggresive:
|
||||
# Even with casualties if the enemy is overwhelmed, they are going to lose ground
|
||||
player_won = True
|
||||
delta = MINOR_DEFEAT_INFLUENCE
|
||||
elif ally_units_alive > 3*enemy_units_alive and player_aggresive:
|
||||
player_won = True
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
else:
|
||||
# But is the enemy is not outnumbered, we lose
|
||||
player_won = False
|
||||
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
else:
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
|
||||
# No progress with defensive strategies
|
||||
if player_won and cp.stances[enemy_cp.id] in [CombatStance.DEFENSIVE, CombatStance.AMBUSH]:
|
||||
print("Defensive stance, progress is limited")
|
||||
delta = MINOR_DEFEAT_INFLUENCE
|
||||
|
||||
if player_won:
|
||||
print(cp.name + " won ! factor > " + str(delta))
|
||||
cp.base.affect_strength(delta)
|
||||
enemy_cp.base.affect_strength(-delta)
|
||||
info = Information("Frontline Report",
|
||||
"Our ground forces from " + cp.name + " are making progress toward " + enemy_cp.name,
|
||||
self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
else:
|
||||
print(cp.name + " lost ! factor > " + str(delta))
|
||||
enemy_cp.base.affect_strength(delta)
|
||||
cp.base.affect_strength(-delta)
|
||||
info = Information("Frontline Report",
|
||||
"Our ground forces from " + cp.name + " are losing ground against the enemy forces from " + enemy_cp.name,
|
||||
self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
|
||||
def skip(self):
|
||||
pass
|
||||
|
||||
def redeploy_units(self, cp):
|
||||
""""
|
||||
Auto redeploy units to newly captured base
|
||||
"""
|
||||
|
||||
ally_connected_cps = [ocp for ocp in cp.connected_points if cp.captured == ocp.captured]
|
||||
enemy_connected_cps = [ocp for ocp in cp.connected_points if cp.captured != ocp.captured]
|
||||
|
||||
# If the newly captured cp does not have enemy connected cp,
|
||||
# then it is not necessary to redeploy frontline units there.
|
||||
if len(enemy_connected_cps) == 0:
|
||||
return
|
||||
else:
|
||||
# From each ally cp, send reinforcements
|
||||
for ally_cp in ally_connected_cps:
|
||||
total_units_redeployed = 0
|
||||
own_enemy_cp = [ocp for ocp in ally_cp.connected_points if ally_cp.captured != ocp.captured]
|
||||
|
||||
moved_units = {}
|
||||
|
||||
# If the connected base, does not have any more enemy cp connected.
|
||||
# Or if it is not the opponent redeploying forces there (enemy AI will never redeploy all their forces at once)
|
||||
if len(own_enemy_cp) > 0 or not cp.captured:
|
||||
for frontline_unit, count in ally_cp.base.armor.items():
|
||||
moved_units[frontline_unit] = int(count/2)
|
||||
total_units_redeployed = total_units_redeployed + int(count/2)
|
||||
else: # So if the old base, does not have any more enemy cp connected, or if it is an enemy base
|
||||
for frontline_unit, count in ally_cp.base.armor.items():
|
||||
moved_units[frontline_unit] = count
|
||||
total_units_redeployed = total_units_redeployed + count
|
||||
|
||||
cp.base.commision_units(moved_units)
|
||||
ally_cp.base.commit_losses(moved_units)
|
||||
|
||||
if total_units_redeployed > 0:
|
||||
info = Information("Units redeployed", "", self.game.turn)
|
||||
info.text = str(total_units_redeployed) + " units have been redeployed from " + ally_cp.name + " to " + cp.name
|
||||
self.game.informations.append(info)
|
||||
logging.info(info.text)
|
||||
|
||||
|
||||
|
||||
class UnitsDeliveryEvent(Event):
|
||||
informational = True
|
||||
units = None # type: typing.Dict[UnitType, int]
|
||||
|
||||
def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game):
|
||||
super(UnitsDeliveryEvent, self).__init__(attacker_name=attacker_name,
|
||||
defender_name=defender_name,
|
||||
super(UnitsDeliveryEvent, self).__init__(game=game,
|
||||
location=to_cp.position,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
game=game)
|
||||
target_cp=to_cp,
|
||||
attacker_name=attacker_name,
|
||||
defender_name=defender_name)
|
||||
|
||||
self.units = {}
|
||||
self.units: Dict[UnitType, int] = {}
|
||||
|
||||
def __str__(self):
|
||||
return "Pending delivery to {}".format(self.to_cp)
|
||||
|
||||
def deliver(self, units: typing.Dict[UnitType, int]):
|
||||
def deliver(self, units: Dict[UnitType, int]):
|
||||
for k, v in units.items():
|
||||
self.units[k] = self.units.get(k, 0) + v
|
||||
|
||||
def skip(self):
|
||||
|
||||
for k, v in self.units.items():
|
||||
info = Information("Ally Reinforcement", str(k.id) + " x " + str(v) + " at " + self.to_cp.name, self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
|
||||
self.to_cp.base.commision_units(self.units)
|
||||
|
||||
49
game/event/frontlineattack.py
Normal file
49
game/event/frontlineattack.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from typing import List, Type
|
||||
|
||||
from dcs.task import CAP, CAS, Task
|
||||
|
||||
from game import db
|
||||
from game.operation.frontlineattack import FrontlineAttackOperation
|
||||
from .event import Event
|
||||
from ..debriefing import Debriefing
|
||||
|
||||
|
||||
class FrontlineAttackEvent(Event):
|
||||
|
||||
@property
|
||||
def tasks(self) -> List[Type[Task]]:
|
||||
if self.is_player_attacking:
|
||||
return [CAS, CAP]
|
||||
else:
|
||||
return [CAP]
|
||||
|
||||
@property
|
||||
def global_cp_available(self) -> bool:
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return "Frontline attack"
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing):
|
||||
attackers_success = True
|
||||
if self.from_cp.captured:
|
||||
return attackers_success
|
||||
else:
|
||||
return not attackers_success
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
super(FrontlineAttackEvent, self).commit(debriefing)
|
||||
|
||||
def skip(self):
|
||||
if self.to_cp.captured:
|
||||
self.to_cp.base.affect_strength(-0.1)
|
||||
|
||||
def player_attacking(self, flights: db.TaskForceDict):
|
||||
assert self.departure_cp is not None
|
||||
op = FrontlineAttackOperation(game=self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
from_cp=self.from_cp,
|
||||
departure_cp=self.departure_cp,
|
||||
to_cp=self.to_cp)
|
||||
self.operation = op
|
||||
@@ -1,40 +0,0 @@
|
||||
import math
|
||||
import random
|
||||
|
||||
from dcs.task import *
|
||||
|
||||
from game import *
|
||||
from game.event import *
|
||||
from game.event.groundintercept import GroundInterceptEvent
|
||||
from game.operation.groundattack import GroundAttackOperation
|
||||
|
||||
|
||||
class GroundAttackEvent(GroundInterceptEvent):
|
||||
def __str__(self):
|
||||
return "Destroy insurgents at {}".format(self.to_cp)
|
||||
|
||||
@property
|
||||
def threat_description(self):
|
||||
return ""
|
||||
|
||||
def player_defending(self, strikegroup: db.PlaneDict, clients: db.PlaneDict):
|
||||
suitable_unittypes = db.find_unittype(Reconnaissance, self.attacker_name)
|
||||
random.shuffle(suitable_unittypes)
|
||||
unittypes = suitable_unittypes[:self.TARGET_VARIETY]
|
||||
typecount = max(math.floor(self.difficulty * self.TARGET_AMOUNT_FACTOR), 1)
|
||||
self.targets = {unittype: typecount for unittype in unittypes}
|
||||
|
||||
op = GroundAttackOperation(game=self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker_clients={},
|
||||
defender_clients=clients,
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp)
|
||||
op.setup(target=self.targets,
|
||||
strikegroup=strikegroup)
|
||||
|
||||
self.operation = op
|
||||
|
||||
def player_attacking(self, interceptors: db.PlaneDict, clients: db.PlaneDict):
|
||||
assert False
|
||||
@@ -1,106 +0,0 @@
|
||||
import math
|
||||
import random
|
||||
|
||||
from dcs.task import *
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from game import *
|
||||
from game.event import *
|
||||
from game.operation.groundintercept import GroundInterceptOperation
|
||||
from userdata.debriefing import Debriefing
|
||||
|
||||
|
||||
class GroundInterceptEvent(Event):
|
||||
TARGET_AMOUNT_FACTOR = 2
|
||||
TARGET_VARIETY = 2
|
||||
STRENGTH_INFLUENCE = 0.3
|
||||
SUCCESS_TARGETS_HIT_PERCENTAGE = 0.5
|
||||
|
||||
targets = None # type: db.ArmorDict
|
||||
|
||||
@property
|
||||
def threat_description(self):
|
||||
if not self.game.is_player_attack(self):
|
||||
return "{} aicraft".format(self.from_cp.base.scramble_count(self.game.settings.multiplier, CAS))
|
||||
else:
|
||||
return super(GroundInterceptEvent, self).threat_description
|
||||
|
||||
def __str__(self):
|
||||
return "Fontline CAS from {} at {}".format(self.from_cp, self.to_cp)
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing):
|
||||
total_targets = sum(self.targets.values())
|
||||
destroyed_targets = 0
|
||||
for unit, count in debriefing.destroyed_units[self.defender_name].items():
|
||||
if unit in self.targets:
|
||||
destroyed_targets += count
|
||||
|
||||
if self.from_cp.captured:
|
||||
return float(destroyed_targets) / total_targets >= self.SUCCESS_TARGETS_HIT_PERCENTAGE
|
||||
else:
|
||||
return float(destroyed_targets) / total_targets < self.SUCCESS_TARGETS_HIT_PERCENTAGE
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
super(GroundInterceptEvent, self).commit(debriefing)
|
||||
|
||||
if self.from_cp.captured:
|
||||
if self.is_successfull(debriefing):
|
||||
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
else:
|
||||
self.to_cp.base.affect_strength(+self.STRENGTH_INFLUENCE)
|
||||
else:
|
||||
if self.is_successfull(debriefing):
|
||||
self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
else:
|
||||
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
|
||||
def skip(self):
|
||||
if self.to_cp.captured:
|
||||
self.to_cp.base.affect_strength(-0.1)
|
||||
|
||||
def player_attacking(self, strikegroup: db.PlaneDict, clients: db.PlaneDict):
|
||||
suitable_unittypes = db.find_unittype(PinpointStrike, self.defender_name)
|
||||
random.shuffle(suitable_unittypes)
|
||||
unittypes = suitable_unittypes[:self.TARGET_VARIETY]
|
||||
typecount = max(math.floor(self.difficulty * self.TARGET_AMOUNT_FACTOR), 1)
|
||||
self.targets = {unittype: typecount for unittype in unittypes}
|
||||
|
||||
defense_aa_unit = random.choice(self.game.commision_unit_types(self.to_cp, AirDefence))
|
||||
self.targets[defense_aa_unit] = 1
|
||||
|
||||
op = GroundInterceptOperation(game=self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker_clients=clients,
|
||||
defender_clients={},
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp)
|
||||
op.setup(target=self.targets,
|
||||
strikegroup=strikegroup,
|
||||
interceptors={})
|
||||
|
||||
self.operation = op
|
||||
|
||||
def player_defending(self, interceptors: db.PlaneDict, clients: db.PlaneDict):
|
||||
suitable_unittypes = db.find_unittype(PinpointStrike, self.defender_name)
|
||||
random.shuffle(suitable_unittypes)
|
||||
unittypes = suitable_unittypes[:self.TARGET_VARIETY]
|
||||
typecount = max(math.floor(self.difficulty * self.TARGET_AMOUNT_FACTOR), 1)
|
||||
self.targets = {unittype: typecount for unittype in unittypes}
|
||||
|
||||
op = GroundInterceptOperation(
|
||||
self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker_clients={},
|
||||
defender_clients=clients,
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp
|
||||
)
|
||||
|
||||
strikegroup = self.from_cp.base.scramble_cas(self.game.settings.multiplier)
|
||||
op.setup(target=self.targets,
|
||||
strikegroup=strikegroup,
|
||||
interceptors=interceptors)
|
||||
|
||||
self.operation = op
|
||||
@@ -1,47 +0,0 @@
|
||||
import math
|
||||
import random
|
||||
|
||||
from dcs.task import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from game import db
|
||||
from game.operation.infantrytransport import InfantryTransportOperation
|
||||
from theater.conflicttheater import *
|
||||
from userdata.debriefing import Debriefing
|
||||
|
||||
from .event import Event
|
||||
|
||||
|
||||
class InfantryTransportEvent(Event):
|
||||
STRENGTH_INFLUENCE = 0.3
|
||||
|
||||
def __str__(self):
|
||||
return "Frontline transport troops to {}".format(self.to_cp)
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing):
|
||||
return True
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
super(InfantryTransportEvent, self).commit(debriefing)
|
||||
|
||||
if self.is_successfull(debriefing):
|
||||
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
else:
|
||||
self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
|
||||
def player_attacking(self, transport: db.HeliDict, clients: db.HeliDict):
|
||||
op = InfantryTransportOperation(
|
||||
game=self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker_clients=clients,
|
||||
defender_clients={},
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp
|
||||
)
|
||||
|
||||
air_defense = db.find_unittype(AirDefence, self.defender_name)[0]
|
||||
op.setup(transport=transport,
|
||||
aa={air_defense: 2})
|
||||
|
||||
self.operation = op
|
||||
@@ -1,100 +0,0 @@
|
||||
import math
|
||||
import random
|
||||
|
||||
from dcs.task import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from game import db
|
||||
from game.operation.intercept import InterceptOperation
|
||||
from theater.conflicttheater import *
|
||||
from userdata.debriefing import Debriefing
|
||||
|
||||
from .event import Event
|
||||
|
||||
|
||||
class InterceptEvent(Event):
|
||||
STRENGTH_INFLUENCE = 0.3
|
||||
GLOBAL_STRENGTH_INFLUENCE = 0.3
|
||||
AIRDEFENSE_COUNT = 3
|
||||
|
||||
transport_unit = None # type: FlyingType
|
||||
|
||||
def __str__(self):
|
||||
return "Intercept from {} at {}".format(self.from_cp, self.to_cp)
|
||||
|
||||
@property
|
||||
def threat_description(self):
|
||||
return "{} aircraft".format(self.enemy_cp.base.scramble_count(self.game.settings.multiplier, CAP))
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing):
|
||||
units_destroyed = debriefing.destroyed_units[self.defender_name].get(self.transport_unit, 0)
|
||||
if self.from_cp.captured:
|
||||
return units_destroyed > 0
|
||||
else:
|
||||
return units_destroyed == 0
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
super(InterceptEvent, self).commit(debriefing)
|
||||
|
||||
if self.attacker_name == self.game.player:
|
||||
if self.is_successfull(debriefing):
|
||||
for _, cp in self.game.theater.conflicts(True):
|
||||
cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
else:
|
||||
self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
else:
|
||||
# enemy attacking
|
||||
if self.is_successfull(debriefing):
|
||||
self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
else:
|
||||
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
|
||||
def skip(self):
|
||||
if self.to_cp.captured:
|
||||
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
|
||||
def player_attacking(self, interceptors: db.PlaneDict, clients: db.PlaneDict):
|
||||
escort = self.to_cp.base.scramble_sweep(self.game.settings.multiplier)
|
||||
|
||||
self.transport_unit = random.choice(db.find_unittype(Transport, self.defender_name))
|
||||
assert self.transport_unit is not None
|
||||
|
||||
airdefense_unit = db.find_unittype(AirDefence, self.defender_name)[-1]
|
||||
op = InterceptOperation(game=self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker_clients=clients,
|
||||
defender_clients={},
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp)
|
||||
|
||||
op.setup(escort=escort,
|
||||
transport={self.transport_unit: 1},
|
||||
airdefense={airdefense_unit: self.AIRDEFENSE_COUNT},
|
||||
interceptors=interceptors)
|
||||
|
||||
self.operation = op
|
||||
|
||||
def player_defending(self, escort: db.PlaneDict, clients: db.PlaneDict):
|
||||
# TODO: even not quick mission is too quick
|
||||
interceptors = self.from_cp.base.scramble_interceptors(self.game.settings.multiplier)
|
||||
|
||||
self.transport_unit = random.choice(db.find_unittype(Transport, self.defender_name))
|
||||
assert self.transport_unit is not None
|
||||
|
||||
op = InterceptOperation(game=self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker_clients={},
|
||||
defender_clients=clients,
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp)
|
||||
|
||||
op.setup(escort=escort,
|
||||
transport={self.transport_unit: 1},
|
||||
interceptors=interceptors,
|
||||
airdefense={})
|
||||
|
||||
self.operation = op
|
||||
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import typing
|
||||
import math
|
||||
import random
|
||||
|
||||
from dcs.task import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from game import db
|
||||
from game.operation.navalintercept import NavalInterceptionOperation
|
||||
from userdata.debriefing import Debriefing
|
||||
|
||||
from .event import Event
|
||||
|
||||
|
||||
class NavalInterceptEvent(Event):
|
||||
STRENGTH_INFLUENCE = 0.3
|
||||
SUCCESS_RATE = 0.5
|
||||
|
||||
targets = None # type: db.ShipDict
|
||||
|
||||
def _targets_count(self) -> int:
|
||||
from gen.conflictgen import IMPORTANCE_LOW, IMPORTANCE_HIGH
|
||||
factor = (self.to_cp.importance - IMPORTANCE_LOW) * 10
|
||||
return max(int(factor), 1)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "Naval intercept at {}".format(self.to_cp)
|
||||
|
||||
@property
|
||||
def threat_description(self):
|
||||
s = "{} ship(s)".format(self._targets_count())
|
||||
if not self.from_cp.captured:
|
||||
s += ", {} aircraft".format(self.from_cp.base.scramble_count(self.game.settings.multiplier))
|
||||
return s
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing):
|
||||
total_targets = sum(self.targets.values())
|
||||
destroyed_targets = 0
|
||||
for unit, count in debriefing.destroyed_units[self.defender_name].items():
|
||||
if unit in self.targets:
|
||||
destroyed_targets += count
|
||||
|
||||
if self.from_cp.captured:
|
||||
return math.ceil(float(destroyed_targets) / total_targets) > self.SUCCESS_RATE
|
||||
else:
|
||||
return math.ceil(float(destroyed_targets) / total_targets) < self.SUCCESS_RATE
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
super(NavalInterceptEvent, self).commit(debriefing)
|
||||
|
||||
if self.attacker_name == self.game.player:
|
||||
if self.is_successfull(debriefing):
|
||||
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
else:
|
||||
self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
else:
|
||||
# enemy attacking
|
||||
if self.is_successfull(debriefing):
|
||||
self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
else:
|
||||
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
|
||||
def skip(self):
|
||||
if self.to_cp.captured:
|
||||
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
|
||||
|
||||
def player_attacking(self, strikegroup: db.PlaneDict, clients: db.PlaneDict):
|
||||
self.targets = {
|
||||
random.choice(db.find_unittype(CargoTransportation, self.defender_name)): self._targets_count(),
|
||||
}
|
||||
|
||||
op = NavalInterceptionOperation(
|
||||
self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker_clients=clients,
|
||||
defender_clients={},
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp
|
||||
)
|
||||
|
||||
op.setup(strikegroup=strikegroup,
|
||||
interceptors={},
|
||||
targets=self.targets)
|
||||
|
||||
self.operation = op
|
||||
|
||||
def player_defending(self, interceptors: db.PlaneDict, clients: db.PlaneDict):
|
||||
self.targets = {
|
||||
random.choice(db.find_unittype(CargoTransportation, self.defender_name)): self._targets_count(),
|
||||
}
|
||||
|
||||
op = NavalInterceptionOperation(
|
||||
self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker_clients={},
|
||||
defender_clients=clients,
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp
|
||||
)
|
||||
|
||||
strikegroup = self.from_cp.base.scramble_cas(self.game.settings.multiplier)
|
||||
op.setup(strikegroup=strikegroup,
|
||||
interceptors=interceptors,
|
||||
targets=self.targets)
|
||||
|
||||
self.operation = op
|
||||
269
game/factions/faction.py
Normal file
269
game/factions/faction.py
Normal file
@@ -0,0 +1,269 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict, Type, List, Any, cast
|
||||
|
||||
import dcs
|
||||
from dcs.countries import country_dict
|
||||
from dcs.planes import plane_map
|
||||
from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
|
||||
from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
|
||||
|
||||
from game.data.building_data import WW2_ALLIES_BUILDINGS, DEFAULT_AVAILABLE_BUILDINGS, WW2_GERMANY_BUILDINGS, WW2_FREE
|
||||
from game.data.doctrine import Doctrine, MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
|
||||
from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
|
||||
|
||||
|
||||
@dataclass
|
||||
class Faction:
|
||||
|
||||
# Country used by this faction
|
||||
country: str = field(default="")
|
||||
|
||||
# Nice name of the faction
|
||||
name: str = field(default="")
|
||||
|
||||
# List of faction file authors
|
||||
authors: str = field(default="")
|
||||
|
||||
# A description of the faction
|
||||
description: str = field(default="")
|
||||
|
||||
# Available aircraft
|
||||
aircrafts: List[UnitType] = field(default_factory=list)
|
||||
|
||||
# Available awacs aircraft
|
||||
awacs: List[UnitType] = field(default_factory=list)
|
||||
|
||||
# Available tanker aircraft
|
||||
tankers: List[UnitType] = field(default_factory=list)
|
||||
|
||||
# Available frontline units
|
||||
frontline_units: List[VehicleType] = field(default_factory=list)
|
||||
|
||||
# Available artillery units
|
||||
artillery_units: List[VehicleType] = field(default_factory=list)
|
||||
|
||||
# Infantry units used
|
||||
infantry_units: List[VehicleType] = field(default_factory=list)
|
||||
|
||||
# Logistics units used
|
||||
logistics_units: List[VehicleType] = field(default_factory=list)
|
||||
|
||||
# List of units that can be deployed as SHORAD
|
||||
shorads: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible SAMS site generators for this faction
|
||||
sams: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible EWR generators for this faction.
|
||||
ewrs: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible Missile site generators for this faction
|
||||
missiles: List[str] = field(default_factory=list)
|
||||
|
||||
# Required mods or asset packs
|
||||
requirements: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
# possible aircraft carrier units
|
||||
aircraft_carrier: List[UnitType] = field(default_factory=list)
|
||||
|
||||
# possible helicopter carrier units
|
||||
helicopter_carrier: List[UnitType] = field(default_factory=list)
|
||||
|
||||
# Possible carrier names
|
||||
carrier_names: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible helicopter carrier names
|
||||
helicopter_carrier_names: List[str] = field(default_factory=list)
|
||||
|
||||
# Navy group generators
|
||||
navy_generators: List[str] = field(default_factory=list)
|
||||
|
||||
# Available destroyers
|
||||
destroyers: List[str] = field(default_factory=list)
|
||||
|
||||
# Available cruisers
|
||||
cruisers: List[str] = field(default_factory=list)
|
||||
|
||||
# How many navy group should we try to generate per CP on startup for this faction
|
||||
navy_group_count: int = field(default=1)
|
||||
|
||||
# How many missiles group should we try to generate per CP on startup for this faction
|
||||
missiles_group_count: int = field(default=1)
|
||||
|
||||
# Whether this faction has JTAC access
|
||||
has_jtac: bool = field(default=False)
|
||||
|
||||
# Unit to use as JTAC for this faction
|
||||
jtac_unit: Optional[FlyingType] = field(default=None)
|
||||
|
||||
# doctrine
|
||||
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
|
||||
|
||||
# List of available buildings for this faction
|
||||
building_set: List[str] = field(default_factory=list)
|
||||
|
||||
# List of default livery overrides
|
||||
liveries_overrides: Dict[UnitType, List[str]] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
|
||||
faction = Faction()
|
||||
|
||||
faction.country = json.get("country", "/")
|
||||
if faction.country not in [c.name for c in country_dict.values()]:
|
||||
raise AssertionError("Faction's country (\"{}\") is not a valid DCS country ID".format(faction.country))
|
||||
|
||||
faction.name = json.get("name", "")
|
||||
if not faction.name:
|
||||
raise AssertionError("Faction has no valid name")
|
||||
|
||||
faction.authors = json.get("authors", "")
|
||||
faction.description = json.get("description", "")
|
||||
|
||||
faction.aircrafts = load_all_aircraft(json.get("aircrafts", []))
|
||||
faction.awacs = load_all_aircraft(json.get("awacs", []))
|
||||
faction.tankers = load_all_aircraft(json.get("tankers", []))
|
||||
|
||||
faction.frontline_units = load_all_vehicles(
|
||||
json.get("frontline_units", []))
|
||||
faction.artillery_units = load_all_vehicles(
|
||||
json.get("artillery_units", []))
|
||||
faction.infantry_units = load_all_vehicles(
|
||||
json.get("infantry_units", []))
|
||||
faction.logistics_units = load_all_vehicles(
|
||||
json.get("logistics_units", []))
|
||||
|
||||
faction.sams = json.get("sams", [])
|
||||
faction.ewrs = json.get("ewrs", [])
|
||||
faction.shorads = json.get("shorads", [])
|
||||
faction.missiles = json.get("missiles", [])
|
||||
faction.requirements = json.get("requirements", {})
|
||||
|
||||
faction.carrier_names = json.get("carrier_names", [])
|
||||
faction.helicopter_carrier_names = json.get(
|
||||
"helicopter_carrier_names", [])
|
||||
faction.navy_generators = json.get("navy_generators", [])
|
||||
faction.aircraft_carrier = load_all_ships(
|
||||
json.get("aircraft_carrier", []))
|
||||
faction.helicopter_carrier = load_all_ships(
|
||||
json.get("helicopter_carrier", []))
|
||||
faction.destroyers = load_all_ships(json.get("destroyers", []))
|
||||
faction.cruisers = load_all_ships(json.get("cruisers", []))
|
||||
faction.has_jtac = json.get("has_jtac", False)
|
||||
jtac_name = json.get("jtac_unit", None)
|
||||
if jtac_name is not None:
|
||||
faction.jtac_unit = load_aircraft(jtac_name)
|
||||
else:
|
||||
faction.jtac_unit = None
|
||||
faction.navy_group_count = int(json.get("navy_group_count", 1))
|
||||
faction.missiles_group_count = int(json.get("missiles_group_count", 0))
|
||||
|
||||
# Load doctrine
|
||||
doctrine = json.get("doctrine", "modern")
|
||||
if doctrine == "modern":
|
||||
faction.doctrine = MODERN_DOCTRINE
|
||||
elif doctrine == "coldwar":
|
||||
faction.doctrine = COLDWAR_DOCTRINE
|
||||
elif doctrine == "ww2":
|
||||
faction.doctrine = WWII_DOCTRINE
|
||||
else:
|
||||
faction.doctrine = MODERN_DOCTRINE
|
||||
|
||||
# Load the building set
|
||||
building_set = json.get("building_set", "default")
|
||||
if building_set == "default":
|
||||
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
|
||||
elif building_set == "ww2free":
|
||||
faction.building_set = WW2_FREE
|
||||
elif building_set == "ww2ally":
|
||||
faction.building_set = WW2_ALLIES_BUILDINGS
|
||||
elif building_set == "ww2germany":
|
||||
faction.building_set = WW2_GERMANY_BUILDINGS
|
||||
else:
|
||||
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
|
||||
|
||||
# Load liveries override
|
||||
faction.liveries_overrides = {}
|
||||
liveries_overrides = json.get("liveries_overrides", {})
|
||||
for k, v in liveries_overrides.items():
|
||||
k = load_aircraft(k)
|
||||
if k is not None:
|
||||
faction.liveries_overrides[k] = [s.lower() for s in v]
|
||||
|
||||
return faction
|
||||
|
||||
@property
|
||||
def units(self) -> List[UnitType]:
|
||||
return (self.infantry_units + self.aircrafts + self.awacs +
|
||||
self.artillery_units + self.frontline_units +
|
||||
self.tankers + self.logistics_units)
|
||||
|
||||
|
||||
def unit_loader(unit: str, class_repository: List[Any]) -> Optional[UnitType]:
|
||||
"""
|
||||
Find unit by name
|
||||
:param unit: Unit name as string
|
||||
:param class_repository: Repository of classes (Either a module, a class, or a list of classes)
|
||||
:return: The unit as a PyDCS type
|
||||
"""
|
||||
if unit is None:
|
||||
return None
|
||||
elif unit in plane_map.keys():
|
||||
return plane_map[unit]
|
||||
else:
|
||||
for mother_class in class_repository:
|
||||
if getattr(mother_class, unit, None) is not None:
|
||||
return getattr(mother_class, unit)
|
||||
if type(mother_class) is list:
|
||||
for m in mother_class:
|
||||
if m.__name__ == unit:
|
||||
return m
|
||||
logging.error(f"FACTION ERROR : Unable to find {unit} in pydcs")
|
||||
return None
|
||||
|
||||
|
||||
def load_aircraft(name: str) -> Optional[FlyingType]:
|
||||
return cast(Optional[FlyingType], unit_loader(
|
||||
name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]
|
||||
))
|
||||
|
||||
|
||||
def load_all_aircraft(data) -> List[FlyingType]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_aircraft(name)
|
||||
if item is not None:
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def load_vehicle(name: str) -> Optional[VehicleType]:
|
||||
return cast(Optional[FlyingType], unit_loader(
|
||||
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
|
||||
))
|
||||
|
||||
|
||||
def load_all_vehicles(data) -> List[VehicleType]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_vehicle(name)
|
||||
if item is not None:
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def load_ship(name: str) -> Optional[ShipType]:
|
||||
return cast(Optional[FlyingType], unit_loader(name, [dcs.ships]))
|
||||
|
||||
|
||||
def load_all_ships(data) -> List[ShipType]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_ship(name)
|
||||
if item is not None:
|
||||
items.append(item)
|
||||
return items
|
||||
46
game/factions/faction_loader.py
Normal file
46
game/factions/faction_loader.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterator, Optional, Type
|
||||
|
||||
from game.factions.faction import Faction
|
||||
|
||||
FACTION_DIRECTORY = Path("./resources/factions/")
|
||||
|
||||
|
||||
class FactionLoader:
|
||||
def __init__(self) -> None:
|
||||
self._factions: Optional[Dict[str, Faction]] = None
|
||||
|
||||
@property
|
||||
def factions(self) -> Dict[str, Faction]:
|
||||
self.initialize()
|
||||
assert self._factions is not None
|
||||
return self._factions
|
||||
|
||||
def initialize(self) -> None:
|
||||
if self._factions is None:
|
||||
self._factions = self.load_factions()
|
||||
|
||||
@classmethod
|
||||
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
|
||||
files = [f for f in FACTION_DIRECTORY.glob("*.json") if f.is_file()]
|
||||
factions = {}
|
||||
|
||||
for f in files:
|
||||
try:
|
||||
with f.open("r", encoding="utf-8") as fdata:
|
||||
data = json.load(fdata, encoding="utf-8")
|
||||
factions[data["name"]] = Faction.from_json(data)
|
||||
logging.info("Loaded faction : " + str(f))
|
||||
except Exception:
|
||||
logging.exception(f"Unable to load faction : {f}")
|
||||
|
||||
return factions
|
||||
|
||||
def __getitem__(self, name: str) -> Faction:
|
||||
return self.factions[name]
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self.factions.keys())
|
||||
509
game/game.py
509
game/game.py
@@ -1,16 +1,36 @@
|
||||
import typing
|
||||
import random
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Dict, List
|
||||
|
||||
from dcs.task import *
|
||||
from dcs.vehicles import *
|
||||
from dcs.action import Coalition
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike, Task
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from userdata.debriefing import Debriefing
|
||||
from theater import *
|
||||
|
||||
from . import db
|
||||
from game import db
|
||||
from game.db import PLAYER_BUDGET_BASE, REWARDS
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
from game.models.game_stats import GameStats
|
||||
from game.plugins import LuaPluginManager
|
||||
from gen.ato import AirTaskingOrder
|
||||
from gen.conflictgen import Conflict
|
||||
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
||||
from theater import ConflictTheater, ControlPoint
|
||||
from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
|
||||
from . import persistency
|
||||
from .debriefing import Debriefing
|
||||
from .event.event import Event, UnitsDeliveryEvent
|
||||
from .event.frontlineattack import FrontlineAttackEvent
|
||||
from .factions.faction import Faction
|
||||
from .infos.information import Information
|
||||
from .settings import Settings
|
||||
from .event import *
|
||||
from .weather import Conditions, TimeOfDay
|
||||
|
||||
COMMISION_UNIT_VARIETY = 4
|
||||
COMMISION_LIMITS_SCALE = 1.5
|
||||
@@ -18,42 +38,20 @@ COMMISION_LIMITS_FACTORS = {
|
||||
PinpointStrike: 10,
|
||||
CAS: 5,
|
||||
CAP: 8,
|
||||
AirDefence: 1,
|
||||
AirDefence: 8,
|
||||
}
|
||||
|
||||
COMMISION_AMOUNTS_SCALE = 1.5
|
||||
COMMISION_AMOUNTS_FACTORS = {
|
||||
PinpointStrike: 2,
|
||||
PinpointStrike: 3,
|
||||
CAS: 1,
|
||||
CAP: 2,
|
||||
AirDefence: 0.3,
|
||||
AirDefence: 0.8,
|
||||
}
|
||||
|
||||
PLAYER_INTERCEPT_GLOBAL_PROBABILITY_BASE = 25
|
||||
PLAYER_INTERCEPT_GLOBAL_PROBABILITY_BASE = 30
|
||||
PLAYER_INTERCEPT_GLOBAL_PROBABILITY_LOG = 2
|
||||
|
||||
"""
|
||||
Various events probabilities. First key is player probabilty, second is enemy probability.
|
||||
For the enemy events, only 1 event of each type could be generated for a turn.
|
||||
|
||||
Events:
|
||||
* CaptureEvent - capture base
|
||||
* InterceptEvent - air intercept
|
||||
* GroundInterceptEvent - frontline CAS
|
||||
* GroundAttackEvent - destroy insurgents
|
||||
* NavalInterceptEvent - naval intercept
|
||||
* AntiAAStrikeEvent - anti-AA strike
|
||||
* InfantryTransportEvent - helicopter infantry transport
|
||||
"""
|
||||
EVENT_PROBABILITIES = {
|
||||
CaptureEvent: [100, 10],
|
||||
InterceptEvent: [25, 10],
|
||||
GroundInterceptEvent: [25, 10],
|
||||
GroundAttackEvent: [0, 10],
|
||||
NavalInterceptEvent: [25, 10],
|
||||
AntiAAStrikeEvent: [25, 10],
|
||||
InfantryTransportEvent: [25, 0],
|
||||
}
|
||||
PLAYER_BASEATTACK_THRESHOLD = 0.4
|
||||
|
||||
# amount of strength player bases recover for the turn
|
||||
PLAYER_BASE_STRENGTH_RECOVERY = 0.2
|
||||
@@ -65,112 +63,102 @@ ENEMY_BASE_STRENGTH_RECOVERY = 0.05
|
||||
AWACS_BUDGET_COST = 4
|
||||
|
||||
# Initial budget value
|
||||
PLAYER_BUDGET_INITIAL = 120
|
||||
# Base post-turn bonus value
|
||||
PLAYER_BUDGET_BASE = 10
|
||||
PLAYER_BUDGET_INITIAL = 650
|
||||
|
||||
# Bonus multiplier logarithm base
|
||||
PLAYER_BUDGET_IMPORTANCE_LOG = 2
|
||||
|
||||
|
||||
class Game:
|
||||
settings = None # type: Settings
|
||||
budget = PLAYER_BUDGET_INITIAL
|
||||
events = None # type: typing.List[Event]
|
||||
pending_transfers = None # type: typing.Dict[]
|
||||
ignored_cps = None # type: typing.Collection[ControlPoint]
|
||||
|
||||
def __init__(self, player_name: str, enemy_name: str, theater: ConflictTheater):
|
||||
self.settings = Settings()
|
||||
self.events = []
|
||||
def __init__(self, player_name: str, enemy_name: str,
|
||||
theater: ConflictTheater, start_date: datetime,
|
||||
settings: Settings):
|
||||
self.settings = settings
|
||||
self.events: List[Event] = []
|
||||
self.theater = theater
|
||||
self.player = player_name
|
||||
self.enemy = enemy_name
|
||||
self.player_name = player_name
|
||||
self.player_country = db.FACTIONS[player_name].country
|
||||
self.enemy_name = enemy_name
|
||||
self.enemy_country = db.FACTIONS[enemy_name].country
|
||||
self.turn = 0
|
||||
self.date = date(start_date.year, start_date.month, start_date.day)
|
||||
self.game_stats = GameStats()
|
||||
self.game_stats.update(self)
|
||||
self.ground_planners: Dict[int, GroundPlanner] = {}
|
||||
self.informations = []
|
||||
self.informations.append(Information("Game Start", "-" * 40, 0))
|
||||
self.__culling_points = self.compute_conflicts_position()
|
||||
self.__destroyed_units: List[str] = []
|
||||
self.savepath = ""
|
||||
self.budget = PLAYER_BUDGET_INITIAL
|
||||
self.current_unit_id = 0
|
||||
self.current_group_id = 0
|
||||
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
self.blue_ato = AirTaskingOrder()
|
||||
self.red_ato = AirTaskingOrder()
|
||||
|
||||
self.aircraft_inventory = GlobalAircraftInventory(
|
||||
self.theater.controlpoints
|
||||
)
|
||||
|
||||
self.sanitize_sides()
|
||||
self.on_load()
|
||||
|
||||
def generate_conditions(self) -> Conditions:
|
||||
return Conditions.generate(self.theater, self.date,
|
||||
self.current_turn_time_of_day, self.settings)
|
||||
|
||||
def sanitize_sides(self):
|
||||
"""
|
||||
Make sure the opposing factions are using different countries
|
||||
:return:
|
||||
"""
|
||||
if self.player_country == self.enemy_country:
|
||||
if self.player_country == "USA":
|
||||
self.enemy_country = "USAF Aggressors"
|
||||
elif self.player_country == "Russia":
|
||||
self.enemy_country = "USSR"
|
||||
else:
|
||||
self.enemy_country = "Russia"
|
||||
|
||||
@property
|
||||
def player_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.player_name]
|
||||
|
||||
@property
|
||||
def enemy_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.enemy_name]
|
||||
|
||||
def _roll(self, prob, mult):
|
||||
return random.randint(1, 100) <= prob * mult
|
||||
if self.settings.version == "dev":
|
||||
# always generate all events for dev
|
||||
return 100
|
||||
else:
|
||||
return random.randint(1, 100) <= prob * mult
|
||||
|
||||
def _generate_globalinterceptions(self):
|
||||
global_count = len([x for x in self.theater.player_points() if x.is_global])
|
||||
for from_cp in [x for x in self.theater.player_points() if x.is_global]:
|
||||
probability_base = max(PLAYER_INTERCEPT_GLOBAL_PROBABILITY_BASE / global_count, 1)
|
||||
probability = probability_base * math.log(len(self.theater.player_points()) + 1, PLAYER_INTERCEPT_GLOBAL_PROBABILITY_LOG)
|
||||
if self._roll(probability, from_cp.base.strength):
|
||||
to_cp = random.choice([x for x in self.theater.enemy_points() if x not in self.theater.conflicts()])
|
||||
self.events.append(InterceptEvent(attacker_name=self.player,
|
||||
defender_name=self.enemy,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
game=self))
|
||||
break
|
||||
def _generate_player_event(self, event_class, player_cp, enemy_cp):
|
||||
self.events.append(event_class(self, player_cp, enemy_cp, enemy_cp.position, self.player_name, self.enemy_name))
|
||||
|
||||
def _generate_events(self):
|
||||
enemy_cap_generated = False
|
||||
enemy_generated_types = []
|
||||
|
||||
for player_cp, enemy_cp in self.theater.conflicts(True):
|
||||
if player_cp.is_global or enemy_cp.is_global:
|
||||
continue
|
||||
|
||||
for event_class, (player_probability, enemy_probability) in EVENT_PROBABILITIES.items():
|
||||
if self._roll(player_probability, player_cp.base.strength):
|
||||
if event_class == NavalInterceptEvent and enemy_cp.radials == LAND:
|
||||
pass
|
||||
else:
|
||||
self.events.append(event_class(self.player, self.enemy, player_cp, enemy_cp, self))
|
||||
elif self._roll(enemy_probability, enemy_cp.base.strength):
|
||||
if event_class in enemy_generated_types:
|
||||
continue
|
||||
|
||||
if player_cp in self.ignored_cps:
|
||||
continue
|
||||
|
||||
if enemy_cp.base.total_planes == 0:
|
||||
continue
|
||||
|
||||
if event_class == NavalInterceptEvent:
|
||||
if player_cp.radials == LAND:
|
||||
continue
|
||||
elif event_class == CaptureEvent:
|
||||
if enemy_cap_generated:
|
||||
continue
|
||||
if enemy_cp.base.total_armor == 0:
|
||||
continue
|
||||
enemy_cap_generated = True
|
||||
elif event_class == AntiAAStrikeEvent:
|
||||
if player_cp.base.total_aa == 0:
|
||||
continue
|
||||
|
||||
enemy_generated_types.append(event_class)
|
||||
self.events.append(event_class(self.enemy, self.player, enemy_cp, player_cp, self))
|
||||
|
||||
def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> typing.Collection[UnitType]:
|
||||
importance_factor = (cp.importance - IMPORTANCE_LOW) / (IMPORTANCE_HIGH - IMPORTANCE_LOW)
|
||||
|
||||
if for_task == AirDefence and not self.settings.sams:
|
||||
return [x for x in db.find_unittype(AirDefence, self.enemy) if x not in db.SAM_BAN]
|
||||
else:
|
||||
return db.choose_units(for_task, importance_factor, COMMISION_UNIT_VARIETY, self.enemy)
|
||||
|
||||
def _commision_units(self, cp: ControlPoint):
|
||||
for for_task in [PinpointStrike, CAS, CAP, AirDefence]:
|
||||
limit = COMMISION_LIMITS_FACTORS[for_task] * math.pow(cp.importance, COMMISION_LIMITS_SCALE) * self.settings.multiplier
|
||||
missing_units = limit - cp.base.total_units(for_task)
|
||||
if missing_units > 0:
|
||||
awarded_points = COMMISION_AMOUNTS_FACTORS[for_task] * math.pow(cp.importance, COMMISION_AMOUNTS_SCALE) * self.settings.multiplier
|
||||
points_to_spend = cp.base.append_commision_points(for_task, awarded_points)
|
||||
if points_to_spend > 0:
|
||||
unittypes = self.commision_unit_types(cp, for_task)
|
||||
d = {random.choice(unittypes): points_to_spend}
|
||||
print("Commision {}: {}".format(cp, d))
|
||||
cp.base.commision_units(d)
|
||||
for front_line in self.theater.conflicts(True):
|
||||
self._generate_player_event(FrontlineAttackEvent,
|
||||
front_line.control_point_a,
|
||||
front_line.control_point_b)
|
||||
|
||||
@property
|
||||
def budget_reward_amount(self):
|
||||
reward = 0
|
||||
if len(self.theater.player_points()) > 0:
|
||||
total_importance = sum([x.importance * x.base.strength for x in self.theater.player_points()])
|
||||
return math.ceil(math.log(total_importance + 1, PLAYER_BUDGET_IMPORTANCE_LOG) * PLAYER_BUDGET_BASE * self.settings.multiplier)
|
||||
reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
|
||||
for cp in self.theater.player_points():
|
||||
for g in cp.ground_objects:
|
||||
if g.category in REWARDS.keys() and not g.is_dead:
|
||||
reward = reward + REWARDS[g.category]
|
||||
return reward
|
||||
else:
|
||||
return 0
|
||||
return reward
|
||||
|
||||
def _budget_player(self):
|
||||
self.budget += self.budget_reward_amount
|
||||
@@ -179,8 +167,8 @@ class Game:
|
||||
self.budget -= AWACS_BUDGET_COST
|
||||
|
||||
def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent:
|
||||
event = UnitsDeliveryEvent(attacker_name=self.player,
|
||||
defender_name=self.player,
|
||||
event = UnitsDeliveryEvent(attacker_name=self.player_name,
|
||||
defender_name=self.player_name,
|
||||
from_cp=to_cp,
|
||||
to_cp=to_cp,
|
||||
game=self)
|
||||
@@ -192,12 +180,12 @@ class Game:
|
||||
self.events.remove(event)
|
||||
|
||||
def initiate_event(self, event: Event):
|
||||
assert event in self.events
|
||||
|
||||
#assert event in self.events
|
||||
logging.info("Generating {} (regular)".format(event))
|
||||
event.generate()
|
||||
event.generate_quick()
|
||||
|
||||
def finish_event(self, event: Event, debriefing: Debriefing):
|
||||
logging.info("Finishing event {}".format(event))
|
||||
event.commit(debriefing)
|
||||
if event.is_successfull(debriefing):
|
||||
self.budget += event.bonus()
|
||||
@@ -205,29 +193,258 @@ class Game:
|
||||
if event in self.events:
|
||||
self.events.remove(event)
|
||||
else:
|
||||
print("finish_event: event not in the events!")
|
||||
logging.info("finish_event: event not in the events!")
|
||||
|
||||
def is_player_attack(self, event: Event):
|
||||
return event.attacker_name == self.player
|
||||
def is_player_attack(self, event):
|
||||
if isinstance(event, Event):
|
||||
return event and event.attacker_name and event.attacker_name == self.player_name
|
||||
else:
|
||||
return event and event.name and event.name == self.player_name
|
||||
|
||||
def on_load(self) -> None:
|
||||
LuaPluginManager.load_settings(self.settings)
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
|
||||
# Save game compatibility.
|
||||
|
||||
# TODO: Remove in 2.3.
|
||||
if not hasattr(self, "conditions"):
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
def pass_turn(self, no_action: bool = False) -> None:
|
||||
logging.info("Pass turn")
|
||||
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
|
||||
self.turn += 1
|
||||
|
||||
def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint]=None):
|
||||
for event in self.events:
|
||||
event.skip()
|
||||
if self.settings.version == "dev":
|
||||
# don't damage player CPs in by skipping in dev mode
|
||||
if isinstance(event, UnitsDeliveryEvent):
|
||||
event.skip()
|
||||
else:
|
||||
event.skip()
|
||||
|
||||
if not no_action:
|
||||
self._budget_player()
|
||||
|
||||
for cp in self.theater.enemy_points():
|
||||
self._commision_units(cp)
|
||||
self._enemy_reinforcement()
|
||||
self._budget_player()
|
||||
|
||||
if not no_action and self.turn > 1:
|
||||
for cp in self.theater.player_points():
|
||||
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
|
||||
else:
|
||||
for cp in self.theater.player_points():
|
||||
if not cp.is_carrier and not cp.is_lha:
|
||||
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)
|
||||
|
||||
self.ignored_cps = []
|
||||
if ignored_cps:
|
||||
self.ignored_cps = ignored_cps
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
self.events = [] # type: typing.List[Event]
|
||||
self.initialize_turn()
|
||||
|
||||
# Autosave progress
|
||||
persistency.autosave(self)
|
||||
|
||||
def initialize_turn(self) -> None:
|
||||
self.events = []
|
||||
self._generate_events()
|
||||
self._generate_globalinterceptions()
|
||||
|
||||
# Update statistics
|
||||
self.game_stats.update(self)
|
||||
|
||||
self.aircraft_inventory.reset()
|
||||
for cp in self.theater.controlpoints:
|
||||
self.aircraft_inventory.set_from_control_point(cp)
|
||||
|
||||
# Plan flights & combat for next turn
|
||||
self.__culling_points = self.compute_conflicts_position()
|
||||
self.ground_planners = {}
|
||||
self.blue_ato.clear()
|
||||
self.red_ato.clear()
|
||||
CoalitionMissionPlanner(self, is_player=True).plan_missions()
|
||||
CoalitionMissionPlanner(self, is_player=False).plan_missions()
|
||||
for cp in self.theater.controlpoints:
|
||||
if cp.has_frontline:
|
||||
gplanner = GroundPlanner(cp, self)
|
||||
gplanner.plan_groundwar()
|
||||
self.ground_planners[cp.id] = gplanner
|
||||
|
||||
def _enemy_reinforcement(self):
|
||||
"""
|
||||
Compute and commision reinforcement for enemy bases
|
||||
"""
|
||||
|
||||
MAX_ARMOR = 30 * self.settings.multiplier
|
||||
MAX_AIRCRAFT = 25 * self.settings.multiplier
|
||||
|
||||
production = 0.0
|
||||
for enemy_point in self.theater.enemy_points():
|
||||
for g in enemy_point.ground_objects:
|
||||
if g.category in REWARDS.keys() and not g.is_dead:
|
||||
production = production + REWARDS[g.category]
|
||||
|
||||
production = production * 0.75
|
||||
budget_for_armored_units = production / 2
|
||||
budget_for_aircraft = production / 2
|
||||
|
||||
potential_cp_armor = []
|
||||
for cp in self.theater.enemy_points():
|
||||
for cpe in cp.connected_points:
|
||||
if cpe.captured and cp.base.total_armor < MAX_ARMOR:
|
||||
potential_cp_armor.append(cp)
|
||||
if len(potential_cp_armor) == 0:
|
||||
potential_cp_armor = self.theater.enemy_points()
|
||||
|
||||
i = 0
|
||||
potential_units = db.FACTIONS[self.enemy_name].frontline_units
|
||||
|
||||
print("Enemy Recruiting")
|
||||
print(potential_cp_armor)
|
||||
print(budget_for_armored_units)
|
||||
print(potential_units)
|
||||
|
||||
if len(potential_units) > 0 and len(potential_cp_armor) > 0:
|
||||
while budget_for_armored_units > 0:
|
||||
i = i + 1
|
||||
if i > 50 or budget_for_armored_units <= 0:
|
||||
break
|
||||
target_cp = random.choice(potential_cp_armor)
|
||||
if target_cp.base.total_armor >= MAX_ARMOR:
|
||||
continue
|
||||
unit = random.choice(potential_units)
|
||||
price = db.PRICES[unit] * 2
|
||||
budget_for_armored_units -= price * 2
|
||||
target_cp.base.armor[unit] = target_cp.base.armor.get(unit, 0) + 2
|
||||
info = Information("Enemy Reinforcement", unit.id + " x 2 at " + target_cp.name, self.turn)
|
||||
print(str(info))
|
||||
self.informations.append(info)
|
||||
|
||||
if budget_for_armored_units > 0:
|
||||
budget_for_aircraft += budget_for_armored_units
|
||||
|
||||
potential_units = [u for u in db.FACTIONS[self.enemy_name].aircrafts
|
||||
if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
|
||||
|
||||
if len(potential_units) > 0 and len(potential_cp_armor) > 0:
|
||||
while budget_for_aircraft > 0:
|
||||
i = i + 1
|
||||
if i > 50 or budget_for_aircraft <= 0:
|
||||
break
|
||||
target_cp = random.choice(potential_cp_armor)
|
||||
if target_cp.base.total_planes >= MAX_AIRCRAFT:
|
||||
continue
|
||||
unit = random.choice(potential_units)
|
||||
price = db.PRICES[unit] * 2
|
||||
budget_for_aircraft -= price * 2
|
||||
target_cp.base.aircraft[unit] = target_cp.base.aircraft.get(unit, 0) + 2
|
||||
info = Information("Enemy Reinforcement", unit.id + " x 2 at " + target_cp.name, self.turn)
|
||||
print(str(info))
|
||||
self.informations.append(info)
|
||||
|
||||
@property
|
||||
def current_turn_time_of_day(self) -> TimeOfDay:
|
||||
return list(TimeOfDay)[self.turn % 4]
|
||||
|
||||
@property
|
||||
def current_day(self) -> date:
|
||||
return self.date + timedelta(days=self.turn // 4)
|
||||
|
||||
def next_unit_id(self):
|
||||
"""
|
||||
Next unit id for pre-generated units
|
||||
"""
|
||||
self.current_unit_id += 1
|
||||
return self.current_unit_id
|
||||
|
||||
def next_group_id(self):
|
||||
"""
|
||||
Next unit id for pre-generated units
|
||||
"""
|
||||
self.current_group_id += 1
|
||||
return self.current_group_id
|
||||
|
||||
def compute_conflicts_position(self):
|
||||
"""
|
||||
Compute the current conflict center position(s), mainly used for culling calculation
|
||||
:return: List of points of interests
|
||||
"""
|
||||
points = []
|
||||
|
||||
# By default, use the existing frontline conflict position
|
||||
for front_line in self.theater.conflicts():
|
||||
position = Conflict.frontline_position(self.theater,
|
||||
front_line.control_point_a,
|
||||
front_line.control_point_b)
|
||||
points.append(position[0])
|
||||
points.append(front_line.control_point_a.position)
|
||||
points.append(front_line.control_point_b.position)
|
||||
|
||||
# If there is no conflict take the center point between the two nearest opposing bases
|
||||
if len(points) == 0:
|
||||
cpoint = None
|
||||
min_distance = sys.maxsize
|
||||
for cp in self.theater.player_points():
|
||||
for cp2 in self.theater.enemy_points():
|
||||
d = cp.position.distance_to_point(cp2.position)
|
||||
if d < min_distance:
|
||||
min_distance = d
|
||||
cpoint = Point((cp.position.x + cp2.position.x) / 2, (cp.position.y + cp2.position.y) / 2)
|
||||
points.append(cp.position)
|
||||
points.append(cp2.position)
|
||||
break
|
||||
if cpoint is not None:
|
||||
break
|
||||
if cpoint is not None:
|
||||
points.append(cpoint)
|
||||
|
||||
# Else 0,0, since we need a default value
|
||||
# (in this case this means the whole map is owned by the same player, so it is not an issue)
|
||||
if len(points) == 0:
|
||||
points.append(Point(0, 0))
|
||||
|
||||
return points
|
||||
|
||||
def add_destroyed_units(self, data):
|
||||
pos = Point(data["x"], data["z"])
|
||||
if self.theater.is_on_land(pos):
|
||||
self.__destroyed_units.append(data)
|
||||
|
||||
def get_destroyed_units(self):
|
||||
return self.__destroyed_units
|
||||
|
||||
def position_culled(self, pos):
|
||||
"""
|
||||
Check if unit can be generated at given position depending on culling performance settings
|
||||
:param pos: Position you are tryng to spawn stuff at
|
||||
:return: True if units can not be added at given position
|
||||
"""
|
||||
if self.settings.perf_culling == False:
|
||||
return False
|
||||
else:
|
||||
for c in self.__culling_points:
|
||||
if c.distance_to_point(pos) < self.settings.perf_culling_distance * 1000:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_culling_points(self):
|
||||
"""
|
||||
Check culling points
|
||||
:return: List of culling points
|
||||
"""
|
||||
return self.__culling_points
|
||||
|
||||
# 1 = red, 2 = blue
|
||||
def get_player_coalition_id(self):
|
||||
return 2
|
||||
|
||||
def get_enemy_coalition_id(self):
|
||||
return 1
|
||||
|
||||
def get_player_coalition(self):
|
||||
return Coalition.Blue
|
||||
|
||||
def get_enemy_coalition(self):
|
||||
return Coalition.Red
|
||||
|
||||
def get_player_color(self):
|
||||
return "blue"
|
||||
|
||||
def get_enemy_color(self):
|
||||
return "red"
|
||||
11
game/infos/information.py
Normal file
11
game/infos/information.py
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
class Information():
|
||||
|
||||
def __init__(self, title="", text="", turn=0):
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.turn = turn
|
||||
|
||||
def __str__(self):
|
||||
s = "[" + str(self.turn) + "] " + self.title + "\n" + self.text
|
||||
return s
|
||||
123
game/inventory.py
Normal file
123
game/inventory.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Inventory management APIs."""
|
||||
from collections import defaultdict
|
||||
from typing import Dict, Iterable, Iterator, Set, Tuple
|
||||
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from gen.flights.flight import Flight
|
||||
from theater import ControlPoint
|
||||
|
||||
|
||||
class ControlPointAircraftInventory:
|
||||
"""Aircraft inventory for a single control point."""
|
||||
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
self.control_point = control_point
|
||||
self.inventory: Dict[UnitType, int] = defaultdict(int)
|
||||
|
||||
def add_aircraft(self, aircraft: UnitType, count: int) -> None:
|
||||
"""Adds aircraft to the inventory.
|
||||
|
||||
Args:
|
||||
aircraft: The type of aircraft to add.
|
||||
count: The number of aircraft to add.
|
||||
"""
|
||||
self.inventory[aircraft] += count
|
||||
|
||||
def remove_aircraft(self, aircraft: UnitType, count: int) -> None:
|
||||
"""Removes aircraft from the inventory.
|
||||
|
||||
Args:
|
||||
aircraft: The type of aircraft to remove.
|
||||
count: The number of aircraft to remove.
|
||||
|
||||
Raises:
|
||||
ValueError: The control point cannot fulfill the requested number of
|
||||
aircraft.
|
||||
"""
|
||||
available = self.inventory[aircraft]
|
||||
if available < count:
|
||||
raise ValueError(
|
||||
f"Cannot remove {count} {aircraft.id} from "
|
||||
f"{self.control_point.name}. Only have {available}."
|
||||
)
|
||||
self.inventory[aircraft] -= count
|
||||
|
||||
def available(self, aircraft: UnitType) -> int:
|
||||
"""Returns the number of available aircraft of the given type.
|
||||
|
||||
Args:
|
||||
aircraft: The type of aircraft to query.
|
||||
"""
|
||||
try:
|
||||
return self.inventory[aircraft]
|
||||
except KeyError:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def types_available(self) -> Iterator[UnitType]:
|
||||
"""Iterates over all available aircraft types."""
|
||||
for aircraft, count in self.inventory.items():
|
||||
if count > 0:
|
||||
yield aircraft
|
||||
|
||||
@property
|
||||
def all_aircraft(self) -> Iterator[Tuple[UnitType, int]]:
|
||||
"""Iterates over all available aircraft types, including amounts."""
|
||||
for aircraft, count in self.inventory.items():
|
||||
if count > 0:
|
||||
yield aircraft, count
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clears all aircraft from the inventory."""
|
||||
self.inventory.clear()
|
||||
|
||||
|
||||
class GlobalAircraftInventory:
|
||||
"""Game-wide aircraft inventory."""
|
||||
def __init__(self, control_points: Iterable[ControlPoint]) -> None:
|
||||
self.inventories: Dict[ControlPoint, ControlPointAircraftInventory] = {
|
||||
cp: ControlPointAircraftInventory(cp) for cp in control_points
|
||||
}
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Clears all control points and their inventories."""
|
||||
for inventory in self.inventories.values():
|
||||
inventory.clear()
|
||||
|
||||
def set_from_control_point(self, control_point: ControlPoint) -> None:
|
||||
"""Set the control point's aircraft inventory.
|
||||
|
||||
If the inventory for the given control point has already been set for
|
||||
the turn, it will be overwritten.
|
||||
"""
|
||||
inventory = self.inventories[control_point]
|
||||
for aircraft, count in control_point.base.aircraft.items():
|
||||
inventory.add_aircraft(aircraft, count)
|
||||
|
||||
def for_control_point(
|
||||
self,
|
||||
control_point: ControlPoint) -> ControlPointAircraftInventory:
|
||||
"""Returns the inventory specific to the given control point."""
|
||||
return self.inventories[control_point]
|
||||
|
||||
@property
|
||||
def available_types_for_player(self) -> Iterator[UnitType]:
|
||||
"""Iterates over all aircraft types available to the player."""
|
||||
seen: Set[UnitType] = set()
|
||||
for control_point, inventory in self.inventories.items():
|
||||
if control_point.captured:
|
||||
for aircraft in inventory.types_available:
|
||||
if aircraft not in seen:
|
||||
seen.add(aircraft)
|
||||
yield aircraft
|
||||
|
||||
def claim_for_flight(self, flight: Flight) -> None:
|
||||
"""Removes aircraft from the inventory for the given flight."""
|
||||
inventory = self.for_control_point(flight.from_cp)
|
||||
inventory.remove_aircraft(flight.unit_type, flight.count)
|
||||
|
||||
def return_from_flight(self, flight: Flight) -> None:
|
||||
"""Returns a flight's aircraft to the inventory."""
|
||||
inventory = self.for_control_point(flight.from_cp)
|
||||
inventory.add_aircraft(flight.unit_type, flight.count)
|
||||
14
game/models/destroyed_units.py
Normal file
14
game/models/destroyed_units.py
Normal file
@@ -0,0 +1,14 @@
|
||||
class DestroyedUnit:
|
||||
"""
|
||||
Store info about a destroyed unit
|
||||
"""
|
||||
|
||||
x: int
|
||||
y: int
|
||||
name: str
|
||||
|
||||
def __init__(self, x , y, name):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.name = name
|
||||
|
||||
13
game/models/frontline_data.py
Normal file
13
game/models/frontline_data.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from theater import ControlPoint
|
||||
|
||||
|
||||
class FrontlineData:
|
||||
"""
|
||||
This Data structure will store information about an existing frontline
|
||||
"""
|
||||
|
||||
def __init__(self, from_cp:ControlPoint, to_cp: ControlPoint):
|
||||
self.to_cp = to_cp
|
||||
self.from_cp = from_cp
|
||||
self.enemy_units_position = []
|
||||
self.blue_units_position = []
|
||||
56
game/models/game_stats.py
Normal file
56
game/models/game_stats.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from typing import List
|
||||
|
||||
class FactionTurnMetadata:
|
||||
"""
|
||||
Store metadata about a faction
|
||||
"""
|
||||
|
||||
aircraft_count: int = 0
|
||||
vehicles_count: int = 0
|
||||
sam_count: int = 0
|
||||
|
||||
def __init__(self):
|
||||
self.aircraft_count = 0
|
||||
self.vehicles_count = 0
|
||||
self.sam_count = 0
|
||||
|
||||
|
||||
class GameTurnMetadata:
|
||||
"""
|
||||
Store metadata about a game turn
|
||||
"""
|
||||
|
||||
allied_units:FactionTurnMetadata
|
||||
enemy_units:FactionTurnMetadata
|
||||
|
||||
def __init__(self):
|
||||
self.allied_units = FactionTurnMetadata()
|
||||
self.enemy_units = FactionTurnMetadata()
|
||||
|
||||
|
||||
class GameStats:
|
||||
"""
|
||||
Store statistics for the current game
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.data_per_turn: List[GameTurnMetadata] = []
|
||||
|
||||
def update(self, game):
|
||||
"""
|
||||
Save data for current turn
|
||||
:param game: Game we want to save the data about
|
||||
"""
|
||||
|
||||
turn_data = GameTurnMetadata()
|
||||
|
||||
for cp in game.theater.controlpoints:
|
||||
if cp.captured:
|
||||
turn_data.allied_units.aircraft_count += sum(cp.base.aircraft.values())
|
||||
turn_data.allied_units.vehicles_count += sum(cp.base.armor.values())
|
||||
else:
|
||||
turn_data.enemy_units.aircraft_count += sum(cp.base.aircraft.values())
|
||||
turn_data.enemy_units.vehicles_count += sum(cp.base.armor.values())
|
||||
|
||||
self.data_per_turn.append(turn_data)
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
from dcs.terrain import Terrain
|
||||
|
||||
from game import db
|
||||
from gen.armor import *
|
||||
from gen.aircraft import *
|
||||
from gen.aaa import *
|
||||
from gen.shipgen import *
|
||||
from gen.triggergen import *
|
||||
from gen.awacsgen import *
|
||||
from gen.visualgen import *
|
||||
from gen.conflictgen import Conflict
|
||||
|
||||
from .operation import Operation
|
||||
|
||||
|
||||
class AntiAAStrikeOperation(Operation):
|
||||
strikegroup = None # type: db.PlaneDict
|
||||
interceptors = None # type: db.PlaneDict
|
||||
target = None # type: db.ArmorDict
|
||||
|
||||
def setup(self,
|
||||
target: db.ArmorDict,
|
||||
strikegroup: db.PlaneDict,
|
||||
interceptors: db.PlaneDict):
|
||||
self.strikegroup = strikegroup
|
||||
self.interceptors = interceptors
|
||||
self.target = target
|
||||
|
||||
def prepare(self, terrain: Terrain, is_quick: bool):
|
||||
super(AntiAAStrikeOperation, self).prepare(terrain, is_quick)
|
||||
if self.defender_name == self.game.player:
|
||||
self.attackers_starting_position = None
|
||||
self.defenders_starting_position = None
|
||||
|
||||
conflict = Conflict.ground_base_attack(
|
||||
attacker=self.mission.country(self.attacker_name),
|
||||
defender=self.mission.country(self.defender_name),
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp,
|
||||
theater=self.game.theater
|
||||
)
|
||||
|
||||
self.initialize(mission=self.mission,
|
||||
conflict=conflict)
|
||||
|
||||
def generate(self):
|
||||
self.airgen.generate_cas_strikegroup(self.strikegroup, clients=self.attacker_clients, at=self.attackers_starting_position)
|
||||
|
||||
if self.interceptors:
|
||||
self.airgen.generate_defense(self.interceptors, clients=self.defender_clients, at=self.defenders_starting_position)
|
||||
|
||||
self.armorgen.generate({}, self.target)
|
||||
super(AntiAAStrikeOperation, self).generate()
|
||||
@@ -1,67 +0,0 @@
|
||||
from game import db
|
||||
|
||||
from gen.conflictgen import Conflict
|
||||
from gen.armor import *
|
||||
from gen.aircraft import *
|
||||
from gen.aaa import *
|
||||
from gen.shipgen import *
|
||||
from gen.triggergen import *
|
||||
from gen.awacsgen import *
|
||||
from gen.visualgen import *
|
||||
|
||||
from .operation import Operation
|
||||
|
||||
|
||||
class CaptureOperation(Operation):
|
||||
cas = None # type: db.PlaneDict
|
||||
escort = None # type: db.PlaneDict
|
||||
intercept = None # type: db.PlaneDict
|
||||
attack = None # type: db.ArmorDict
|
||||
defense = None # type: db.ArmorDict
|
||||
aa = None # type: db.AirDefenseDict
|
||||
|
||||
trigger_radius = TRIGGER_RADIUS_SMALL
|
||||
|
||||
def setup(self,
|
||||
cas: db.PlaneDict,
|
||||
escort: db.PlaneDict,
|
||||
attack: db.ArmorDict,
|
||||
intercept: db.PlaneDict,
|
||||
defense: db.ArmorDict,
|
||||
aa: db.AirDefenseDict):
|
||||
self.cas = cas
|
||||
self.escort = escort
|
||||
self.intercept = intercept
|
||||
self.attack = attack
|
||||
self.defense = defense
|
||||
self.aa = aa
|
||||
|
||||
def prepare(self, terrain: dcs.terrain.Terrain, is_quick: bool):
|
||||
super(CaptureOperation, self).prepare(terrain, is_quick)
|
||||
|
||||
self.defenders_starting_position = None
|
||||
if self.game.player == self.defender_name:
|
||||
self.attackers_starting_position = None
|
||||
|
||||
conflict = Conflict.capture_conflict(
|
||||
attacker=self.mission.country(self.attacker_name),
|
||||
defender=self.mission.country(self.defender_name),
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp,
|
||||
theater=self.game.theater
|
||||
)
|
||||
self.initialize(mission=self.mission,
|
||||
conflict=conflict)
|
||||
|
||||
def generate(self):
|
||||
self.armorgen.generate(self.attack, self.defense)
|
||||
self.aagen.generate(self.aa)
|
||||
|
||||
self.airgen.generate_defense(self.intercept, clients=self.defender_clients, at=self.defenders_starting_position)
|
||||
|
||||
self.airgen.generate_cas_strikegroup(self.cas, clients=self.attacker_clients, at=self.attackers_starting_position)
|
||||
self.airgen.generate_strikegroup_escort(self.escort, clients=self.attacker_clients, at=self.attackers_starting_position)
|
||||
|
||||
self.visualgen.generate_target_smokes(self.to_cp)
|
||||
super(CaptureOperation, self).generate()
|
||||
|
||||
38
game/operation/frontlineattack.py
Normal file
38
game/operation/frontlineattack.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from dcs.terrain.terrain import Terrain
|
||||
|
||||
from gen.conflictgen import Conflict
|
||||
from .operation import Operation
|
||||
from .. import db
|
||||
|
||||
MAX_DISTANCE_BETWEEN_GROUPS = 12000
|
||||
|
||||
|
||||
class FrontlineAttackOperation(Operation):
|
||||
interceptors = None # type: db.AssignedUnitsDict
|
||||
escort = None # type: db.AssignedUnitsDict
|
||||
strikegroup = None # type: db.AssignedUnitsDict
|
||||
|
||||
attackers = None # type: db.ArmorDict
|
||||
defenders = None # type: db.ArmorDict
|
||||
|
||||
def prepare(self, terrain: Terrain, is_quick: bool):
|
||||
super(FrontlineAttackOperation, self).prepare(terrain, is_quick)
|
||||
if self.defender_name == self.game.player_name:
|
||||
self.attackers_starting_position = None
|
||||
self.defenders_starting_position = None
|
||||
|
||||
conflict = Conflict.frontline_cas_conflict(
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker=self.current_mission.country(self.attacker_country),
|
||||
defender=self.current_mission.country(self.defender_country),
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp,
|
||||
theater=self.game.theater
|
||||
)
|
||||
|
||||
self.initialize(mission=self.current_mission,
|
||||
conflict=conflict)
|
||||
|
||||
def generate(self):
|
||||
super(FrontlineAttackOperation, self).generate()
|
||||
@@ -1,44 +0,0 @@
|
||||
from dcs.terrain import Terrain
|
||||
|
||||
from game import db
|
||||
from gen.armor import *
|
||||
from gen.aircraft import *
|
||||
from gen.aaa import *
|
||||
from gen.shipgen import *
|
||||
from gen.triggergen import *
|
||||
from gen.awacsgen import *
|
||||
from gen.visualgen import *
|
||||
from gen.conflictgen import Conflict
|
||||
|
||||
from .operation import Operation
|
||||
|
||||
|
||||
class GroundAttackOperation(Operation):
|
||||
strikegroup = None # type: db.PlaneDict
|
||||
target = None # type: db.ArmorDict
|
||||
|
||||
def setup(self,
|
||||
target: db.ArmorDict,
|
||||
strikegroup: db.PlaneDict):
|
||||
self.strikegroup = strikegroup
|
||||
self.target = target
|
||||
|
||||
def prepare(self, terrain: Terrain, is_quick: bool):
|
||||
super(GroundAttackOperation, self).prepare(terrain, is_quick)
|
||||
|
||||
conflict = Conflict.ground_attack_conflict(
|
||||
attacker=self.mission.country(self.attacker_name),
|
||||
defender=self.mission.country(self.defender_name),
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp,
|
||||
theater=self.game.theater
|
||||
)
|
||||
|
||||
self.initialize(mission=self.mission,
|
||||
conflict=conflict)
|
||||
|
||||
def generate(self):
|
||||
self.airgen.generate_defense(self.strikegroup, self.defender_clients, self.defenders_starting_position)
|
||||
self.armorgen.generate(self.target, {})
|
||||
|
||||
super(GroundAttackOperation, self).generate()
|
||||
@@ -1,53 +0,0 @@
|
||||
from dcs.terrain import Terrain
|
||||
|
||||
from game import db
|
||||
from gen.armor import *
|
||||
from gen.aircraft import *
|
||||
from gen.aaa import *
|
||||
from gen.shipgen import *
|
||||
from gen.triggergen import *
|
||||
from gen.awacsgen import *
|
||||
from gen.visualgen import *
|
||||
from gen.conflictgen import Conflict
|
||||
|
||||
from .operation import Operation
|
||||
|
||||
|
||||
class GroundInterceptOperation(Operation):
|
||||
strikegroup = None # type: db.PlaneDict
|
||||
interceptors = None # type: db.PlaneDict
|
||||
target = None # type: db.ArmorDict
|
||||
|
||||
def setup(self,
|
||||
target: db.ArmorDict,
|
||||
strikegroup: db.PlaneDict,
|
||||
interceptors: db.PlaneDict):
|
||||
self.strikegroup = strikegroup
|
||||
self.interceptors = interceptors
|
||||
self.target = target
|
||||
|
||||
def prepare(self, terrain: Terrain, is_quick: bool):
|
||||
super(GroundInterceptOperation, self).prepare(terrain, is_quick)
|
||||
if self.defender_name == self.game.player:
|
||||
self.attackers_starting_position = None
|
||||
self.defenders_starting_position = None
|
||||
|
||||
conflict = Conflict.ground_intercept_conflict(
|
||||
attacker=self.mission.country(self.attacker_name),
|
||||
defender=self.mission.country(self.defender_name),
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp,
|
||||
theater=self.game.theater
|
||||
)
|
||||
|
||||
self.initialize(mission=self.mission,
|
||||
conflict=conflict)
|
||||
|
||||
def generate(self):
|
||||
self.airgen.generate_cas_strikegroup(self.strikegroup, clients=self.attacker_clients, at=self.attackers_starting_position)
|
||||
|
||||
if self.interceptors:
|
||||
self.airgen.generate_defense(self.interceptors, clients=self.defender_clients, at=self.defenders_starting_position)
|
||||
|
||||
self.armorgen.generate({}, self.target)
|
||||
super(GroundInterceptOperation, self).generate()
|
||||
@@ -1,56 +0,0 @@
|
||||
from dcs.terrain import Terrain
|
||||
|
||||
from game import db
|
||||
from gen.armor import *
|
||||
from gen.aircraft import *
|
||||
from gen.aaa import *
|
||||
from gen.shipgen import *
|
||||
from gen.triggergen import *
|
||||
from gen.awacsgen import *
|
||||
from gen.visualgen import *
|
||||
from gen.conflictgen import Conflict
|
||||
|
||||
from .operation import Operation
|
||||
|
||||
|
||||
class InfantryTransportOperation(Operation):
|
||||
transport = None # type: db.HeliDict
|
||||
aa = None # type: db.AirDefenseDict
|
||||
|
||||
def setup(self, transport: db.HeliDict, aa: db.AirDefenseDict):
|
||||
self.transport = transport
|
||||
self.aa = aa
|
||||
|
||||
def prepare(self, terrain: Terrain, is_quick: bool):
|
||||
super(InfantryTransportOperation, self).prepare(terrain, is_quick)
|
||||
|
||||
conflict = Conflict.transport_conflict(
|
||||
attacker=self.mission.country(self.attacker_name),
|
||||
defender=self.mission.country(self.defender_name),
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp,
|
||||
theater=self.game.theater
|
||||
)
|
||||
|
||||
self.initialize(mission=self.mission,
|
||||
conflict=conflict)
|
||||
|
||||
def generate(self):
|
||||
self.airgen.generate_passenger_transport(
|
||||
helis=self.transport,
|
||||
clients=self.attacker_clients,
|
||||
at=self.attackers_starting_position
|
||||
)
|
||||
|
||||
self.armorgen.generate_passengers(count=6)
|
||||
self.aagen.generate_at_defenders_location(self.aa)
|
||||
|
||||
self.visualgen.generate_transportation_marker(self.conflict.ground_attackers_location)
|
||||
self.visualgen.generate_transportation_destination(self.conflict.position)
|
||||
|
||||
# TODO: horrible, horrible hack
|
||||
# this will disable vehicle activation triggers,
|
||||
# which aren't needed on this type of missions
|
||||
self.is_quick = True
|
||||
super(InfantryTransportOperation, self).generate()
|
||||
self.is_quick = False
|
||||
@@ -1,52 +0,0 @@
|
||||
from dcs.terrain import Terrain
|
||||
|
||||
from gen import *
|
||||
from .operation import Operation
|
||||
|
||||
|
||||
class InterceptOperation(Operation):
|
||||
escort = None # type: db.PlaneDict
|
||||
transport = None # type: db.PlaneDict
|
||||
interceptors = None # type: db.PlaneDict
|
||||
airdefense = None # type: db.AirDefenseDict
|
||||
|
||||
trigger_radius = TRIGGER_RADIUS_LARGE
|
||||
|
||||
def setup(self,
|
||||
escort: db.PlaneDict,
|
||||
transport: db.PlaneDict,
|
||||
airdefense: db.AirDefenseDict,
|
||||
interceptors: db.PlaneDict):
|
||||
self.escort = escort
|
||||
self.transport = transport
|
||||
self.airdefense = airdefense
|
||||
self.interceptors = interceptors
|
||||
|
||||
def prepare(self, terrain: Terrain, is_quick: bool):
|
||||
super(InterceptOperation, self).prepare(terrain, is_quick)
|
||||
self.defenders_starting_position = None
|
||||
if self.defender_name == self.game.player:
|
||||
self.attackers_starting_position = None
|
||||
|
||||
conflict = Conflict.intercept_conflict(
|
||||
attacker=self.mission.country(self.attacker_name),
|
||||
defender=self.mission.country(self.defender_name),
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp,
|
||||
theater=self.game.theater
|
||||
)
|
||||
|
||||
self.initialize(mission=self.mission,
|
||||
conflict=conflict)
|
||||
|
||||
def generate(self):
|
||||
self.airgen.generate_transport(self.transport, self.to_cp.at)
|
||||
self.airgen.generate_transport_escort(self.escort, clients=self.defender_clients)
|
||||
|
||||
if self.from_cp.is_global:
|
||||
super(InterceptOperation, self).generate()
|
||||
self.airgen.generate_interception(self.interceptors, clients=self.attacker_clients, at=self.attackers_starting_position)
|
||||
else:
|
||||
self.airgen.generate_interception(self.interceptors, clients=self.attacker_clients, at=self.attackers_starting_position)
|
||||
super(InterceptOperation, self).generate()
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
from dcs.terrain import Terrain
|
||||
|
||||
from gen import *
|
||||
from .operation import Operation
|
||||
|
||||
|
||||
class NavalInterceptionOperation(Operation):
|
||||
strikegroup = None # type: db.PlaneDict
|
||||
interceptors = None # type: db.PlaneDict
|
||||
targets = None # type: db.ShipDict
|
||||
trigger_radius = TRIGGER_RADIUS_LARGE
|
||||
|
||||
def setup(self,
|
||||
strikegroup: db.PlaneDict,
|
||||
interceptors: db.PlaneDict,
|
||||
targets: db.ShipDict):
|
||||
self.strikegroup = strikegroup
|
||||
self.interceptors = interceptors
|
||||
self.targets = targets
|
||||
|
||||
def prepare(self, terrain: Terrain, is_quick: bool):
|
||||
super(NavalInterceptionOperation, self).prepare(terrain, is_quick)
|
||||
if self.defender_name == self.game.player:
|
||||
self.attackers_starting_position = None
|
||||
|
||||
conflict = Conflict.naval_intercept_conflict(
|
||||
attacker=self.mission.country(self.attacker_name),
|
||||
defender=self.mission.country(self.defender_name),
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp,
|
||||
theater=self.game.theater
|
||||
)
|
||||
|
||||
self.initialize(self.mission, conflict)
|
||||
|
||||
def generate(self):
|
||||
super(NavalInterceptionOperation, self).generate()
|
||||
|
||||
target_groups = self.shipgen.generate_cargo(units=self.targets)
|
||||
|
||||
self.airgen.generate_ship_strikegroup(
|
||||
attackers=self.strikegroup,
|
||||
clients=self.attacker_clients,
|
||||
target_groups=target_groups,
|
||||
at=self.attackers_starting_position
|
||||
)
|
||||
|
||||
if self.interceptors:
|
||||
self.airgen.generate_defense(
|
||||
defenders=self.interceptors,
|
||||
clients=self.defender_clients,
|
||||
at=self.defenders_starting_position
|
||||
)
|
||||
@@ -1,103 +1,507 @@
|
||||
from dcs.terrain import Terrain
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Set
|
||||
|
||||
from userdata.debriefing import *
|
||||
from dcs import Mission
|
||||
from dcs.action import DoScript, DoScriptFile
|
||||
from dcs.coalition import Coalition
|
||||
from dcs.countries import country_dict
|
||||
from dcs.lua.parse import loads
|
||||
from dcs.mapping import Point
|
||||
from dcs.terrain.terrain import Terrain
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import TriggerStart
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from theater import *
|
||||
from gen import *
|
||||
from game.plugins import LuaPluginManager
|
||||
from gen import Conflict, FlightType, VisualGenerator
|
||||
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
|
||||
from gen.airfields import AIRFIELD_DATA
|
||||
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
|
||||
from gen.armor import GroundConflictGenerator, JtacInfo
|
||||
from gen.beacons import load_beacons_for_terrain
|
||||
from gen.briefinggen import BriefingGenerator, MissionInfoGenerator
|
||||
from gen.environmentgen import EnvironmentGenerator
|
||||
from gen.forcedoptionsgen import ForcedOptionsGenerator
|
||||
from gen.groundobjectsgen import GroundObjectsGenerator
|
||||
from gen.kneeboard import KneeboardGenerator
|
||||
from gen.radios import RadioFrequency, RadioRegistry
|
||||
from gen.tacan import TacanRegistry
|
||||
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
|
||||
from theater import ControlPoint
|
||||
from .. import db
|
||||
from ..debriefing import Debriefing
|
||||
|
||||
|
||||
class Operation:
|
||||
attackers_starting_position = None # type: db.StartingPosition
|
||||
defenders_starting_position = None # type: db.StartingPosition
|
||||
mission = None # type: dcs.Mission
|
||||
|
||||
current_mission = None # type: Mission
|
||||
regular_mission = None # type: Mission
|
||||
quick_mission = None # type: Mission
|
||||
conflict = None # type: Conflict
|
||||
armorgen = None # type: ArmorConflictGenerator
|
||||
airgen = None # type: AircraftConflictGenerator
|
||||
aagen = None # type: AAConflictGenerator
|
||||
extra_aagen = None # type: ExtraAAConflictGenerator
|
||||
shipgen = None # type: ShipGenerator
|
||||
triggersgen = None # type: TriggersGenerator
|
||||
awacsgen = None # type: AWACSConflictGenerator
|
||||
airsupportgen = None # type: AirSupportConflictGenerator
|
||||
visualgen = None # type: VisualGenerator
|
||||
envgen = None # type: EnvironmentGenerator
|
||||
groundobjectgen = None # type: GroundObjectsGenerator
|
||||
briefinggen = None # type: BriefingGenerator
|
||||
forcedoptionsgen = None # type: ForcedOptionsGenerator
|
||||
radio_registry: Optional[RadioRegistry] = None
|
||||
tacan_registry: Optional[TacanRegistry] = None
|
||||
|
||||
environment_settings = None
|
||||
trigger_radius = TRIGGER_RADIUS_MEDIUM
|
||||
is_quick = None
|
||||
is_awacs_enabled = False
|
||||
ca_slots = 0
|
||||
|
||||
def __init__(self,
|
||||
game,
|
||||
attacker_name: str,
|
||||
defender_name: str,
|
||||
attacker_clients: db.PlaneDict,
|
||||
defender_clients: db.PlaneDict,
|
||||
from_cp: ControlPoint,
|
||||
to_cp: ControlPoint = None):
|
||||
departure_cp: ControlPoint,
|
||||
to_cp: ControlPoint):
|
||||
self.game = game
|
||||
self.attacker_name = attacker_name
|
||||
self.attacker_country = db.FACTIONS[attacker_name].country
|
||||
self.defender_name = defender_name
|
||||
self.attacker_clients = attacker_clients
|
||||
self.defender_clients = defender_clients
|
||||
self.defender_country = db.FACTIONS[defender_name].country
|
||||
print(self.defender_country, self.attacker_country)
|
||||
self.from_cp = from_cp
|
||||
self.departure_cp = departure_cp
|
||||
self.to_cp = to_cp
|
||||
self.is_quick = False
|
||||
self.plugin_scripts: List[str] = []
|
||||
|
||||
def units_of(self, country_name: str) -> List[UnitType]:
|
||||
return []
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_player_attack(self) -> bool:
|
||||
return self.from_cp.captured
|
||||
|
||||
def initialize(self, mission: Mission, conflict: Conflict):
|
||||
self.mission = mission
|
||||
self.current_mission = mission
|
||||
self.conflict = conflict
|
||||
|
||||
self.armorgen = ArmorConflictGenerator(mission, conflict)
|
||||
self.airgen = AircraftConflictGenerator(mission, conflict, self.game.settings)
|
||||
self.aagen = AAConflictGenerator(mission, conflict)
|
||||
self.shipgen = ShipGenerator(mission, conflict)
|
||||
self.awacsgen = AWACSConflictGenerator(mission, conflict, self.game)
|
||||
self.triggersgen = TriggersGenerator(mission, conflict, self.game)
|
||||
self.visualgen = VisualGenerator(mission, conflict, self.game)
|
||||
self.envgen = EnviromentGenerator(mission, conflict, self.game)
|
||||
|
||||
player_name = self.from_cp.captured and self.attacker_name or self.defender_name
|
||||
enemy_name = self.from_cp.captured and self.defender_name or self.attacker_name
|
||||
self.extra_aagen = ExtraAAConflictGenerator(mission, conflict, self.game, player_name, enemy_name)
|
||||
# self.briefinggen = BriefingGenerator(self.current_mission, self.game) Is it safe to remove this, or does it also break save compat?
|
||||
|
||||
def prepare(self, terrain: Terrain, is_quick: bool):
|
||||
self.mission = dcs.Mission(terrain)
|
||||
with open("resources/default_options.lua", "r") as f:
|
||||
options_dict = loads(f.read())["options"]
|
||||
|
||||
self.current_mission = Mission(terrain)
|
||||
|
||||
print(self.game.player_country)
|
||||
print(country_dict[db.country_id_from_name(self.game.player_country)])
|
||||
print(country_dict[db.country_id_from_name(self.game.player_country)]())
|
||||
|
||||
# Setup coalition :
|
||||
self.current_mission.coalition["blue"] = Coalition("blue")
|
||||
self.current_mission.coalition["red"] = Coalition("red")
|
||||
|
||||
p_country = self.game.player_country
|
||||
e_country = self.game.enemy_country
|
||||
self.current_mission.coalition["blue"].add_country(country_dict[db.country_id_from_name(p_country)]())
|
||||
self.current_mission.coalition["red"].add_country(country_dict[db.country_id_from_name(e_country)]())
|
||||
|
||||
print([c for c in self.current_mission.coalition["blue"].countries.keys()])
|
||||
print([c for c in self.current_mission.coalition["red"].countries.keys()])
|
||||
|
||||
if is_quick:
|
||||
self.quick_mission = self.current_mission
|
||||
else:
|
||||
self.regular_mission = self.current_mission
|
||||
|
||||
self.current_mission.options.load_from_dict(options_dict)
|
||||
self.is_quick = is_quick
|
||||
|
||||
if is_quick:
|
||||
self.attackers_starting_position = None
|
||||
self.defenders_starting_position = None
|
||||
else:
|
||||
self.attackers_starting_position = self.from_cp.at
|
||||
self.defenders_starting_position = self.to_cp.at
|
||||
self.attackers_starting_position = self.departure_cp.at
|
||||
# TODO: Is this possible?
|
||||
if self.to_cp is not None:
|
||||
self.defenders_starting_position = self.to_cp.at
|
||||
else:
|
||||
self.defenders_starting_position = None
|
||||
|
||||
def inject_lua_trigger(self, contents: str, comment: str) -> None:
|
||||
trigger = TriggerStart(comment=comment)
|
||||
trigger.add_action(DoScript(String(contents)))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
def bypass_plugin_script(self, mnemonic: str) -> None:
|
||||
self.plugin_scripts.append(mnemonic)
|
||||
|
||||
def inject_plugin_script(self, plugin_mnemonic: str, script: str,
|
||||
script_mnemonic: str) -> None:
|
||||
if script_mnemonic in self.plugin_scripts:
|
||||
logging.debug(
|
||||
f"Skipping already loaded {script} for {plugin_mnemonic}"
|
||||
)
|
||||
else:
|
||||
self.plugin_scripts.append(script_mnemonic)
|
||||
|
||||
plugin_path = Path("./resources/plugins", plugin_mnemonic)
|
||||
|
||||
script_path = Path(plugin_path, script)
|
||||
if not script_path.exists():
|
||||
logging.error(
|
||||
f"Cannot find {script_path} for plugin {plugin_mnemonic}"
|
||||
)
|
||||
return
|
||||
|
||||
trigger = TriggerStart(comment=f"Load {script_mnemonic}")
|
||||
filename = script_path.resolve()
|
||||
fileref = self.current_mission.map_resource.add_resource_file(filename)
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
def notify_info_generators(
|
||||
self,
|
||||
groundobjectgen: GroundObjectsGenerator,
|
||||
airsupportgen: AirSupportConflictGenerator,
|
||||
jtacs: List[JtacInfo],
|
||||
airgen: AircraftConflictGenerator,
|
||||
):
|
||||
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)
|
||||
"""
|
||||
gens: List[MissionInfoGenerator] = [
|
||||
KneeboardGenerator(self.current_mission, self.game),
|
||||
BriefingGenerator(self.current_mission, self.game)
|
||||
]
|
||||
for gen in gens:
|
||||
for dynamic_runway in groundobjectgen.runways.values():
|
||||
gen.add_dynamic_runway(dynamic_runway)
|
||||
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
gen.add_tanker(tanker)
|
||||
|
||||
if self.is_awacs_enabled:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
gen.add_awacs(awacs)
|
||||
|
||||
for jtac in jtacs:
|
||||
gen.add_jtac(jtac)
|
||||
|
||||
for flight in airgen.flights:
|
||||
gen.add_flight(flight)
|
||||
gen.generate()
|
||||
|
||||
def generate(self):
|
||||
self.visualgen.generate()
|
||||
radio_registry = RadioRegistry()
|
||||
tacan_registry = TacanRegistry()
|
||||
|
||||
if self.is_awacs_enabled:
|
||||
self.awacsgen.generate()
|
||||
# Dedup beacon/radio frequencies, since some maps have some frequencies
|
||||
# used multiple times.
|
||||
beacons = load_beacons_for_terrain(self.game.theater.terrain.name)
|
||||
unique_map_frequencies: Set[RadioFrequency] = set()
|
||||
for beacon in beacons:
|
||||
unique_map_frequencies.add(beacon.frequency)
|
||||
if beacon.is_tacan:
|
||||
if beacon.channel is None:
|
||||
logging.error(
|
||||
f"TACAN beacon has no channel: {beacon.callsign}")
|
||||
else:
|
||||
tacan_registry.reserve(beacon.tacan_channel)
|
||||
|
||||
self.extra_aagen.generate()
|
||||
self.triggersgen.generate(self.is_quick, self.trigger_radius)
|
||||
for airfield, data in AIRFIELD_DATA.items():
|
||||
if data.theater == self.game.theater.terrain.name:
|
||||
unique_map_frequencies.add(data.atc.hf)
|
||||
unique_map_frequencies.add(data.atc.vhf_fm)
|
||||
unique_map_frequencies.add(data.atc.vhf_am)
|
||||
unique_map_frequencies.add(data.atc.uhf)
|
||||
# No need to reserve ILS or TACAN because those are in the
|
||||
# beacon list.
|
||||
|
||||
if self.environment_settings is None:
|
||||
self.environment_settings = self.envgen.generate()
|
||||
else:
|
||||
self.envgen.load(self.environment_settings)
|
||||
for frequency in unique_map_frequencies:
|
||||
radio_registry.reserve(frequency)
|
||||
|
||||
for global_cp in self.game.theater.controlpoints:
|
||||
if not global_cp.is_global:
|
||||
# Set mission time and weather conditions.
|
||||
EnvironmentGenerator(self.current_mission,
|
||||
self.game.conditions).generate()
|
||||
|
||||
# Generate ground object first
|
||||
|
||||
groundobjectgen = GroundObjectsGenerator(
|
||||
self.current_mission,
|
||||
self.conflict,
|
||||
self.game,
|
||||
radio_registry,
|
||||
tacan_registry
|
||||
)
|
||||
groundobjectgen.generate()
|
||||
|
||||
# Generate destroyed units
|
||||
for d in self.game.get_destroyed_units():
|
||||
try:
|
||||
utype = db.unit_type_from_name(d["type"])
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
ship = self.shipgen.generate_carrier(type=db.find_unittype(Carriage, self.game.player)[0],
|
||||
country=self.game.player,
|
||||
at=global_cp.at)
|
||||
pos = Point(d["x"], d["z"])
|
||||
if utype is not None and not self.game.position_culled(pos) and self.game.settings.perf_destroyed_units:
|
||||
self.current_mission.static_group(
|
||||
country=self.current_mission.country(self.game.player_country),
|
||||
name="",
|
||||
_type=utype,
|
||||
hidden=True,
|
||||
position=pos,
|
||||
heading=d["orientation"],
|
||||
dead=True,
|
||||
)
|
||||
|
||||
if global_cp == self.from_cp and not self.is_quick:
|
||||
self.attackers_starting_position = ship
|
||||
# Air Support (Tanker & Awacs)
|
||||
airsupportgen = AirSupportConflictGenerator(
|
||||
self.current_mission, self.conflict, self.game, radio_registry,
|
||||
tacan_registry)
|
||||
airsupportgen.generate(self.is_awacs_enabled)
|
||||
|
||||
def units_of(self, country_name: str) -> typing.Collection[UnitType]:
|
||||
return []
|
||||
# Generate Activity on the map
|
||||
airgen = AircraftConflictGenerator(
|
||||
self.current_mission, self.conflict, self.game.settings, self.game,
|
||||
radio_registry)
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing) -> bool:
|
||||
return True
|
||||
airgen.generate_flights(
|
||||
self.current_mission.country(self.game.player_country),
|
||||
self.game.blue_ato,
|
||||
groundobjectgen.runways
|
||||
)
|
||||
airgen.generate_flights(
|
||||
self.current_mission.country(self.game.enemy_country),
|
||||
self.game.red_ato,
|
||||
groundobjectgen.runways
|
||||
)
|
||||
|
||||
# Generate ground units on frontline everywhere
|
||||
jtacs: List[JtacInfo] = []
|
||||
for front_line in self.game.theater.conflicts(True):
|
||||
player_cp = front_line.control_point_a
|
||||
enemy_cp = front_line.control_point_b
|
||||
conflict = Conflict.frontline_cas_conflict(self.attacker_name, self.defender_name,
|
||||
self.current_mission.country(self.attacker_country),
|
||||
self.current_mission.country(self.defender_country),
|
||||
player_cp, enemy_cp, self.game.theater)
|
||||
# Generate frontline ops
|
||||
player_gp = self.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id]
|
||||
enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
|
||||
groundConflictGen = GroundConflictGenerator(self.current_mission, conflict, self.game, player_gp, enemy_gp, player_cp.stances[enemy_cp.id])
|
||||
groundConflictGen.generate()
|
||||
jtacs.extend(groundConflictGen.jtacs)
|
||||
|
||||
# Setup combined arms parameters
|
||||
self.current_mission.groundControl.pilot_can_control_vehicles = self.ca_slots > 0
|
||||
if self.game.player_country in [country.name for country in self.current_mission.coalition["blue"].countries.values()]:
|
||||
self.current_mission.groundControl.blue_tactical_commander = self.ca_slots
|
||||
else:
|
||||
self.current_mission.groundControl.red_tactical_commander = self.ca_slots
|
||||
|
||||
# Triggers
|
||||
triggersgen = TriggersGenerator(self.current_mission, self.conflict,
|
||||
self.game)
|
||||
triggersgen.generate()
|
||||
|
||||
# Options
|
||||
forcedoptionsgen = ForcedOptionsGenerator(self.current_mission,
|
||||
self.conflict, self.game)
|
||||
forcedoptionsgen.generate()
|
||||
|
||||
# Generate Visuals Smoke Effects
|
||||
visualgen = VisualGenerator(self.current_mission, self.conflict,
|
||||
self.game)
|
||||
if self.game.settings.perf_smoke_gen:
|
||||
visualgen.generate()
|
||||
|
||||
luaData = {}
|
||||
luaData["AircraftCarriers"] = {}
|
||||
luaData["Tankers"] = {}
|
||||
luaData["AWACs"] = {}
|
||||
luaData["JTACs"] = {}
|
||||
luaData["TargetPoints"] = {}
|
||||
|
||||
self.assign_channels_to_flights(airgen.flights,
|
||||
airsupportgen.air_support)
|
||||
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
luaData["Tankers"][tanker.callsign] = {
|
||||
"dcsGroupName": tanker.dcsGroupName,
|
||||
"callsign": tanker.callsign,
|
||||
"variant": tanker.variant,
|
||||
"radio": tanker.freq.mhz,
|
||||
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name
|
||||
}
|
||||
|
||||
if self.is_awacs_enabled:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
luaData["AWACs"][awacs.callsign] = {
|
||||
"dcsGroupName": awacs.dcsGroupName,
|
||||
"callsign": awacs.callsign,
|
||||
"radio": awacs.freq.mhz
|
||||
}
|
||||
|
||||
for jtac in jtacs:
|
||||
luaData["JTACs"][jtac.callsign] = {
|
||||
"dcsGroupName": jtac.dcsGroupName,
|
||||
"callsign": jtac.callsign,
|
||||
"zone": jtac.region,
|
||||
"dcsUnit": jtac.unit_name,
|
||||
"laserCode": jtac.code
|
||||
}
|
||||
|
||||
for flight in airgen.flights:
|
||||
if flight.friendly and flight.flight_type in [FlightType.ANTISHIP, FlightType.DEAD, FlightType.SEAD, FlightType.STRIKE]:
|
||||
flightType = flight.flight_type.name
|
||||
flightTarget = flight.package.target
|
||||
if flightTarget:
|
||||
flightTargetName = None
|
||||
flightTargetType = None
|
||||
if hasattr(flightTarget, 'obj_name'):
|
||||
flightTargetName = flightTarget.obj_name
|
||||
flightTargetType = flightType + f" TGT ({flightTarget.category})"
|
||||
elif hasattr(flightTarget, 'name'):
|
||||
flightTargetName = flightTarget.name
|
||||
flightTargetType = flightType + " TGT (Airbase)"
|
||||
luaData["TargetPoints"][flightTargetName] = {
|
||||
"name": flightTargetName,
|
||||
"type": flightTargetType,
|
||||
"position": { "x": flightTarget.position.x, "y": flightTarget.position.y}
|
||||
}
|
||||
|
||||
# set a LUA table with data from Liberation that we want to set
|
||||
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
|
||||
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
|
||||
state_location = "[[" + os.path.abspath(".") + "]]"
|
||||
lua = """
|
||||
-- setting configuration table
|
||||
env.info("DCSLiberation|: setting configuration table")
|
||||
|
||||
-- all data in this table is overridable.
|
||||
dcsLiberation = {}
|
||||
|
||||
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
|
||||
dcsLiberation.installPath=""" + state_location + """
|
||||
|
||||
"""
|
||||
# Process the tankers
|
||||
lua += """
|
||||
|
||||
-- list the tankers generated by Liberation
|
||||
dcsLiberation.Tankers = {
|
||||
"""
|
||||
for key in luaData["Tankers"]:
|
||||
data = luaData["Tankers"][key]
|
||||
dcsGroupName= data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
variant = data["variant"]
|
||||
tacan = data["tacan"]
|
||||
radio = data["radio"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', variant='{variant}', tacan='{tacan}', radio='{radio}' }}, \n"
|
||||
#lua += f" {{name='{dcsGroupName}', description='{callsign} ({variant})', information='Tacan:{tacan} Radio:{radio}' }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the AWACSes
|
||||
lua += """
|
||||
|
||||
-- list the AWACs generated by Liberation
|
||||
dcsLiberation.AWACs = {
|
||||
"""
|
||||
for key in luaData["AWACs"]:
|
||||
data = luaData["AWACs"][key]
|
||||
dcsGroupName= data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
radio = data["radio"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', radio='{radio}' }}, \n"
|
||||
#lua += f" {{name='{dcsGroupName}', description='{callsign} (AWACS)', information='Radio:{radio}' }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the JTACs
|
||||
lua += """
|
||||
|
||||
-- list the JTACs generated by Liberation
|
||||
dcsLiberation.JTACs = {
|
||||
"""
|
||||
for key in luaData["JTACs"]:
|
||||
data = luaData["JTACs"][key]
|
||||
dcsGroupName= data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
zone = data["zone"]
|
||||
laserCode = data["laserCode"]
|
||||
dcsUnit = data["dcsUnit"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone='{zone}', laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
|
||||
#lua += f" {{name='{dcsGroupName}', description='JTAC {callsign} ', information='Laser:{laserCode}', jtac={laserCode} }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the Target Points
|
||||
lua += """
|
||||
|
||||
-- list the target points generated by Liberation
|
||||
dcsLiberation.TargetPoints = {
|
||||
"""
|
||||
for key in luaData["TargetPoints"]:
|
||||
data = luaData["TargetPoints"][key]
|
||||
name = data["name"]
|
||||
pointType = data["type"]
|
||||
positionX = data["position"]["x"]
|
||||
positionY = data["position"]["y"]
|
||||
lua += f" {{name='{name}', pointType='{pointType}', positionX='{positionX}', positionY='{positionY}' }}, \n"
|
||||
#lua += f" {{name='{pointType} {name}', point{{x={positionX}, z={positionY} }} }}, \n"
|
||||
lua += "}"
|
||||
|
||||
lua += """
|
||||
|
||||
-- list the airbases generated by Liberation
|
||||
-- dcsLiberation.Airbases = {}
|
||||
|
||||
-- list the aircraft carriers generated by Liberation
|
||||
-- dcsLiberation.Carriers = {}
|
||||
|
||||
-- later, we'll add more data to the table
|
||||
|
||||
"""
|
||||
|
||||
|
||||
trigger = TriggerStart(comment="Set DCS Liberation data")
|
||||
trigger.add_action(DoScript(String(lua)))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
# Inject Plugins Lua Scripts and data
|
||||
for plugin in LuaPluginManager.plugins():
|
||||
if plugin.enabled:
|
||||
plugin.inject_scripts(self)
|
||||
plugin.inject_configuration(self)
|
||||
|
||||
self.assign_channels_to_flights(airgen.flights,
|
||||
airsupportgen.air_support)
|
||||
self.notify_info_generators(groundobjectgen, airsupportgen, jtacs, airgen)
|
||||
|
||||
def assign_channels_to_flights(self, flights: List[FlightData],
|
||||
air_support: AirSupport) -> None:
|
||||
"""Assigns preset radio channels for client flights."""
|
||||
for flight in flights:
|
||||
if not flight.client_units:
|
||||
continue
|
||||
self.assign_channels_to_flight(flight, air_support)
|
||||
|
||||
def assign_channels_to_flight(self, flight: FlightData,
|
||||
air_support: AirSupport) -> None:
|
||||
"""Assigns preset radio channels for a client flight."""
|
||||
airframe = flight.aircraft_type
|
||||
|
||||
try:
|
||||
aircraft_data = AIRCRAFT_DATA[airframe.id]
|
||||
except KeyError:
|
||||
logging.warning(f"No aircraft data for {airframe.id}")
|
||||
return
|
||||
|
||||
if aircraft_data.channel_allocator is not None:
|
||||
aircraft_data.channel_allocator.assign_channels_for_flight(
|
||||
flight, air_support
|
||||
)
|
||||
|
||||
84
game/persistency.py
Normal file
84
game/persistency.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import shutil
|
||||
from typing import Optional
|
||||
|
||||
_dcs_saved_game_folder: Optional[str] = None
|
||||
_file_abs_path = None
|
||||
|
||||
def setup(user_folder: str):
|
||||
global _dcs_saved_game_folder
|
||||
_dcs_saved_game_folder = user_folder
|
||||
_file_abs_path = os.path.join(base_path(), "default.liberation")
|
||||
|
||||
def base_path() -> str:
|
||||
global _dcs_saved_game_folder
|
||||
assert _dcs_saved_game_folder
|
||||
return _dcs_saved_game_folder
|
||||
|
||||
def _save_file() -> str:
|
||||
return os.path.join(base_path(), "default.liberation")
|
||||
|
||||
def _temporary_save_file() -> str:
|
||||
return os.path.join(base_path(), "tmpsave.liberation")
|
||||
|
||||
def _autosave_path() -> str:
|
||||
return os.path.join(base_path(), "autosave.liberation")
|
||||
|
||||
def _save_file_exists() -> bool:
|
||||
return os.path.exists(_save_file())
|
||||
|
||||
def mission_path_for(name: str) -> str:
|
||||
return os.path.join(base_path(), "Missions", "{}".format(name))
|
||||
|
||||
|
||||
def restore_game():
|
||||
if not _save_file_exists():
|
||||
return None
|
||||
|
||||
with open(_save_file(), "rb") as f:
|
||||
try:
|
||||
save = pickle.load(f)
|
||||
return save
|
||||
except Exception:
|
||||
logging.exception("Invalid Save game")
|
||||
return None
|
||||
|
||||
|
||||
def load_game(path):
|
||||
with open(path, "rb") as f:
|
||||
try:
|
||||
save = pickle.load(f)
|
||||
save.savepath = path
|
||||
return save
|
||||
except Exception:
|
||||
logging.exception("Invalid Save game")
|
||||
return None
|
||||
|
||||
|
||||
def save_game(game) -> bool:
|
||||
try:
|
||||
with open(_temporary_save_file(), "wb") as f:
|
||||
pickle.dump(game, f)
|
||||
shutil.copy(_temporary_save_file(), game.savepath)
|
||||
return True
|
||||
except Exception:
|
||||
logging.exception("Could not save game")
|
||||
return False
|
||||
|
||||
|
||||
def autosave(game) -> bool:
|
||||
"""
|
||||
Autosave to the autosave location
|
||||
:param game: Game to save
|
||||
:return: True if saved succesfully
|
||||
"""
|
||||
try:
|
||||
with open(_autosave_path(), "wb") as f:
|
||||
pickle.dump(game, f)
|
||||
return True
|
||||
except Exception:
|
||||
logging.exception("Could not save game")
|
||||
return False
|
||||
|
||||
2
game/plugins/__init__.py
Normal file
2
game/plugins/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .luaplugin import LuaPlugin
|
||||
from .manager import LuaPluginManager
|
||||
180
game/plugins/luaplugin.py
Normal file
180
game/plugins/luaplugin.py
Normal file
@@ -0,0 +1,180 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import textwrap
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
|
||||
from game.settings import Settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.operation.operation import Operation
|
||||
|
||||
|
||||
class LuaPluginWorkOrder:
|
||||
|
||||
def __init__(self, parent_mnemonic: str, filename: str, mnemonic: str,
|
||||
disable: bool) -> None:
|
||||
self.parent_mnemonic = parent_mnemonic
|
||||
self.filename = filename
|
||||
self.mnemonic = mnemonic
|
||||
self.disable = disable
|
||||
|
||||
def work(self, operation: Operation) -> None:
|
||||
if self.disable:
|
||||
operation.bypass_plugin_script(self.mnemonic)
|
||||
else:
|
||||
operation.inject_plugin_script(self.parent_mnemonic, self.filename,
|
||||
self.mnemonic)
|
||||
|
||||
|
||||
class PluginSettings:
|
||||
def __init__(self, identifier: str, enabled_by_default: bool) -> None:
|
||||
self.identifier = identifier
|
||||
self.enabled_by_default = enabled_by_default
|
||||
self.settings = Settings()
|
||||
self.initialize_settings()
|
||||
|
||||
def set_settings(self, settings: Settings):
|
||||
self.settings = settings
|
||||
self.initialize_settings()
|
||||
|
||||
def initialize_settings(self) -> None:
|
||||
# Plugin options are saved in the game's Settings, but it's possible for
|
||||
# plugins to change across loads. If new plugins are added or new
|
||||
# options added to those plugins, initialize the new settings.
|
||||
self.settings.initialize_plugin_option(self.identifier,
|
||||
self.enabled_by_default)
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
return self.settings.plugin_option(self.identifier)
|
||||
|
||||
def set_enabled(self, enabled: bool) -> None:
|
||||
self.settings.set_plugin_option(self.identifier, enabled)
|
||||
|
||||
|
||||
class LuaPluginOption(PluginSettings):
|
||||
def __init__(self, identifier: str, name: str,
|
||||
enabled_by_default: bool) -> None:
|
||||
super().__init__(identifier, enabled_by_default)
|
||||
self.name = name
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LuaPluginDefinition:
|
||||
identifier: str
|
||||
name: str
|
||||
present_in_ui: bool
|
||||
enabled_by_default: bool
|
||||
options: List[LuaPluginOption]
|
||||
work_orders: List[LuaPluginWorkOrder]
|
||||
config_work_orders: List[LuaPluginWorkOrder]
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, name: str, path: Path) -> LuaPluginDefinition:
|
||||
data = json.loads(path.read_text())
|
||||
|
||||
options = []
|
||||
for option in data.get("specificOptions"):
|
||||
option_id = option["mnemonic"]
|
||||
options.append(LuaPluginOption(
|
||||
identifier=f"{name}.{option_id}",
|
||||
name=option.get("nameInUI", name),
|
||||
enabled_by_default=option.get("defaultValue")
|
||||
))
|
||||
|
||||
work_orders = []
|
||||
for work_order in data.get("scriptsWorkOrders"):
|
||||
work_orders.append(LuaPluginWorkOrder(
|
||||
name, work_order.get("file"), work_order["mnemonic"],
|
||||
work_order.get("disable", False)
|
||||
))
|
||||
config_work_orders = []
|
||||
for work_order in data.get("configurationWorkOrders"):
|
||||
config_work_orders.append(LuaPluginWorkOrder(
|
||||
name, work_order.get("file"), work_order["mnemonic"],
|
||||
work_order.get("disable", False)
|
||||
))
|
||||
|
||||
return cls(
|
||||
identifier=name,
|
||||
name=data["nameInUI"],
|
||||
present_in_ui=not data.get("skipUI", False),
|
||||
enabled_by_default=data.get("defaultValue", False),
|
||||
options=options,
|
||||
work_orders=work_orders,
|
||||
config_work_orders=config_work_orders
|
||||
)
|
||||
|
||||
|
||||
class LuaPlugin(PluginSettings):
|
||||
|
||||
def __init__(self, definition: LuaPluginDefinition) -> None:
|
||||
self.definition = definition
|
||||
super().__init__(self.definition.identifier,
|
||||
self.definition.enabled_by_default)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.definition.name
|
||||
|
||||
@property
|
||||
def show_in_ui(self) -> bool:
|
||||
return self.definition.present_in_ui
|
||||
|
||||
@property
|
||||
def options(self) -> List[LuaPluginOption]:
|
||||
return self.definition.options
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, name: str, path: Path) -> Optional[LuaPlugin]:
|
||||
try:
|
||||
definition = LuaPluginDefinition.from_json(name, path)
|
||||
except KeyError:
|
||||
logging.exception("Required plugin configuration value missing")
|
||||
return None
|
||||
|
||||
return cls(definition)
|
||||
|
||||
def set_settings(self, settings: Settings):
|
||||
super().set_settings(settings)
|
||||
for option in self.definition.options:
|
||||
option.set_settings(self.settings)
|
||||
|
||||
def inject_scripts(self, operation: Operation) -> None:
|
||||
for work_order in self.definition.work_orders:
|
||||
work_order.work(operation)
|
||||
|
||||
def inject_configuration(self, operation: Operation) -> None:
|
||||
# inject the plugin options
|
||||
if self.options:
|
||||
option_decls = []
|
||||
for option in self.options:
|
||||
enabled = str(option.enabled).lower()
|
||||
name = option.identifier
|
||||
option_decls.append(
|
||||
f" dcsLiberation.plugins.{name} = {enabled}")
|
||||
|
||||
joined_options = "\n".join(option_decls)
|
||||
|
||||
lua = textwrap.dedent(f"""\
|
||||
-- {self.identifier} plugin configuration.
|
||||
|
||||
if dcsLiberation then
|
||||
if not dcsLiberation.plugins then
|
||||
dcsLiberation.plugins = {{}}
|
||||
end
|
||||
dcsLiberation.plugins.{self.identifier} = {{}}
|
||||
{joined_options}
|
||||
end
|
||||
|
||||
""")
|
||||
|
||||
operation.inject_lua_trigger(
|
||||
lua, f"{self.identifier} plugin configuration")
|
||||
|
||||
for work_order in self.definition.config_work_orders:
|
||||
work_order.work(operation)
|
||||
50
game/plugins/manager.py
Normal file
50
game/plugins/manager.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from game.settings import Settings
|
||||
from game.plugins.luaplugin import LuaPlugin
|
||||
|
||||
|
||||
class LuaPluginManager:
|
||||
_plugins_loaded = False
|
||||
_plugins: Dict[str, LuaPlugin] = {}
|
||||
|
||||
@classmethod
|
||||
def _load_plugins(cls) -> None:
|
||||
plugins_path = Path("resources/plugins")
|
||||
|
||||
path = plugins_path / "plugins.json"
|
||||
if not path.exists():
|
||||
raise RuntimeError(f"{path} does not exist. Cannot continue.")
|
||||
|
||||
logging.info(f"Reading plugins list from {path}")
|
||||
|
||||
data = json.loads(path.read_text())
|
||||
for name in data:
|
||||
plugin_path = plugins_path / name / "plugin.json"
|
||||
if not plugin_path.exists():
|
||||
raise RuntimeError(
|
||||
f"Invalid plugin configuration: required plugin {name} "
|
||||
f"does not exist at {plugin_path}")
|
||||
logging.info(f"Loading plugin {name} from {plugin_path}")
|
||||
plugin = LuaPlugin.from_json(name, plugin_path)
|
||||
if plugin is not None:
|
||||
cls._plugins[name] = plugin
|
||||
cls._plugins_loaded = True
|
||||
|
||||
@classmethod
|
||||
def _get_plugins(cls) -> Dict[str, LuaPlugin]:
|
||||
if not cls._plugins_loaded:
|
||||
cls._load_plugins()
|
||||
return cls._plugins
|
||||
|
||||
@classmethod
|
||||
def plugins(cls) -> List[LuaPlugin]:
|
||||
return list(cls._get_plugins().values())
|
||||
|
||||
@classmethod
|
||||
def load_settings(cls, settings: Settings) -> None:
|
||||
for plugin in cls.plugins():
|
||||
plugin.set_settings(settings)
|
||||
@@ -1,8 +1,76 @@
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class Settings:
|
||||
player_skill = "Good"
|
||||
enemy_skill = "Average"
|
||||
only_player_takeoff = False
|
||||
night_disabled = False
|
||||
multiplier = 1
|
||||
sams = True
|
||||
|
||||
def __init__(self):
|
||||
# Generator settings
|
||||
self.inverted = False
|
||||
self.do_not_generate_carrier = False # TODO : implement
|
||||
self.do_not_generate_lha = False # TODO : implement
|
||||
self.do_not_generate_player_navy = True # TODO : implement
|
||||
self.do_not_generate_enemy_navy = True # TODO : implement
|
||||
|
||||
# Difficulty settings
|
||||
self.player_skill = "Good"
|
||||
self.enemy_skill = "Average"
|
||||
self.enemy_vehicle_skill = "Average"
|
||||
self.map_coalition_visibility = "All Units"
|
||||
self.labels = "Full"
|
||||
self.only_player_takeoff = True # Legacy parameter do not use
|
||||
self.night_disabled = False
|
||||
self.external_views_allowed = True
|
||||
self.supercarrier = False
|
||||
self.multiplier = 1.0
|
||||
self.generate_marks = True
|
||||
self.sams = True # Legacy parameter do not use
|
||||
self.cold_start = False # Legacy parameter do not use
|
||||
self.version = None
|
||||
|
||||
# Performance oriented
|
||||
self.perf_red_alert_state = True
|
||||
self.perf_smoke_gen = True
|
||||
self.perf_artillery = True
|
||||
self.perf_moving_units = True
|
||||
self.perf_infantry = True
|
||||
self.perf_ai_parking_start = True
|
||||
self.perf_destroyed_units = True
|
||||
|
||||
# Performance culling
|
||||
self.perf_culling = False
|
||||
self.perf_culling_distance = 100
|
||||
|
||||
# LUA Plugins system
|
||||
self.plugins: Dict[str, bool] = {}
|
||||
|
||||
# Cheating
|
||||
self.show_red_ato = False
|
||||
|
||||
self.never_delay_player_flights = False
|
||||
|
||||
@staticmethod
|
||||
def plugin_settings_key(identifier: str) -> str:
|
||||
return f"plugins.{identifier}"
|
||||
|
||||
def initialize_plugin_option(self, identifier: str,
|
||||
default_value: bool) -> None:
|
||||
try:
|
||||
self.plugin_option(identifier)
|
||||
except KeyError:
|
||||
self.set_plugin_option(identifier, default_value)
|
||||
|
||||
def plugin_option(self, identifier: str) -> bool:
|
||||
return self.plugins[self.plugin_settings_key(identifier)]
|
||||
|
||||
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
|
||||
self.plugins[self.plugin_settings_key(identifier)] = enabled
|
||||
|
||||
def __setstate__(self, state) -> None:
|
||||
# __setstate__ is called with the dict of the object being unpickled. We
|
||||
# can provide save compatibility for new settings options (which
|
||||
# normally would not be present in the unpickled object) by creating a
|
||||
# new settings object, updating it with the unpickled state, and
|
||||
# updating our dict with that.
|
||||
new_state = Settings().__dict__
|
||||
new_state.update(state)
|
||||
self.__dict__.update(new_state)
|
||||
|
||||
14
game/utils.py
Normal file
14
game/utils.py
Normal file
@@ -0,0 +1,14 @@
|
||||
def meter_to_feet(value_in_meter: float) -> int:
|
||||
return int(3.28084 * value_in_meter)
|
||||
|
||||
|
||||
def feet_to_meter(value_in_feet: float) -> int:
|
||||
return int(value_in_feet / 3.28084)
|
||||
|
||||
|
||||
def meter_to_nm(value_in_meter: float) -> int:
|
||||
return int(value_in_meter / 1852)
|
||||
|
||||
|
||||
def nm_to_meter(value_in_nm: float) -> int:
|
||||
return int(value_in_nm * 1852)
|
||||
18
game/version.py
Normal file
18
game/version.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _build_version_string() -> str:
|
||||
components = ["2.2.0"]
|
||||
build_number_path = Path("resources/buildnumber")
|
||||
if build_number_path.exists():
|
||||
with build_number_path.open("r") as build_number_file:
|
||||
components.append(build_number_file.readline())
|
||||
|
||||
if not Path("resources/final").exists():
|
||||
components.append("preview")
|
||||
|
||||
return "-".join(components)
|
||||
|
||||
|
||||
#: Current version of Liberation.
|
||||
VERSION = _build_version_string()
|
||||
183
game/weather.py
Normal file
183
game/weather.py
Normal file
@@ -0,0 +1,183 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from dcs.weather import Weather as PydcsWeather, Wind
|
||||
|
||||
from game.settings import Settings
|
||||
from theater import ConflictTheater
|
||||
|
||||
|
||||
class TimeOfDay(Enum):
|
||||
Dawn = "dawn"
|
||||
Day = "day"
|
||||
Dusk = "dusk"
|
||||
Night = "night"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WindConditions:
|
||||
at_0m: Wind
|
||||
at_2000m: Wind
|
||||
at_8000m: Wind
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Clouds:
|
||||
base: int
|
||||
density: int
|
||||
thickness: int
|
||||
precipitation: PydcsWeather.Preceptions
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Fog:
|
||||
visibility: int
|
||||
thickness: int
|
||||
|
||||
|
||||
class Weather:
|
||||
def __init__(self) -> None:
|
||||
self.clouds = self.generate_clouds()
|
||||
self.fog = self.generate_fog()
|
||||
self.wind = self.generate_wind()
|
||||
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
raise NotImplementedError
|
||||
|
||||
def generate_fog(self) -> Optional[Fog]:
|
||||
if random.randrange(5) != 0:
|
||||
return None
|
||||
return Fog(
|
||||
visibility=random.randint(2500, 5000),
|
||||
thickness=random.randint(100, 500)
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def random_wind(minimum: int, maximum) -> WindConditions:
|
||||
wind_direction = random.randint(0, 360)
|
||||
at_0m_factor = 1
|
||||
at_2000m_factor = 2
|
||||
at_8000m_factor = 3
|
||||
base_wind = random.randint(minimum, maximum)
|
||||
|
||||
return WindConditions(
|
||||
# Always some wind to make the smoke move a bit.
|
||||
at_0m=Wind(wind_direction, max(1, base_wind * at_0m_factor)),
|
||||
at_2000m=Wind(wind_direction, base_wind * at_2000m_factor),
|
||||
at_8000m=Wind(wind_direction, base_wind * at_8000m_factor)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def random_cloud_base() -> int:
|
||||
return random.randint(2000, 3000)
|
||||
|
||||
@staticmethod
|
||||
def random_cloud_thickness() -> int:
|
||||
return random.randint(100, 400)
|
||||
|
||||
|
||||
class ClearSkies(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return None
|
||||
|
||||
def generate_fog(self) -> Optional[Fog]:
|
||||
return None
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 0)
|
||||
|
||||
|
||||
class Cloudy(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds(
|
||||
base=self.random_cloud_base(),
|
||||
density=random.randint(1, 8),
|
||||
thickness=self.random_cloud_thickness(),
|
||||
precipitation=PydcsWeather.Preceptions.None_
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 4)
|
||||
|
||||
|
||||
class Raining(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds(
|
||||
base=self.random_cloud_base(),
|
||||
density=random.randint(5, 8),
|
||||
thickness=self.random_cloud_thickness(),
|
||||
precipitation=PydcsWeather.Preceptions.Rain
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 6)
|
||||
|
||||
|
||||
class Thunderstorm(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds(
|
||||
base=self.random_cloud_base(),
|
||||
density=random.randint(9, 10),
|
||||
thickness=self.random_cloud_thickness(),
|
||||
precipitation=PydcsWeather.Preceptions.Thunderstorm
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 8)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Conditions:
|
||||
time_of_day: TimeOfDay
|
||||
start_time: datetime.datetime
|
||||
weather: Weather
|
||||
|
||||
@classmethod
|
||||
def generate(cls, theater: ConflictTheater, day: datetime.date,
|
||||
time_of_day: TimeOfDay, settings: Settings) -> Conditions:
|
||||
return cls(
|
||||
time_of_day=time_of_day,
|
||||
start_time=cls.generate_start_time(
|
||||
theater, day, time_of_day, settings.night_disabled
|
||||
),
|
||||
weather=cls.generate_weather()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate_start_time(cls, theater: ConflictTheater, day: datetime.date,
|
||||
time_of_day: TimeOfDay,
|
||||
night_disabled: bool) -> datetime.datetime:
|
||||
if night_disabled:
|
||||
logging.info("Skip Night mission due to user settings")
|
||||
time_range = {
|
||||
TimeOfDay.Dawn: (8, 9),
|
||||
TimeOfDay.Day: (10, 12),
|
||||
TimeOfDay.Dusk: (12, 14),
|
||||
TimeOfDay.Night: (14, 17),
|
||||
}[time_of_day]
|
||||
else:
|
||||
time_range = theater.daytime_map[time_of_day.value]
|
||||
|
||||
time = datetime.time(hour=random.randint(*time_range))
|
||||
return datetime.datetime.combine(day, time)
|
||||
|
||||
@classmethod
|
||||
def generate_weather(cls) -> Weather:
|
||||
chances = {
|
||||
Thunderstorm: 1,
|
||||
Raining: 20,
|
||||
Cloudy: 60,
|
||||
ClearSkies: 20,
|
||||
}
|
||||
weather_type = random.choices(list(chances.keys()),
|
||||
weights=list(chances.values()))[0]
|
||||
return weather_type()
|
||||
@@ -1,12 +1,13 @@
|
||||
from .aaa import *
|
||||
from .aircraft import *
|
||||
from .armor import *
|
||||
from .awacsgen import *
|
||||
from .airsupportgen import *
|
||||
from .conflictgen import *
|
||||
from .shipgen import *
|
||||
from .visualgen import *
|
||||
from .triggergen import *
|
||||
from .environmentgen import *
|
||||
from .groundobjectsgen import *
|
||||
from .briefinggen import *
|
||||
from .forcedoptionsgen import *
|
||||
from .kneeboard import *
|
||||
|
||||
from . import naming
|
||||
|
||||
|
||||
74
gen/aaa.py
74
gen/aaa.py
@@ -1,74 +0,0 @@
|
||||
from game import *
|
||||
|
||||
from theater.conflicttheater import ConflictTheater
|
||||
from .conflictgen import *
|
||||
from .naming import *
|
||||
|
||||
from dcs.mission import *
|
||||
|
||||
DISTANCE_FACTOR = 0.5, 1
|
||||
EXTRA_AA_MIN_DISTANCE = 35000
|
||||
EXTRA_AA_POSITION_FROM_CP = 550
|
||||
|
||||
|
||||
class AAConflictGenerator:
|
||||
def __init__(self, mission: Mission, conflict: Conflict):
|
||||
self.m = mission
|
||||
self.conflict = conflict
|
||||
|
||||
def generate_at_defenders_location(self, units: db.AirDefenseDict):
|
||||
for unit_type, count in units.items():
|
||||
for _ in range(count):
|
||||
self.m.vehicle_group(
|
||||
country=self.conflict.defenders_side,
|
||||
name=namegen.next_ground_group_name(),
|
||||
_type=unit_type,
|
||||
position=self.conflict.ground_defenders_location.random_point_within(100, 100),
|
||||
group_size=1)
|
||||
|
||||
def generate(self, units: db.AirDefenseDict):
|
||||
for type, count in units.items():
|
||||
for _, radial in zip(range(count), self.conflict.radials):
|
||||
distance = randint(self.conflict.size * DISTANCE_FACTOR[0], self.conflict.size * DISTANCE_FACTOR[1])
|
||||
p = self.conflict.position.point_from_heading(radial, distance)
|
||||
|
||||
self.m.vehicle_group(
|
||||
country=self.conflict.defenders_side,
|
||||
name=namegen.next_ground_group_name(),
|
||||
_type=type,
|
||||
position=p,
|
||||
group_size=1)
|
||||
|
||||
|
||||
class ExtraAAConflictGenerator:
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game, player_name: Country, enemy_name: Country):
|
||||
self.mission = mission
|
||||
self.game = game
|
||||
self.conflict = conflict
|
||||
self.player_name = player_name
|
||||
self.enemy_name = enemy_name
|
||||
|
||||
def generate(self):
|
||||
from theater.conflicttheater import ControlPoint
|
||||
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.is_global:
|
||||
continue
|
||||
|
||||
if cp.position.distance_to_point(self.conflict.position) < EXTRA_AA_MIN_DISTANCE:
|
||||
continue
|
||||
|
||||
if cp.position.distance_to_point(self.conflict.from_cp.position) < EXTRA_AA_MIN_DISTANCE:
|
||||
continue
|
||||
|
||||
country_name = cp.captured and self.player_name or self.enemy_name
|
||||
position = cp.position.point_from_heading(0, EXTRA_AA_POSITION_FROM_CP)
|
||||
|
||||
self.mission.vehicle_group(
|
||||
country=self.mission.country(country_name),
|
||||
name=namegen.next_ground_group_name(),
|
||||
_type=db.EXTRA_AA[country_name],
|
||||
position=position,
|
||||
group_size=2
|
||||
)
|
||||
|
||||
1668
gen/aircraft.py
1668
gen/aircraft.py
File diff suppressed because it is too large
Load Diff
1505
gen/airfields.py
Normal file
1505
gen/airfields.py
Normal file
File diff suppressed because it is too large
Load Diff
145
gen/airsupportgen.py
Normal file
145
gen/airsupportgen.py
Normal file
@@ -0,0 +1,145 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Type
|
||||
|
||||
from dcs.mission import Mission, StartType
|
||||
from dcs.planes import IL_78M
|
||||
from dcs.task import (
|
||||
AWACS,
|
||||
ActivateBeaconCommand,
|
||||
MainTask,
|
||||
Refueling,
|
||||
SetImmortalCommand,
|
||||
SetInvisibleCommand,
|
||||
)
|
||||
|
||||
from game import db
|
||||
from .naming import namegen
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .conflictgen import Conflict
|
||||
from .radios import RadioFrequency, RadioRegistry
|
||||
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||
|
||||
TANKER_DISTANCE = 15000
|
||||
TANKER_ALT = 4572
|
||||
TANKER_HEADING_OFFSET = 45
|
||||
|
||||
AWACS_DISTANCE = 150000
|
||||
AWACS_ALT = 13000
|
||||
|
||||
|
||||
@dataclass
|
||||
class AwacsInfo:
|
||||
"""AWACS information for the kneeboard."""
|
||||
dcsGroupName: str
|
||||
callsign: str
|
||||
freq: RadioFrequency
|
||||
|
||||
|
||||
@dataclass
|
||||
class TankerInfo:
|
||||
"""Tanker information for the kneeboard."""
|
||||
dcsGroupName: str
|
||||
callsign: str
|
||||
variant: str
|
||||
freq: RadioFrequency
|
||||
tacan: TacanChannel
|
||||
|
||||
|
||||
@dataclass
|
||||
class AirSupport:
|
||||
awacs: List[AwacsInfo] = field(default_factory=list)
|
||||
tankers: List[TankerInfo] = field(default_factory=list)
|
||||
|
||||
|
||||
class AirSupportConflictGenerator:
|
||||
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game,
|
||||
radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry) -> None:
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
self.air_support = AirSupport()
|
||||
self.radio_registry = radio_registry
|
||||
self.tacan_registry = tacan_registry
|
||||
|
||||
@classmethod
|
||||
def support_tasks(cls) -> List[Type[MainTask]]:
|
||||
return [Refueling, AWACS]
|
||||
|
||||
def generate(self, is_awacs_enabled):
|
||||
player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp
|
||||
|
||||
fallback_tanker_number = 0
|
||||
|
||||
for i, tanker_unit_type in enumerate(db.find_unittype(Refueling, self.conflict.attackers_side)):
|
||||
variant = db.unit_type_name(tanker_unit_type)
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
|
||||
tanker_heading = self.conflict.to_cp.position.heading_between_point(self.conflict.from_cp.position) + TANKER_HEADING_OFFSET * i
|
||||
tanker_position = player_cp.position.point_from_heading(tanker_heading, TANKER_DISTANCE)
|
||||
tanker_group = self.mission.refuel_flight(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
name=namegen.next_tanker_name(self.mission.country(self.game.player_country), tanker_unit_type),
|
||||
airport=None,
|
||||
plane_type=tanker_unit_type,
|
||||
position=tanker_position,
|
||||
altitude=TANKER_ALT,
|
||||
race_distance=58000,
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
speed=574,
|
||||
tacanchannel=str(tacan),
|
||||
)
|
||||
tanker_group.set_frequency(freq.mhz)
|
||||
|
||||
callsign = callsign_for_support_unit(tanker_group)
|
||||
tacan_callsign = {
|
||||
"Texaco": "TEX",
|
||||
"Arco": "ARC",
|
||||
"Shell": "SHL",
|
||||
}.get(callsign)
|
||||
if tacan_callsign is None:
|
||||
# The dict above is all the callsigns currently in the game, but
|
||||
# non-Western countries don't use the callsigns and instead just
|
||||
# use numbers. It's possible that none of those nations have
|
||||
# TACAN compatible refueling aircraft, but fallback just in
|
||||
# case.
|
||||
tacan_callsign = f"TK{fallback_tanker_number}"
|
||||
fallback_tanker_number += 1
|
||||
|
||||
if tanker_unit_type != IL_78M:
|
||||
# Override PyDCS tacan channel.
|
||||
tanker_group.points[0].tasks.pop()
|
||||
tanker_group.points[0].tasks.append(ActivateBeaconCommand(
|
||||
tacan.number, tacan.band.value, tacan_callsign, True,
|
||||
tanker_group.units[0].id, True))
|
||||
|
||||
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.air_support.tankers.append(TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan))
|
||||
|
||||
if is_awacs_enabled:
|
||||
try:
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
awacs_unit = db.find_unittype(AWACS, self.conflict.attackers_side)[0]
|
||||
awacs_flight = self.mission.awacs_flight(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
name=namegen.next_awacs_name(self.mission.country(self.game.player_country)),
|
||||
plane_type=awacs_unit,
|
||||
altitude=AWACS_ALT,
|
||||
airport=None,
|
||||
position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE),
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
)
|
||||
awacs_flight.set_frequency(freq.mhz)
|
||||
|
||||
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.air_support.awacs.append(AwacsInfo(
|
||||
str(awacs_flight.name), callsign_for_support_unit(awacs_flight), freq))
|
||||
except:
|
||||
print("No AWACS for faction")
|
||||
522
gen/armor.py
522
gen/armor.py
@@ -1,64 +1,490 @@
|
||||
from game import db
|
||||
from .conflictgen import *
|
||||
from .naming import *
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from dcs.mission import *
|
||||
from dcs.unittype import *
|
||||
from dcs.point import *
|
||||
from dcs.task import *
|
||||
from dcs.country import *
|
||||
from dcs import Mission
|
||||
from dcs.action import AITaskPush
|
||||
from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged
|
||||
from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
from dcs.planes import MQ_9_Reaper
|
||||
from dcs.point import PointAction
|
||||
from dcs.task import (
|
||||
AttackGroup,
|
||||
ControlledTask,
|
||||
EPLRS,
|
||||
FireAtPoint,
|
||||
GoToWaypoint,
|
||||
Hold,
|
||||
OrbitAction,
|
||||
SetImmortalCommand,
|
||||
SetInvisibleCommand,
|
||||
)
|
||||
from dcs.triggers import Event, TriggerOnce
|
||||
from dcs.unit import Vehicle
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game import db
|
||||
from .naming import namegen
|
||||
from gen.ground_forces.ai_ground_planner import (
|
||||
CombatGroupRole,
|
||||
DISTANCE_FROM_FRONTLINE,
|
||||
)
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .conflictgen import Conflict
|
||||
from .ground_forces.combat_stance import CombatStance
|
||||
from game.plugins import LuaPluginManager
|
||||
|
||||
SPREAD_DISTANCE_FACTOR = 0.1, 0.3
|
||||
SPREAD_DISTANCE_SIZE_FACTOR = 0.1
|
||||
|
||||
FRONTLINE_CAS_FIGHTS_COUNT = 16, 24
|
||||
FRONTLINE_CAS_GROUP_MIN = 1, 2
|
||||
FRONTLINE_CAS_PADDING = 12000
|
||||
|
||||
class ArmorConflictGenerator:
|
||||
def __init__(self, mission: Mission, conflict: Conflict):
|
||||
self.m = mission
|
||||
RETREAT_DISTANCE = 20000
|
||||
BREAKTHROUGH_OFFENSIVE_DISTANCE = 35000
|
||||
AGGRESIVE_MOVE_DISTANCE = 16000
|
||||
|
||||
FIGHT_DISTANCE = 3500
|
||||
|
||||
RANDOM_OFFSET_ATTACK = 250
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JtacInfo:
|
||||
"""JTAC information."""
|
||||
dcsGroupName: str
|
||||
unit_name: str
|
||||
callsign: str
|
||||
region: str
|
||||
code: str
|
||||
# TODO: Radio info? Type?
|
||||
|
||||
|
||||
class GroundConflictGenerator:
|
||||
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance):
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
self.enemy_planned_combat_groups = enemy_planned_combat_groups
|
||||
self.player_planned_combat_groups = player_planned_combat_groups
|
||||
self.player_stance = CombatStance(player_stance)
|
||||
self.enemy_stance = random.choice([CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]) if len(enemy_planned_combat_groups) > len(player_planned_combat_groups) else random.choice([CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.AMBUSH, CombatStance.AGGRESSIVE])
|
||||
self.game = game
|
||||
self.jtacs: List[JtacInfo] = []
|
||||
|
||||
def _group_point(self, point) -> Point:
|
||||
distance = randint(
|
||||
distance = random.randint(
|
||||
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]),
|
||||
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[1]),
|
||||
)
|
||||
|
||||
return point.random_point_within(distance, self.conflict.size * SPREAD_DISTANCE_SIZE_FACTOR)
|
||||
|
||||
def _generate_group(self, side: Country, unit: VehicleType, count: int, at: Point):
|
||||
def generate(self):
|
||||
|
||||
player_groups = []
|
||||
enemy_groups = []
|
||||
|
||||
combat_width = self.conflict.distance/2
|
||||
if combat_width > 500000:
|
||||
combat_width = 500000
|
||||
if combat_width < 35000:
|
||||
combat_width = 35000
|
||||
|
||||
position = Conflict.frontline_position(self.game.theater, self.conflict.from_cp, self.conflict.to_cp)
|
||||
|
||||
# Create player groups at random position
|
||||
for group in self.player_planned_combat_groups:
|
||||
if group.role == CombatGroupRole.ARTILLERY:
|
||||
distance_from_frontline = self.get_artilery_group_distance_from_frontline(group)
|
||||
else:
|
||||
distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role]
|
||||
final_position = self.get_valid_position_for_group(position, True, combat_width, distance_from_frontline)
|
||||
|
||||
if final_position is not None:
|
||||
g = self._generate_group(
|
||||
side=self.mission.country(self.game.player_country),
|
||||
unit=group.units[0],
|
||||
heading=self.conflict.heading+90,
|
||||
count=len(group.units),
|
||||
at=final_position)
|
||||
g.set_skill(self.game.settings.player_skill)
|
||||
player_groups.append((g,group))
|
||||
|
||||
self.gen_infantry_group_for_group(g, True, self.mission.country(self.game.player_country), self.conflict.heading + 90)
|
||||
|
||||
# Create enemy groups at random position
|
||||
for group in self.enemy_planned_combat_groups:
|
||||
if group.role == CombatGroupRole.ARTILLERY:
|
||||
distance_from_frontline = self.get_artilery_group_distance_from_frontline(group)
|
||||
else:
|
||||
distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role]
|
||||
final_position = self.get_valid_position_for_group(position, False, combat_width, distance_from_frontline)
|
||||
|
||||
if final_position is not None:
|
||||
g = self._generate_group(
|
||||
side=self.mission.country(self.game.enemy_country),
|
||||
unit=group.units[0],
|
||||
heading=self.conflict.heading - 90,
|
||||
count=len(group.units),
|
||||
at=final_position)
|
||||
g.set_skill(self.game.settings.enemy_vehicle_skill)
|
||||
enemy_groups.append((g, group))
|
||||
|
||||
self.gen_infantry_group_for_group(g, False, self.mission.country(self.game.enemy_country), self.conflict.heading - 90)
|
||||
|
||||
# Plan combat actions for groups
|
||||
self.plan_action_for_groups(self.player_stance, player_groups, enemy_groups, self.conflict.heading + 90, self.conflict.from_cp, self.conflict.to_cp)
|
||||
self.plan_action_for_groups(self.enemy_stance, enemy_groups, player_groups, self.conflict.heading - 90, self.conflict.to_cp, self.conflict.from_cp)
|
||||
|
||||
# Add JTAC
|
||||
if self.game.player_faction.has_jtac:
|
||||
n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id)
|
||||
code = 1688 - len(self.jtacs)
|
||||
|
||||
utype = MQ_9_Reaper
|
||||
if self.game.player_faction.jtac_unit is not None:
|
||||
utype = self.game.player_faction.jtac_unit
|
||||
|
||||
jtac = self.mission.flight_group(country=self.mission.country(self.game.player_country),
|
||||
name=n,
|
||||
aircraft_type=utype,
|
||||
position=position[0],
|
||||
airport=None,
|
||||
altitude=5000)
|
||||
jtac.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
jtac.points[0].tasks.append(SetImmortalCommand(True))
|
||||
jtac.points[0].tasks.append(OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle))
|
||||
frontline = f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}"
|
||||
# Note: Will need to change if we ever add ground based JTAC.
|
||||
callsign = callsign_for_support_unit(jtac)
|
||||
self.jtacs.append(JtacInfo(str(jtac.name), n, callsign, frontline, str(code)))
|
||||
|
||||
def gen_infantry_group_for_group(self, group, is_player, side:Country, forward_heading):
|
||||
|
||||
# Disable infantry unit gen if disabled
|
||||
if not self.game.settings.perf_infantry:
|
||||
return
|
||||
|
||||
infantry_position = group.points[0].position.random_point_within(250, 50)
|
||||
|
||||
if side == self.conflict.attackers_country:
|
||||
cp = self.conflict.from_cp
|
||||
else:
|
||||
cp = self.conflict.to_cp
|
||||
|
||||
if is_player:
|
||||
faction = self.game.player_name
|
||||
else:
|
||||
faction = self.game.enemy_name
|
||||
|
||||
possible_infantry_units = db.find_infantry(faction)
|
||||
if len(possible_infantry_units) == 0:
|
||||
return
|
||||
|
||||
u = random.choice(possible_infantry_units)
|
||||
self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp, u), u,
|
||||
position=infantry_position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
move_formation=PointAction.OffRoad)
|
||||
|
||||
for i in range(random.randint(3, 10)):
|
||||
u = random.choice(possible_infantry_units)
|
||||
position = infantry_position.random_point_within(55, 5)
|
||||
self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp, u), u,
|
||||
position=position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
move_formation=PointAction.OffRoad)
|
||||
|
||||
|
||||
def plan_action_for_groups(self, stance, ally_groups, enemy_groups, forward_heading, from_cp, to_cp):
|
||||
|
||||
if not self.game.settings.perf_moving_units:
|
||||
return
|
||||
|
||||
for dcs_group, group in ally_groups:
|
||||
|
||||
if hasattr(group.units[0], 'eplrs'):
|
||||
if group.units[0].eplrs:
|
||||
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
|
||||
|
||||
if group.role == CombatGroupRole.ARTILLERY:
|
||||
# Fire on any ennemy in range
|
||||
if self.game.settings.perf_artillery:
|
||||
target = self.get_artillery_target_in_range(dcs_group, group, enemy_groups)
|
||||
if target is not None:
|
||||
|
||||
if stance != CombatStance.RETREAT:
|
||||
hold_task = Hold()
|
||||
hold_task.number = 1
|
||||
dcs_group.add_trigger_action(hold_task)
|
||||
|
||||
# Artillery strike random start
|
||||
artillery_trigger = TriggerOnce(Event.NoEvent, "ArtilleryFireTask #" + str(dcs_group.id))
|
||||
artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45)* 60))
|
||||
|
||||
fire_task = FireAtPoint(target, len(group.units) * 10, 100)
|
||||
if stance != CombatStance.RETREAT:
|
||||
fire_task.number = 2
|
||||
else:
|
||||
fire_task.number = 1
|
||||
dcs_group.add_trigger_action(fire_task)
|
||||
artillery_trigger.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
|
||||
self.mission.triggerrules.triggers.append(artillery_trigger)
|
||||
|
||||
# Artillery will fall back when under attack
|
||||
if stance != CombatStance.RETREAT:
|
||||
|
||||
# Hold position
|
||||
dcs_group.points[0].tasks.append(Hold())
|
||||
retreat = self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE/3))
|
||||
dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad)
|
||||
dcs_group.points[1].tasks.append(Hold())
|
||||
dcs_group.add_waypoint(retreat, PointAction.OffRoad)
|
||||
|
||||
artillery_fallback = TriggerOnce(Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id))
|
||||
for i, u in enumerate(dcs_group.units):
|
||||
artillery_fallback.add_condition(UnitDamaged(u.id))
|
||||
if i < len(dcs_group.units) - 1:
|
||||
artillery_fallback.add_condition(Or())
|
||||
|
||||
hold_2 = Hold()
|
||||
hold_2.number = 3
|
||||
dcs_group.add_trigger_action(hold_2)
|
||||
|
||||
retreat_task = GoToWaypoint(toIndex=3)
|
||||
retreat_task.number = 4
|
||||
dcs_group.add_trigger_action(retreat_task)
|
||||
|
||||
artillery_fallback.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
|
||||
self.mission.triggerrules.triggers.append(artillery_fallback)
|
||||
|
||||
for u in dcs_group.units:
|
||||
u.initial = True
|
||||
u.heading = forward_heading + random.randint(-5,5)
|
||||
|
||||
elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]:
|
||||
if stance == CombatStance.AGGRESSIVE:
|
||||
# Attack nearest enemy if any
|
||||
# Then move forward OR Attack enemy base if it is not too far away
|
||||
target = self.find_nearest_enemy_group(dcs_group, enemy_groups)
|
||||
if target is not None:
|
||||
rand_offset = Point(random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK), random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK))
|
||||
dcs_group.add_waypoint(target.points[0].position + rand_offset, PointAction.OffRoad)
|
||||
dcs_group.points[1].tasks.append(AttackGroup(target.id))
|
||||
|
||||
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
|
||||
attack_point = to_cp.position.random_point_within(500, 0)
|
||||
else:
|
||||
attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE)
|
||||
dcs_group.add_waypoint(attack_point, PointAction.OnRoad)
|
||||
elif stance == CombatStance.BREAKTHROUGH:
|
||||
# In breakthrough mode, the units will move forward
|
||||
# If the enemy base is close enough, the units will attack the base
|
||||
if to_cp.position.distance_to_point(
|
||||
dcs_group.points[0].position) <= BREAKTHROUGH_OFFENSIVE_DISTANCE:
|
||||
attack_point = to_cp.position.random_point_within(500, 0)
|
||||
else:
|
||||
attack_point = self.find_offensive_point(dcs_group, forward_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE)
|
||||
dcs_group.add_waypoint(attack_point, PointAction.OnRoad)
|
||||
elif stance == CombatStance.ELIMINATION:
|
||||
# In elimination mode, the units focus on destroying as much enemy groups as possible
|
||||
targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3)
|
||||
i = 1
|
||||
for target in targets:
|
||||
rand_offset = Point(random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK), random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK))
|
||||
dcs_group.add_waypoint(target.points[0].position+rand_offset, PointAction.OffRoad)
|
||||
dcs_group.points[i].tasks.append(AttackGroup(target.id))
|
||||
i = i + 1
|
||||
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
|
||||
attack_point = to_cp.position.random_point_within(500, 0)
|
||||
dcs_group.add_waypoint(attack_point)
|
||||
|
||||
if stance != CombatStance.RETREAT:
|
||||
self.add_morale_trigger(dcs_group, forward_heading)
|
||||
|
||||
elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]:
|
||||
|
||||
if stance in [CombatStance.AGGRESSIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]:
|
||||
# APC & ATGM will never move too much forward, but will follow along any offensive
|
||||
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
|
||||
attack_point = to_cp.position.random_point_within(500, 0)
|
||||
else:
|
||||
attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE)
|
||||
dcs_group.add_waypoint(attack_point, PointAction.OnRoad)
|
||||
|
||||
if stance != CombatStance.RETREAT:
|
||||
self.add_morale_trigger(dcs_group, forward_heading)
|
||||
|
||||
if stance == CombatStance.RETREAT:
|
||||
# In retreat mode, the units will fall back
|
||||
# If the ally base is close enough, the units will even regroup there
|
||||
if from_cp.position.distance_to_point(dcs_group.points[0].position) <= RETREAT_DISTANCE:
|
||||
retreat_point = from_cp.position.random_point_within(500, 250)
|
||||
else:
|
||||
retreat_point = self.find_retreat_point(dcs_group, forward_heading)
|
||||
reposition_point = retreat_point.point_from_heading(forward_heading, 10) # Another point to make the unit face the enemy
|
||||
dcs_group.add_waypoint(retreat_point, PointAction.OnRoad)
|
||||
dcs_group.add_waypoint(reposition_point, PointAction.OffRoad)
|
||||
|
||||
|
||||
def add_morale_trigger(self, dcs_group, forward_heading):
|
||||
"""
|
||||
This add a trigger to manage units fleeing whenever their group is hit hard, or being engaged by CAS
|
||||
"""
|
||||
|
||||
if len(dcs_group.units) == 1:
|
||||
return
|
||||
|
||||
# Units should hold position on last waypoint
|
||||
dcs_group.points[len(dcs_group.points) - 1].tasks.append(Hold())
|
||||
|
||||
# Force unit heading
|
||||
for unit in dcs_group.units:
|
||||
unit.heading = forward_heading
|
||||
dcs_group.manualHeading = True
|
||||
|
||||
# We add a new retreat waypoint
|
||||
dcs_group.add_waypoint(self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)), PointAction.OffRoad)
|
||||
|
||||
# Fallback task
|
||||
fallback = ControlledTask(GoToWaypoint(toIndex=len(dcs_group.points)))
|
||||
fallback.enabled = False
|
||||
dcs_group.add_trigger_action(Hold())
|
||||
dcs_group.add_trigger_action(fallback)
|
||||
|
||||
# Create trigger
|
||||
fallback = TriggerOnce(Event.NoEvent, "Morale manager #" + str(dcs_group.id))
|
||||
|
||||
# Usually more than 50% casualties = RETREAT
|
||||
fallback.add_condition(GroupLifeLess(dcs_group.id, random.randint(51, 76)))
|
||||
|
||||
# Do retreat to the configured retreat waypoint
|
||||
fallback.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
|
||||
|
||||
self.mission.triggerrules.triggers.append(fallback)
|
||||
|
||||
|
||||
def find_retreat_point(self, dcs_group, frontline_heading, distance=RETREAT_DISTANCE):
|
||||
"""
|
||||
Find a point to retreat to
|
||||
:param dcs_group: DCS mission group we are searching a retreat point for
|
||||
:param frontline_heading: Heading of the frontline
|
||||
:return: dcs.mapping.Point object with the desired position
|
||||
"""
|
||||
return dcs_group.points[0].position.point_from_heading(frontline_heading-180, distance)
|
||||
|
||||
def find_offensive_point(self, dcs_group, frontline_heading, distance):
|
||||
"""
|
||||
Find a point to attack
|
||||
:param dcs_group: DCS mission group we are searching an attack point for
|
||||
:param frontline_heading: Heading of the frontline
|
||||
:param distance: Distance of the offensive (how far unit should move)
|
||||
:return: dcs.mapping.Point object with the desired position
|
||||
"""
|
||||
return dcs_group.points[0].position.point_from_heading(frontline_heading, distance)
|
||||
|
||||
def find_n_nearest_enemy_groups(self, player_group, enemy_groups, n):
|
||||
"""
|
||||
Return the neaarest enemy group for the player group
|
||||
@param group Group for which we should find the nearest ennemies
|
||||
@param enemy_groups Potential enemy groups
|
||||
@param n number of nearby groups to take
|
||||
"""
|
||||
targets = []
|
||||
sorted_list = sorted(enemy_groups, key=lambda group: player_group.points[0].position.distance_to_point(group[0].points[0].position))
|
||||
for i in range(n):
|
||||
if len(sorted_list) <= i:
|
||||
break
|
||||
else:
|
||||
targets.append(sorted_list[i][0])
|
||||
return targets
|
||||
|
||||
|
||||
def find_nearest_enemy_group(self, player_group, enemy_groups):
|
||||
"""
|
||||
Search the enemy groups for a potential target suitable to armored assault
|
||||
@param group Group for which we should find the nearest ennemy
|
||||
@param enemy_groups Potential enemy groups
|
||||
"""
|
||||
min_distance = 99999999
|
||||
target = None
|
||||
for dcs_group, group in enemy_groups:
|
||||
dist = player_group.points[0].position.distance_to_point(dcs_group.points[0].position)
|
||||
if dist < min_distance:
|
||||
min_distance = dist
|
||||
target = dcs_group
|
||||
return target
|
||||
|
||||
|
||||
def get_artillery_target_in_range(self, dcs_group, group, enemy_groups):
|
||||
"""
|
||||
Search the enemy groups for a potential target suitable to an artillery unit
|
||||
"""
|
||||
rng = group.units[0].threat_range
|
||||
if len(enemy_groups) == 0:
|
||||
return None
|
||||
for o in range(10):
|
||||
potential_target = random.choice(enemy_groups)[0]
|
||||
distance_to_target = dcs_group.points[0].position.distance_to_point(potential_target.points[0].position)
|
||||
if distance_to_target < rng:
|
||||
return potential_target.points[0].position
|
||||
return None
|
||||
|
||||
|
||||
def get_artilery_group_distance_from_frontline(self, group):
|
||||
"""
|
||||
For artilery group, decide the distance from frontline with the range of the unit
|
||||
"""
|
||||
rg = group.units[0].threat_range - 7500
|
||||
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY]:
|
||||
rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY]
|
||||
if rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK]:
|
||||
rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK] + 100
|
||||
return rg
|
||||
|
||||
|
||||
def get_valid_position_for_group(self, conflict_position, isplayer, combat_width, distance_from_frontline):
|
||||
i = 0
|
||||
while i < 25: # 25 attempt for valid position
|
||||
heading_diff = -90 if isplayer else 90
|
||||
shifted = conflict_position[0].point_from_heading(self.conflict.heading,
|
||||
random.randint((int)(-combat_width / 2), (int)(combat_width / 2)))
|
||||
final_position = shifted.point_from_heading(self.conflict.heading + heading_diff, distance_from_frontline)
|
||||
|
||||
if self.conflict.theater.is_on_land(final_position):
|
||||
return final_position
|
||||
else:
|
||||
i = i + 1
|
||||
continue
|
||||
return None
|
||||
|
||||
def _generate_group(self, side: Country, unit: VehicleType, count: int, at: Point, move_formation: PointAction = PointAction.OffRoad, heading=0):
|
||||
|
||||
if side == self.conflict.attackers_country:
|
||||
cp = self.conflict.from_cp
|
||||
else:
|
||||
cp = self.conflict.to_cp
|
||||
|
||||
logging.info("armorgen: {} for {}".format(unit, side.id))
|
||||
group = self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_unit_name(side, cp.id, unit), unit,
|
||||
position=self._group_point(at),
|
||||
group_size=count,
|
||||
heading=heading,
|
||||
move_formation=move_formation)
|
||||
|
||||
for c in range(count):
|
||||
group = self.m.vehicle_group(
|
||||
side,
|
||||
namegen.next_armor_group_name(),
|
||||
unit,
|
||||
position=self._group_point(at),
|
||||
group_size=1,
|
||||
move_formation=PointAction.OffRoad)
|
||||
wayp = group.add_waypoint(self.conflict.position.point_from_heading(0, 500))
|
||||
wayp.tasks = []
|
||||
vehicle: Vehicle = group.units[c]
|
||||
vehicle.player_can_drive = True
|
||||
|
||||
def generate(self, attackers: db.ArmorDict, defenders: db.ArmorDict):
|
||||
for type, count in attackers.items():
|
||||
self._generate_group(
|
||||
side=self.conflict.attackers_side,
|
||||
unit=type,
|
||||
count=count,
|
||||
at=self.conflict.ground_attackers_location)
|
||||
|
||||
for type, count in defenders.items():
|
||||
self._generate_group(
|
||||
side=self.conflict.defenders_side,
|
||||
unit=type,
|
||||
count=count,
|
||||
at=self.conflict.ground_defenders_location)
|
||||
|
||||
def generate_passengers(self, count: int):
|
||||
unit_type = random.choice(db.find_unittype(Nothing, self.conflict.attackers_side.name))
|
||||
|
||||
self.m.vehicle_group(
|
||||
country=self.conflict.attackers_side,
|
||||
name=namegen.next_passenger_group_name(),
|
||||
_type=unit_type,
|
||||
position=self.conflict.ground_attackers_location,
|
||||
group_size=count
|
||||
)
|
||||
return group
|
||||
205
gen/ato.py
Normal file
205
gen/ato.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""Air Tasking Orders.
|
||||
|
||||
The classes of the Air Tasking Order (ATO) define all of the missions that have
|
||||
been planned, and which aircraft have been assigned to them. Each planned
|
||||
mission, or "package" is composed of individual flights. The package may contain
|
||||
dissimilar aircraft performing different roles, but all for the same goal. For
|
||||
example, the package to strike an enemy airfield may contain an escort flight,
|
||||
a SEAD flight, and the strike aircraft themselves. CAP packages may contain only
|
||||
the single CAP flight.
|
||||
"""
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
from theater.missiontarget import MissionTarget
|
||||
from .flights.flight import Flight, FlightType
|
||||
from .flights.flightplan import FormationFlightPlan
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Task:
|
||||
"""The main task of a flight or package."""
|
||||
|
||||
#: The type of task.
|
||||
task_type: FlightType
|
||||
|
||||
#: The location of the objective.
|
||||
location: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackageWaypoints:
|
||||
join: Point
|
||||
ingress: Point
|
||||
egress: Point
|
||||
split: Point
|
||||
|
||||
|
||||
@dataclass
|
||||
class Package:
|
||||
"""A mission package."""
|
||||
|
||||
#: The mission target. Currently can be either a ControlPoint or a
|
||||
#: TheaterGroundObject (non-ControlPoint map objectives).
|
||||
target: MissionTarget
|
||||
|
||||
#: The set of flights in the package.
|
||||
flights: List[Flight] = field(default_factory=list)
|
||||
|
||||
delay: int = field(default=0)
|
||||
|
||||
#: Desired TOT as an offset from mission start.
|
||||
time_over_target: timedelta = field(default=timedelta())
|
||||
|
||||
waypoints: Optional[PackageWaypoints] = field(default=None)
|
||||
|
||||
@property
|
||||
def formation_speed(self) -> Optional[int]:
|
||||
"""The speed of the package when in formation.
|
||||
|
||||
If none of the flights in the package will join a formation, this
|
||||
returns None. This is nto uncommon, since only strike-like (strike,
|
||||
DEAD, anti-ship, BAI, etc.) flights and their escorts fly in formation.
|
||||
Others (CAP and CAS, currently) will coordinate in target timing but
|
||||
fly their own path to the target.
|
||||
"""
|
||||
speeds = []
|
||||
for flight in self.flights:
|
||||
if isinstance(flight.flight_plan, FormationFlightPlan):
|
||||
speeds.append(flight.flight_plan.best_flight_formation_speed)
|
||||
if not speeds:
|
||||
return None
|
||||
return min(speeds)
|
||||
|
||||
# TODO: Should depend on the type of escort.
|
||||
# SEAD might be able to leave before CAP.
|
||||
@property
|
||||
def escort_start_time(self) -> Optional[timedelta]:
|
||||
times = []
|
||||
for flight in self.flights:
|
||||
waypoint = flight.flight_plan.request_escort_at()
|
||||
if waypoint is None:
|
||||
continue
|
||||
tot = flight.flight_plan.tot_for_waypoint(waypoint)
|
||||
if tot is None:
|
||||
logging.error(
|
||||
f"{flight} requested escort at {waypoint} but that "
|
||||
"waypoint has no TOT. It may not be escorted.")
|
||||
continue
|
||||
times.append(tot)
|
||||
if times:
|
||||
return min(times)
|
||||
return None
|
||||
|
||||
@property
|
||||
def escort_end_time(self) -> Optional[timedelta]:
|
||||
times = []
|
||||
for flight in self.flights:
|
||||
waypoint = flight.flight_plan.dismiss_escort_at()
|
||||
if waypoint is None:
|
||||
continue
|
||||
tot = flight.flight_plan.tot_for_waypoint(waypoint)
|
||||
if tot is None:
|
||||
tot = flight.flight_plan.depart_time_for_waypoint(waypoint)
|
||||
if tot is None:
|
||||
logging.error(
|
||||
f"{flight} dismissed escort at {waypoint} but that "
|
||||
"waypoint has no TOT or departure time. It may not be "
|
||||
"escorted.")
|
||||
continue
|
||||
times.append(tot)
|
||||
if times:
|
||||
return max(times)
|
||||
return None
|
||||
|
||||
def add_flight(self, flight: Flight) -> None:
|
||||
"""Adds a flight to the package."""
|
||||
self.flights.append(flight)
|
||||
|
||||
def remove_flight(self, flight: Flight) -> None:
|
||||
"""Removes a flight from the package."""
|
||||
self.flights.remove(flight)
|
||||
if not self.flights:
|
||||
self.waypoints = None
|
||||
|
||||
@property
|
||||
def primary_task(self) -> Optional[FlightType]:
|
||||
if not self.flights:
|
||||
return None
|
||||
|
||||
flight_counts: Dict[FlightType, int] = defaultdict(lambda: 0)
|
||||
for flight in self.flights:
|
||||
flight_counts[flight.flight_type] += 1
|
||||
|
||||
# The package will contain a mix of mission types, but in general we can
|
||||
# determine the goal of the mission because some mission types are more
|
||||
# likely to be the main task than others. For example, a package with
|
||||
# only CAP flights is a CAP package, a flight with CAP and strike is a
|
||||
# strike package, a flight with CAP and DEAD is a DEAD package, and a
|
||||
# flight with strike and SEAD is an OCA/Strike package. The type of
|
||||
# package is determined by the highest priority flight in the package.
|
||||
task_priorities = [
|
||||
FlightType.CAS,
|
||||
FlightType.STRIKE,
|
||||
FlightType.ANTISHIP,
|
||||
FlightType.BAI,
|
||||
FlightType.EVAC,
|
||||
FlightType.TROOP_TRANSPORT,
|
||||
FlightType.RECON,
|
||||
FlightType.ELINT,
|
||||
FlightType.DEAD,
|
||||
FlightType.SEAD,
|
||||
FlightType.LOGISTICS,
|
||||
FlightType.INTERCEPTION,
|
||||
FlightType.TARCAP,
|
||||
FlightType.CAP,
|
||||
FlightType.BARCAP,
|
||||
FlightType.EWAR,
|
||||
FlightType.ESCORT,
|
||||
]
|
||||
for task in task_priorities:
|
||||
if flight_counts[task]:
|
||||
return task
|
||||
|
||||
# If we get here, our task_priorities list above is incomplete. Log the
|
||||
# issue and return the type of *any* flight in the package.
|
||||
some_mission = next(iter(self.flights)).flight_type
|
||||
logging.warning(f"Unhandled mission type: {some_mission}")
|
||||
return some_mission
|
||||
|
||||
@property
|
||||
def package_description(self) -> str:
|
||||
"""Generates a package description based on flight composition."""
|
||||
task = self.primary_task
|
||||
if task is None:
|
||||
return "No mission"
|
||||
return task.name
|
||||
|
||||
def __hash__(self) -> int:
|
||||
# TODO: Far from perfect. Number packages?
|
||||
return hash(self.target.name)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AirTaskingOrder:
|
||||
"""The entire ATO for one coalition."""
|
||||
|
||||
#: The set of all planned packages in the ATO.
|
||||
packages: List[Package] = field(default_factory=list)
|
||||
|
||||
def add_package(self, package: Package) -> None:
|
||||
"""Adds a package to the ATO."""
|
||||
self.packages.append(package)
|
||||
|
||||
def remove_package(self, package: Package) -> None:
|
||||
"""Removes a package from the ATO."""
|
||||
self.packages.remove(package)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Removes all packages from the ATO."""
|
||||
self.packages.clear()
|
||||
@@ -1,30 +0,0 @@
|
||||
from game import db
|
||||
from .conflictgen import *
|
||||
from .naming import *
|
||||
|
||||
from dcs.mission import *
|
||||
from dcs.unitgroup import *
|
||||
from dcs.unittype import *
|
||||
from dcs.task import *
|
||||
from dcs.terrain.terrain import NoParkingSlotError
|
||||
|
||||
AWACS_DISTANCE = 150000
|
||||
AWACS_ALT = 10000
|
||||
|
||||
|
||||
class AWACSConflictGenerator:
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game):
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
|
||||
def generate(self):
|
||||
plane = db.find_unittype(AWACS, self.conflict.attackers_side.name)[0]
|
||||
|
||||
self.mission.awacs_flight(
|
||||
country=self.mission.country(self.game.player),
|
||||
name=namegen.next_awacs_group_name(),
|
||||
plane_type=plane,
|
||||
altitude=AWACS_ALT,
|
||||
airport=None,
|
||||
position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE))
|
||||
74
gen/beacons.py
Normal file
74
gen/beacons.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import auto, IntEnum
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from gen.radios import RadioFrequency
|
||||
from gen.tacan import TacanBand, TacanChannel
|
||||
|
||||
|
||||
BEACONS_RESOURCE_PATH = Path("resources/dcs/beacons")
|
||||
|
||||
|
||||
class BeaconType(IntEnum):
|
||||
BEACON_TYPE_NULL = auto()
|
||||
BEACON_TYPE_VOR = auto()
|
||||
BEACON_TYPE_DME = auto()
|
||||
BEACON_TYPE_VOR_DME = auto()
|
||||
BEACON_TYPE_TACAN = auto()
|
||||
BEACON_TYPE_VORTAC = auto()
|
||||
BEACON_TYPE_RSBN = auto()
|
||||
BEACON_TYPE_BROADCAST_STATION = auto()
|
||||
|
||||
BEACON_TYPE_HOMER = auto()
|
||||
BEACON_TYPE_AIRPORT_HOMER = auto()
|
||||
BEACON_TYPE_AIRPORT_HOMER_WITH_MARKER = auto()
|
||||
BEACON_TYPE_ILS_FAR_HOMER = auto()
|
||||
BEACON_TYPE_ILS_NEAR_HOMER = auto()
|
||||
|
||||
BEACON_TYPE_ILS_LOCALIZER = auto()
|
||||
BEACON_TYPE_ILS_GLIDESLOPE = auto()
|
||||
|
||||
BEACON_TYPE_PRMG_LOCALIZER = auto()
|
||||
BEACON_TYPE_PRMG_GLIDESLOPE = auto()
|
||||
|
||||
BEACON_TYPE_ICLS_LOCALIZER = auto()
|
||||
BEACON_TYPE_ICLS_GLIDESLOPE = auto()
|
||||
|
||||
BEACON_TYPE_NAUTICAL_HOMER = auto()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Beacon:
|
||||
name: str
|
||||
callsign: str
|
||||
beacon_type: BeaconType
|
||||
hertz: int
|
||||
channel: Optional[int]
|
||||
|
||||
@property
|
||||
def frequency(self) -> RadioFrequency:
|
||||
return RadioFrequency(self.hertz)
|
||||
|
||||
@property
|
||||
def is_tacan(self) -> bool:
|
||||
return self.beacon_type in (
|
||||
BeaconType.BEACON_TYPE_VORTAC,
|
||||
BeaconType.BEACON_TYPE_TACAN,
|
||||
)
|
||||
|
||||
@property
|
||||
def tacan_channel(self) -> TacanChannel:
|
||||
assert self.is_tacan
|
||||
assert self.channel is not None
|
||||
return TacanChannel(self.channel, TacanBand.X)
|
||||
|
||||
|
||||
def load_beacons_for_terrain(name: str) -> Iterable[Beacon]:
|
||||
beacons_file = BEACONS_RESOURCE_PATH / f"{name.lower()}.json"
|
||||
if not beacons_file.exists():
|
||||
raise RuntimeError(f"Beacon file {beacons_file.resolve()} is missing")
|
||||
|
||||
for beacon in json.loads(beacons_file.read_text()):
|
||||
yield Beacon(**beacon)
|
||||
164
gen/briefinggen.py
Normal file
164
gen/briefinggen.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
Briefing generation logic
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import random
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from theater.frontline import FrontLine
|
||||
from typing import List, Dict, TYPE_CHECKING
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
from dcs.mission import Mission
|
||||
from .aircraft import FlightData
|
||||
from .airsupportgen import AwacsInfo, TankerInfo
|
||||
from .armor import JtacInfo
|
||||
from theater import ControlPoint
|
||||
from .ground_forces.combat_stance import CombatStance
|
||||
from .radios import RadioFrequency
|
||||
from .runways import RunwayData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
@dataclass
|
||||
class CommInfo:
|
||||
"""Communications information for the kneeboard."""
|
||||
name: str
|
||||
freq: RadioFrequency
|
||||
|
||||
|
||||
class FrontLineInfo:
|
||||
def __init__(self, front_line: FrontLine):
|
||||
self.front_line: FrontLine = front_line
|
||||
self.player_base: ControlPoint = front_line.control_point_a
|
||||
self.enemy_base: ControlPoint = front_line.control_point_b
|
||||
self.player_zero: bool = self.player_base.base.total_armor == 0
|
||||
self.enemy_zero: bool = self.enemy_base.base.total_armor == 0
|
||||
self.advantage: bool = self.player_base.base.total_armor > self.enemy_base.base.total_armor
|
||||
self.stance: CombatStance = self.player_base.stances[self.enemy_base.id]
|
||||
self.combat_stances = CombatStance
|
||||
|
||||
class MissionInfoGenerator:
|
||||
"""Base type for generators of mission information for the player.
|
||||
|
||||
Examples of subtypes include briefing generators, kneeboard generators, etc.
|
||||
"""
|
||||
|
||||
def __init__(self, mission: Mission, game: Game) -> None:
|
||||
self.mission = mission
|
||||
self.game = game
|
||||
self.awacs: List[AwacsInfo] = []
|
||||
self.comms: List[CommInfo] = []
|
||||
self.flights: List[FlightData] = []
|
||||
self.jtacs: List[JtacInfo] = []
|
||||
self.tankers: List[TankerInfo] = []
|
||||
self.frontlines: List[FrontLineInfo] = []
|
||||
self.dynamic_runways: List[RunwayData] = []
|
||||
|
||||
def add_awacs(self, awacs: AwacsInfo) -> None:
|
||||
"""Adds an AWACS/GCI to the mission.
|
||||
|
||||
Args:
|
||||
awacs: AWACS information.
|
||||
"""
|
||||
self.awacs.append(awacs)
|
||||
|
||||
def add_comm(self, name: str, freq: RadioFrequency) -> None:
|
||||
"""Adds communications info to the mission.
|
||||
|
||||
Args:
|
||||
name: Name of the radio channel.
|
||||
freq: Frequency of the radio channel.
|
||||
"""
|
||||
self.comms.append(CommInfo(name, freq))
|
||||
|
||||
def add_flight(self, flight: FlightData) -> None:
|
||||
"""Adds flight info to the mission.
|
||||
|
||||
Args:
|
||||
flight: Flight information.
|
||||
"""
|
||||
self.flights.append(flight)
|
||||
|
||||
def add_jtac(self, jtac: JtacInfo) -> None:
|
||||
"""Adds a JTAC to the mission.
|
||||
|
||||
Args:
|
||||
jtac: JTAC information.
|
||||
"""
|
||||
self.jtacs.append(jtac)
|
||||
|
||||
def add_tanker(self, tanker: TankerInfo) -> None:
|
||||
"""Adds a tanker to the mission.
|
||||
|
||||
Args:
|
||||
tanker: Tanker information.
|
||||
"""
|
||||
self.tankers.append(tanker)
|
||||
|
||||
def add_frontline(self, frontline: FrontLineInfo) -> None:
|
||||
"""Adds a frontline to the briefing
|
||||
|
||||
Arguments:
|
||||
frontline: Frontline conflict information
|
||||
"""
|
||||
self.frontlines.append(frontline)
|
||||
|
||||
def add_dynamic_runway(self, runway: RunwayData) -> None:
|
||||
"""Adds a dynamically generated runway to the briefing.
|
||||
|
||||
Dynamic runways are any valid landing point that is a unit rather than a
|
||||
map feature. These include carriers, ships with a helipad, and FARPs.
|
||||
"""
|
||||
self.dynamic_runways.append(runway)
|
||||
|
||||
def generate(self) -> None:
|
||||
"""Generates the mission information."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BriefingGenerator(MissionInfoGenerator):
|
||||
|
||||
def __init__(self, mission: Mission, game: Game):
|
||||
super().__init__(mission, game)
|
||||
self.allied_flights_by_departure: Dict[str, List[FlightData]] = {}
|
||||
env = Environment(
|
||||
loader=FileSystemLoader("resources/briefing/templates"),
|
||||
autoescape=select_autoescape(
|
||||
disabled_extensions=("",),
|
||||
default_for_string=True,
|
||||
default=True,
|
||||
),
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
self.template = env.get_template("briefingtemplate_EN.j2")
|
||||
|
||||
def generate(self) -> None:
|
||||
"""Generate the mission briefing
|
||||
"""
|
||||
self._generate_frontline_info()
|
||||
self.generate_allied_flights_by_departure()
|
||||
self.mission.set_description_text(self.template.render(vars(self)))
|
||||
self.mission.add_picture_blue(os.path.abspath(
|
||||
"./resources/ui/splash_screen.png"))
|
||||
|
||||
def _generate_frontline_info(self) -> None:
|
||||
"""Build FrontLineInfo objects from FrontLine type and append to briefing.
|
||||
"""
|
||||
for front_line in self.game.theater.conflicts(from_player=True):
|
||||
self.add_frontline(FrontLineInfo(front_line))
|
||||
|
||||
# TODO: This should determine if runway is friendly through a method more robust than the existing string match
|
||||
def generate_allied_flights_by_departure(self) -> None:
|
||||
"""Create iterable to display allied flights grouped by departure airfield.
|
||||
"""
|
||||
for flight in self.flights:
|
||||
if not flight.client_units and flight.friendly:
|
||||
name = flight.departure.airfield_name
|
||||
if name in self.allied_flights_by_departure: # where else can we get this?
|
||||
self.allied_flights_by_departure[name].append(flight)
|
||||
else:
|
||||
self.allied_flights_by_departure[name] = [flight]
|
||||
34
gen/callsigns.py
Normal file
34
gen/callsigns.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Support for working with DCS group callsigns."""
|
||||
import logging
|
||||
import re
|
||||
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
from dcs.flyingunit import FlyingUnit
|
||||
|
||||
|
||||
def callsign_for_support_unit(group: FlyingGroup) -> str:
|
||||
# Either something like Overlord11 for Western AWACS, or else just a number.
|
||||
# Convert to either "Overlord" or "Flight 123".
|
||||
lead = group.units[0]
|
||||
raw_callsign = lead.callsign_as_str()
|
||||
try:
|
||||
return f"Flight {int(raw_callsign)}"
|
||||
except ValueError:
|
||||
return raw_callsign.rstrip("1234567890")
|
||||
|
||||
|
||||
def create_group_callsign_from_unit(lead: FlyingUnit) -> str:
|
||||
raw_callsign = lead.callsign_as_str()
|
||||
if not lead.callsign_is_western:
|
||||
# Callsigns for non-Western countries are just a number per flight,
|
||||
# similar to tail numbers.
|
||||
return f"Flight {raw_callsign}"
|
||||
|
||||
# Callsign from pydcs is in the format `<name><group ID><unit ID>`,
|
||||
# where unit ID is guaranteed to be a single digit but the group ID may
|
||||
# be more.
|
||||
match = re.search(r"^(\D+)(\d+)(\d)$", raw_callsign)
|
||||
if match is None:
|
||||
logging.error(f"Could not parse unit callsign: {raw_callsign}")
|
||||
return f"Flight {raw_callsign}"
|
||||
return f"{match.group(1)} {match.group(2)}"
|
||||
@@ -1,27 +1,28 @@
|
||||
import typing
|
||||
import pdb
|
||||
import dcs
|
||||
import logging
|
||||
import random
|
||||
from typing import Tuple
|
||||
|
||||
from random import randint
|
||||
from dcs import Mission
|
||||
from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
|
||||
from dcs.mission import *
|
||||
from dcs.vehicles import *
|
||||
from dcs.unitgroup import *
|
||||
from dcs.unittype import *
|
||||
from dcs.mapping import *
|
||||
from dcs.point import *
|
||||
from dcs.task import *
|
||||
from dcs.country import *
|
||||
|
||||
from theater import *
|
||||
from theater import ConflictTheater, ControlPoint
|
||||
|
||||
AIR_DISTANCE = 40000
|
||||
|
||||
CAPTURE_AIR_ATTACKERS_DISTANCE = 25000
|
||||
CAPTURE_AIR_DEFENDERS_DISTANCE = 60000
|
||||
STRIKE_AIR_ATTACKERS_DISTANCE = 45000
|
||||
STRIKE_AIR_DEFENDERS_DISTANCE = 25000
|
||||
|
||||
GROUND_DISTANCE_FACTOR = 1
|
||||
GROUNDINTERCEPT_DISTANCE_FACTOR = 6
|
||||
AIR_DISTANCE = 32000
|
||||
CAP_CAS_DISTANCE = 10000, 120000
|
||||
|
||||
GROUND_INTERCEPT_SPREAD = 5000
|
||||
GROUND_DISTANCE_FACTOR = 1.4
|
||||
GROUND_DISTANCE = 2000
|
||||
|
||||
GROUND_ATTACK_DISTANCE = 25000, 13000
|
||||
|
||||
TRANSPORT_FRONTLINE_DIST = 1800
|
||||
|
||||
INTERCEPT_ATTACKERS_HEADING = -45, 45
|
||||
INTERCEPT_DEFENDERS_HEADING = -10, 10
|
||||
@@ -30,10 +31,14 @@ INTERCEPT_ATTACKERS_DISTANCE = 100000
|
||||
INTERCEPT_MAX_DISTANCE = 160000
|
||||
INTERCEPT_MIN_DISTANCE = 100000
|
||||
|
||||
NAVAL_INTERCEPT_DISTANCE_FACTOR = 0.4
|
||||
NAVAL_INTERCEPT_DISTANCE_FACTOR = 1
|
||||
NAVAL_INTERCEPT_DISTANCE_MAX = 40000
|
||||
NAVAL_INTERCEPT_STEP = 5000
|
||||
|
||||
FRONTLINE_LENGTH = 80000
|
||||
FRONTLINE_MIN_CP_DISTANCE = 5000
|
||||
FRONTLINE_DISTANCE_STRENGTH_FACTOR = 0.7
|
||||
|
||||
|
||||
def _opposite_heading(h):
|
||||
return h+180
|
||||
@@ -50,36 +55,33 @@ def _heading_sum(h, a) -> int:
|
||||
|
||||
|
||||
class Conflict:
|
||||
attackers_side = None # type: Country
|
||||
defenders_side = None # type: Country
|
||||
from_cp = None # type: ControlPoint
|
||||
to_cp = None # type: ControlPoint
|
||||
position = None # type: Point
|
||||
size = None # type: int
|
||||
radials = None # type: typing.List[int]
|
||||
|
||||
ground_attackers_location = None # type: Point
|
||||
ground_defenders_location = None # type: Point
|
||||
air_attackers_location = None # type: Point
|
||||
air_defenders_location = None # type: Point
|
||||
|
||||
def __init__(self,
|
||||
position: Point,
|
||||
theater: ConflictTheater,
|
||||
from_cp: ControlPoint,
|
||||
to_cp: ControlPoint,
|
||||
attackers_side: Country,
|
||||
defenders_side: Country,
|
||||
ground_attackers_location: Point,
|
||||
ground_defenders_location: Point,
|
||||
air_attackers_location: Point,
|
||||
air_defenders_location: Point):
|
||||
attackers_side: str,
|
||||
defenders_side: str,
|
||||
attackers_country: Country,
|
||||
defenders_country: Country,
|
||||
position: Point,
|
||||
heading=None,
|
||||
distance=None,
|
||||
ground_attackers_location: Point = None,
|
||||
ground_defenders_location: Point = None,
|
||||
air_attackers_location: Point = None,
|
||||
air_defenders_location: Point = None):
|
||||
|
||||
self.attackers_side = attackers_side
|
||||
self.defenders_side = defenders_side
|
||||
self.attackers_country = attackers_country
|
||||
self.defenders_country = defenders_country
|
||||
|
||||
self.from_cp = from_cp
|
||||
self.to_cp = to_cp
|
||||
self.theater = theater
|
||||
self.position = position
|
||||
self.heading = heading
|
||||
self.distance = distance
|
||||
self.size = to_cp.size
|
||||
self.radials = to_cp.radials
|
||||
self.ground_attackers_location = ground_attackers_location
|
||||
@@ -87,41 +89,181 @@ class Conflict:
|
||||
self.air_attackers_location = air_attackers_location
|
||||
self.air_defenders_location = air_defenders_location
|
||||
|
||||
@property
|
||||
def center(self) -> Point:
|
||||
return self.position.point_from_heading(self.heading, self.distance / 2)
|
||||
|
||||
@property
|
||||
def tail(self) -> Point:
|
||||
return self.position.point_from_heading(self.heading, self.distance)
|
||||
|
||||
@property
|
||||
def is_vector(self) -> bool:
|
||||
return self.heading is not None
|
||||
|
||||
@property
|
||||
def opposite_heading(self) -> int:
|
||||
return _heading_sum(self.heading, 180)
|
||||
|
||||
@property
|
||||
def to_size(self):
|
||||
return self.to_cp.size * GROUND_DISTANCE_FACTOR
|
||||
|
||||
def find_insertion_point(self, other_point: Point) -> Point:
|
||||
if self.is_vector:
|
||||
dx = self.position.x - self.tail.x
|
||||
dy = self.position.y - self.tail.y
|
||||
dr2 = float(dx ** 2 + dy ** 2)
|
||||
|
||||
lerp = ((other_point.x - self.tail.x) * dx + (other_point.y - self.tail.y) * dy) / dr2
|
||||
if lerp < 0:
|
||||
lerp = 0
|
||||
elif lerp > 1:
|
||||
lerp = 1
|
||||
|
||||
x = lerp * dx + self.tail.x
|
||||
y = lerp * dy + self.tail.y
|
||||
return Point(x, y)
|
||||
else:
|
||||
return self.position
|
||||
|
||||
def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> Point:
|
||||
return Conflict._find_ground_position(at, max_distance, heading, self.theater)
|
||||
|
||||
@classmethod
|
||||
def _find_ground_location(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
|
||||
for _ in range(0, int(max_distance), 800):
|
||||
for _ in range(3):
|
||||
if theater.is_on_land(initial):
|
||||
return initial
|
||||
def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool:
|
||||
return from_cp.has_frontline and to_cp.has_frontline
|
||||
|
||||
initial = initial.random_point_within(1000, 1000)
|
||||
@classmethod
|
||||
def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]:
|
||||
attack_heading = from_cp.position.heading_between_point(to_cp.position)
|
||||
attack_distance = from_cp.position.distance_to_point(to_cp.position)
|
||||
middle_point = from_cp.position.point_from_heading(attack_heading, attack_distance / 2)
|
||||
|
||||
initial = initial.point_from_heading(heading, 800)
|
||||
strength_delta = (from_cp.base.strength - to_cp.base.strength) / 1.0
|
||||
position = middle_point.point_from_heading(attack_heading, strength_delta * attack_distance / 2 - FRONTLINE_MIN_CP_DISTANCE)
|
||||
return position, _opposite_heading(attack_heading)
|
||||
|
||||
@classmethod
|
||||
def flight_frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:
|
||||
"""Returns the frontline vector without regard for exclusion zones, used in CAS flight plan"""
|
||||
frontline = cls.frontline_position(theater, from_cp, to_cp)
|
||||
center_position, heading = frontline
|
||||
left_position = center_position.point_from_heading(_heading_sum(heading, -90), int(FRONTLINE_LENGTH/2))
|
||||
right_position = center_position.point_from_heading(_heading_sum(heading, 90), int(FRONTLINE_LENGTH/2))
|
||||
|
||||
return left_position, _heading_sum(heading, 90), int(right_position.distance_to_point(left_position))
|
||||
|
||||
|
||||
@classmethod
|
||||
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:
|
||||
"""
|
||||
probe_end_point = initial.point_from_heading(heading, FRONTLINE_LENGTH)
|
||||
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ])
|
||||
intersection = probe.intersection(theater.land_poly)
|
||||
|
||||
if isinstance(intersection, geometry.LineString):
|
||||
intersection = intersection
|
||||
elif isinstance(intersection, geometry.MultiLineString):
|
||||
intersection = intersection.geoms[0]
|
||||
else:
|
||||
print(intersection)
|
||||
return None
|
||||
|
||||
return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length
|
||||
"""
|
||||
frontline = cls.frontline_position(theater, from_cp, to_cp)
|
||||
center_position, heading = frontline
|
||||
left_position, right_position = None, None
|
||||
|
||||
if not theater.is_on_land(center_position):
|
||||
pos = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, -90), theater)
|
||||
if pos:
|
||||
right_position = pos
|
||||
center_position = pos
|
||||
else:
|
||||
pos = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, +90), theater)
|
||||
if pos:
|
||||
left_position = pos
|
||||
center_position = pos
|
||||
|
||||
if left_position is None:
|
||||
left_position = cls._extend_ground_position(center_position, int(FRONTLINE_LENGTH/2), _heading_sum(heading, -90), theater)
|
||||
|
||||
if right_position is None:
|
||||
right_position = cls._extend_ground_position(center_position, int(FRONTLINE_LENGTH/2), _heading_sum(heading, 90), theater)
|
||||
|
||||
return left_position, _heading_sum(heading, 90), int(right_position.distance_to_point(left_position))
|
||||
|
||||
@classmethod
|
||||
def _extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
|
||||
pos = initial
|
||||
for offset in range(0, int(max_distance), 500):
|
||||
new_pos = initial.point_from_heading(heading, offset)
|
||||
if theater.is_on_land(new_pos):
|
||||
pos = new_pos
|
||||
else:
|
||||
return pos
|
||||
return pos
|
||||
|
||||
"""
|
||||
probe_end_point = initial.point_from_heading(heading, max_distance)
|
||||
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y)])
|
||||
|
||||
intersection = probe.intersection(theater.land_poly)
|
||||
if intersection is geometry.LineString:
|
||||
return Point(*intersection.xy[1])
|
||||
elif intersection is geometry.MultiLineString:
|
||||
return Point(*intersection.geoms[0].xy[1])
|
||||
|
||||
print("Didn't find ground position!")
|
||||
return None
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def capture_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
|
||||
pos = initial
|
||||
for _ in range(0, int(max_distance), 500):
|
||||
if theater.is_on_land(pos):
|
||||
return pos
|
||||
|
||||
pos = pos.point_from_heading(heading, 500)
|
||||
"""
|
||||
probe_end_point = initial.point_from_heading(heading, max_distance)
|
||||
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ])
|
||||
|
||||
intersection = probe.intersection(theater.land_poly)
|
||||
if isinstance(intersection, geometry.LineString):
|
||||
return Point(*intersection.xy[1])
|
||||
elif isinstance(intersection, geometry.MultiLineString):
|
||||
return Point(*intersection.geoms[0].xy[1])
|
||||
"""
|
||||
|
||||
logging.error("Didn't find ground position ({})!".format(initial))
|
||||
return initial
|
||||
|
||||
@classmethod
|
||||
def capture_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
position = to_cp.position
|
||||
attack_raw_heading = to_cp.position.heading_between_point(from_cp.position)
|
||||
attack_heading = to_cp.find_radial(attack_raw_heading)
|
||||
defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading)
|
||||
|
||||
distance = to_cp.size * GROUND_DISTANCE_FACTOR
|
||||
distance = GROUND_DISTANCE
|
||||
attackers_location = position.point_from_heading(attack_heading, distance)
|
||||
attackers_location = Conflict._find_ground_location(attackers_location, distance * 2, _heading_sum(attack_heading, 180), theater)
|
||||
attackers_location = Conflict._find_ground_position(attackers_location, distance * 2, attack_heading, theater)
|
||||
|
||||
defenders_location = position.point_from_heading(defense_heading, distance)
|
||||
defenders_location = Conflict._find_ground_location(defenders_location, distance * 2, _heading_sum(defense_heading, 180), theater)
|
||||
defenders_location = position.point_from_heading(defense_heading, 0)
|
||||
defenders_location = Conflict._find_ground_position(defenders_location, distance * 2, defense_heading, theater)
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker,
|
||||
defenders_side=defender,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=attackers_location,
|
||||
ground_defenders_location=defenders_location,
|
||||
air_attackers_location=position.point_from_heading(attack_raw_heading, CAPTURE_AIR_ATTACKERS_DISTANCE),
|
||||
@@ -129,20 +271,57 @@ class Conflict:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def intercept_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
def strike_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
position = to_cp.position
|
||||
attack_raw_heading = to_cp.position.heading_between_point(from_cp.position)
|
||||
attack_heading = to_cp.find_radial(attack_raw_heading)
|
||||
defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading)
|
||||
|
||||
distance = to_cp.size * GROUND_DISTANCE_FACTOR
|
||||
attackers_location = position.point_from_heading(attack_heading, distance)
|
||||
attackers_location = Conflict._find_ground_position(
|
||||
attackers_location, int(distance * 2),
|
||||
_heading_sum(attack_heading, 180), theater)
|
||||
|
||||
defenders_location = position.point_from_heading(defense_heading, distance)
|
||||
defenders_location = Conflict._find_ground_position(
|
||||
defenders_location, int(distance * 2),
|
||||
_heading_sum(defense_heading, 180), theater)
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=attackers_location,
|
||||
ground_defenders_location=defenders_location,
|
||||
air_attackers_location=position.point_from_heading(attack_raw_heading, STRIKE_AIR_ATTACKERS_DISTANCE),
|
||||
air_defenders_location=position.point_from_heading(_opposite_heading(attack_raw_heading), STRIKE_AIR_DEFENDERS_DISTANCE)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def intercept_position(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> Point:
|
||||
raw_distance = from_cp.position.distance_to_point(to_cp.position) * 1.5
|
||||
distance = max(min(raw_distance, INTERCEPT_MAX_DISTANCE), INTERCEPT_MIN_DISTANCE)
|
||||
|
||||
heading = _heading_sum(from_cp.position.heading_between_point(to_cp.position), random.choice([-1, 1]) * random.randint(60, 100))
|
||||
position = from_cp.position.point_from_heading(heading, distance)
|
||||
return from_cp.position.point_from_heading(heading, distance)
|
||||
|
||||
@classmethod
|
||||
def intercept_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, position: Point, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
heading = from_cp.position.heading_between_point(position)
|
||||
return cls(
|
||||
position=position.point_from_heading(position.heading_between_point(to_cp.position), INTERCEPT_CONFLICT_DISTANCE),
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker,
|
||||
defenders_side=defender,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=None,
|
||||
ground_defenders_location=None,
|
||||
air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, INTERCEPT_ATTACKERS_DISTANCE),
|
||||
@@ -150,10 +329,10 @@ class Conflict:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def ground_attack_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
def ground_attack_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
heading = random.choice(to_cp.radials)
|
||||
initial_location = to_cp.position.random_point_within(*GROUND_ATTACK_DISTANCE)
|
||||
position = Conflict._find_ground_location(initial_location, GROUND_INTERCEPT_SPREAD, _heading_sum(heading, 180), theater)
|
||||
position = Conflict._find_ground_position(initial_location, GROUND_INTERCEPT_SPREAD, _heading_sum(heading, 180), theater)
|
||||
if not position:
|
||||
heading = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
|
||||
position = to_cp.position.point_from_heading(heading, to_cp.size * GROUND_DISTANCE_FACTOR)
|
||||
@@ -163,8 +342,10 @@ class Conflict:
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker,
|
||||
defenders_side=defender,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=position,
|
||||
ground_defenders_location=None,
|
||||
air_attackers_location=None,
|
||||
@@ -172,42 +353,103 @@ class Conflict:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def ground_intercept_conflict(cls, attacker: Country, defender: Country, heading: int, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
heading = random.choice(to_cp.radials)
|
||||
initial_location = to_cp.position.point_from_heading(heading, to_cp.size * GROUNDINTERCEPT_DISTANCE_FACTOR)
|
||||
max_distance = to_cp.size * GROUNDINTERCEPT_DISTANCE_FACTOR
|
||||
ground_location = Conflict._find_ground_location(initial_location, max_distance, _heading_sum(heading, 180), theater)
|
||||
def convoy_strike_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
frontline_position, frontline_heading, frontline_length = Conflict.frontline_vector(from_cp, to_cp, theater)
|
||||
if not frontline_position:
|
||||
assert False
|
||||
|
||||
heading = frontline_heading
|
||||
starting_position = Conflict._find_ground_position(frontline_position.point_from_heading(heading, 7000),
|
||||
GROUND_INTERCEPT_SPREAD,
|
||||
_opposite_heading(heading), theater)
|
||||
if not starting_position:
|
||||
starting_position = frontline_position
|
||||
destination_position = frontline_position
|
||||
else:
|
||||
destination_position = frontline_position
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
position=destination_position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker,
|
||||
defenders_side=defender,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=None,
|
||||
ground_defenders_location=position,
|
||||
air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, AIR_DISTANCE),
|
||||
air_defenders_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + _opposite_heading(heading), AIR_DISTANCE)
|
||||
ground_defenders_location=starting_position,
|
||||
air_attackers_location=starting_position.point_from_heading(_opposite_heading(heading), AIR_DISTANCE),
|
||||
air_defenders_location=starting_position.point_from_heading(heading, AIR_DISTANCE),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def ground_base_attack(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
def frontline_cas_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
assert cls.has_frontline_between(from_cp, to_cp)
|
||||
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
heading=heading,
|
||||
distance=distance,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=None,
|
||||
ground_defenders_location=None,
|
||||
air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, AIR_DISTANCE),
|
||||
air_defenders_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + _opposite_heading(heading), AIR_DISTANCE),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def frontline_cap_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
assert cls.has_frontline_between(from_cp, to_cp)
|
||||
|
||||
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
|
||||
attack_position = position.point_from_heading(heading, random.randint(0, int(distance)))
|
||||
attackers_position = attack_position.point_from_heading(heading - 90, AIR_DISTANCE)
|
||||
defenders_position = attack_position.point_from_heading(heading + 90, random.randint(*CAP_CAS_DISTANCE))
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
heading=heading,
|
||||
distance=distance,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
air_attackers_location=attackers_position,
|
||||
air_defenders_location=defenders_position,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def ground_base_attack(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
position = to_cp.position
|
||||
attack_heading = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
|
||||
defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading)
|
||||
|
||||
distance = to_cp.size * GROUND_DISTANCE_FACTOR
|
||||
defenders_location = position.point_from_heading(defense_heading, distance)
|
||||
defenders_location = Conflict._find_ground_location(defenders_location, distance * 2, _heading_sum(defense_heading, 180), theater)
|
||||
defenders_location = Conflict._find_ground_position(
|
||||
defenders_location, int(distance * 2),
|
||||
_heading_sum(defense_heading, 180), theater)
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker,
|
||||
defenders_side=defender,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=None,
|
||||
ground_defenders_location=defenders_location,
|
||||
air_attackers_location=position.point_from_heading(attack_heading, AIR_DISTANCE),
|
||||
@@ -215,25 +457,30 @@ class Conflict:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def naval_intercept_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
def naval_intercept_position(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
radial = random.choice(to_cp.sea_radials)
|
||||
|
||||
initial_distance = min(int(from_cp.position.distance_to_point(to_cp.position) * NAVAL_INTERCEPT_DISTANCE_FACTOR), NAVAL_INTERCEPT_DISTANCE_MAX)
|
||||
position = to_cp.position.point_from_heading(radial, initial_distance)
|
||||
initial_position = to_cp.position.point_from_heading(radial, initial_distance)
|
||||
for offset in range(0, initial_distance, NAVAL_INTERCEPT_STEP):
|
||||
position = to_cp.position.point_from_heading(_opposite_heading(radial), initial_distance - offset)
|
||||
position = initial_position.point_from_heading(_opposite_heading(radial), offset)
|
||||
|
||||
if not theater.is_on_land(position):
|
||||
break
|
||||
return position
|
||||
|
||||
@classmethod
|
||||
def naval_intercept_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, position: Point, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
attacker_heading = from_cp.position.heading_between_point(to_cp.position)
|
||||
return cls(
|
||||
position=position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker,
|
||||
defenders_side=defender,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=None,
|
||||
ground_defenders_location=position,
|
||||
air_attackers_location=position.point_from_heading(attacker_heading, AIR_DISTANCE),
|
||||
@@ -241,11 +488,10 @@ class Conflict:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def transport_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
frontline_position = cls._frontline_position(from_cp, to_cp)
|
||||
heading = to_cp.position.heading_between_point(from_cp.position)
|
||||
def transport_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
frontline_position, heading = cls.frontline_position(theater, from_cp, to_cp)
|
||||
initial_dest = frontline_position.point_from_heading(heading, TRANSPORT_FRONTLINE_DIST)
|
||||
dest = cls._find_ground_location(initial_dest, from_cp.position.distance_to_point(to_cp.position) / 3, heading, theater)
|
||||
dest = cls._find_ground_position(initial_dest, from_cp.position.distance_to_point(to_cp.position) / 3, heading, theater)
|
||||
if not dest:
|
||||
radial = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
|
||||
dest = to_cp.position.point_from_heading(radial, to_cp.size * GROUND_DISTANCE_FACTOR)
|
||||
@@ -255,8 +501,10 @@ class Conflict:
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker,
|
||||
defenders_side=defender,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=from_cp.position,
|
||||
ground_defenders_location=frontline_position,
|
||||
air_attackers_location=from_cp.position.point_from_heading(0, 100),
|
||||
|
||||
39
gen/defenses/armor_group_generator.py
Normal file
39
gen/defenses/armor_group_generator.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import random
|
||||
|
||||
from dcs.vehicles import Armor
|
||||
|
||||
from game import db
|
||||
from gen.defenses.armored_group_generator import ArmoredGroupGenerator, FixedSizeArmorGroupGenerator
|
||||
|
||||
|
||||
def generate_armor_group(faction:str, game, ground_object):
|
||||
"""
|
||||
This generate a group of ground units
|
||||
:return: Generated group
|
||||
"""
|
||||
possible_unit = [u for u in db.FACTIONS[faction].frontline_units if u in Armor.__dict__.values()]
|
||||
if len(possible_unit) > 0:
|
||||
unit_type = random.choice(possible_unit)
|
||||
return generate_armor_group_of_type(game, ground_object, unit_type)
|
||||
return None
|
||||
|
||||
|
||||
def generate_armor_group_of_type(game, ground_object, unit_type):
|
||||
"""
|
||||
This generate a group of ground units of given type
|
||||
:return: Generated group
|
||||
"""
|
||||
generator = ArmoredGroupGenerator(game, ground_object, unit_type)
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
|
||||
|
||||
def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size: int):
|
||||
"""
|
||||
This generate a group of ground units of given type and size
|
||||
:return: Generated group
|
||||
"""
|
||||
generator = FixedSizeArmorGroupGenerator(game, ground_object, unit_type, size)
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
|
||||
44
gen/defenses/armored_group_generator.py
Normal file
44
gen/defenses/armored_group_generator.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import random
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class ArmoredGroupGenerator(GroupGenerator):
|
||||
|
||||
def __init__(self, game, ground_object, unit_type):
|
||||
super(ArmoredGroupGenerator, self).__init__(game, ground_object)
|
||||
self.unit_type = unit_type
|
||||
|
||||
def generate(self):
|
||||
|
||||
grid_x = random.randint(2, 3)
|
||||
grid_y = random.randint(1, 2)
|
||||
|
||||
spacing = random.randint(30, 80)
|
||||
|
||||
index = 0
|
||||
for i in range(grid_x):
|
||||
for j in range(grid_y):
|
||||
index = index + 1
|
||||
self.add_unit(self.unit_type, "Armor#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y + spacing * j, self.heading)
|
||||
|
||||
|
||||
class FixedSizeArmorGroupGenerator(GroupGenerator):
|
||||
|
||||
def __init__(self, game, ground_object, unit_type, size):
|
||||
super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object)
|
||||
self.unit_type = unit_type
|
||||
self.size = size
|
||||
|
||||
def generate(self):
|
||||
spacing = random.randint(20, 70)
|
||||
|
||||
index = 0
|
||||
for i in range(self.size):
|
||||
index = index + 1
|
||||
self.add_unit(self.unit_type, "Armor#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y, self.heading)
|
||||
|
||||
@@ -1,107 +1,36 @@
|
||||
import typing
|
||||
import random
|
||||
from datetime import datetime, timedelta, time
|
||||
from typing import Optional
|
||||
|
||||
from dcs.mission import Mission
|
||||
from dcs.triggers import *
|
||||
from dcs.condition import *
|
||||
from dcs.action import *
|
||||
from dcs.unit import Skill
|
||||
from dcs.point import MovingPoint, PointProperties
|
||||
from dcs.action import *
|
||||
from dcs.weather import *
|
||||
|
||||
from game import db
|
||||
from theater import *
|
||||
from gen import *
|
||||
|
||||
WEATHER_CLOUD_BASE = 2000, 3000
|
||||
WEATHER_CLOUD_DENSITY = 1, 8
|
||||
WEATHER_CLOUD_THICKNESS = 100, 400
|
||||
WEATHER_CLOUD_BASE_MIN = 1200
|
||||
|
||||
RANDOM_TIME = {
|
||||
"night": 5,
|
||||
"dusk": 30,
|
||||
"dawn": 30,
|
||||
"day": 100,
|
||||
}
|
||||
|
||||
RANDOM_WEATHER = {
|
||||
1: 5, # heavy rain
|
||||
2: 15, # rain
|
||||
3: 25, # dynamic
|
||||
4: 35, # clear
|
||||
5: 100, # random
|
||||
}
|
||||
from game.weather import Clouds, Fog, Conditions, WindConditions
|
||||
|
||||
|
||||
class EnvironmentSettings:
|
||||
weather_dict = None
|
||||
start_time = None
|
||||
|
||||
|
||||
class EnviromentGenerator:
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game):
|
||||
class EnvironmentGenerator:
|
||||
def __init__(self, mission: Mission, conditions: Conditions) -> None:
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
self.conditions = conditions
|
||||
|
||||
def _gen_random_time(self):
|
||||
start_time = datetime.combine(datetime.today(), time())
|
||||
time_range = None
|
||||
for k, v in RANDOM_TIME.items():
|
||||
if self.game.settings.night_disabled and k == "night":
|
||||
continue
|
||||
def set_clouds(self, clouds: Optional[Clouds]) -> None:
|
||||
if clouds is None:
|
||||
return
|
||||
self.mission.weather.clouds_base = clouds.base
|
||||
self.mission.weather.clouds_thickness = clouds.thickness
|
||||
self.mission.weather.clouds_density = clouds.density
|
||||
self.mission.weather.clouds_iprecptns = clouds.precipitation
|
||||
|
||||
if random.randint(0, 100) <= v:
|
||||
time_range = self.game.theater.daytime_map[k]
|
||||
break
|
||||
def set_fog(self, fog: Optional[Fog]) -> None:
|
||||
if fog is None:
|
||||
return
|
||||
self.mission.weather.fog_visibility = fog.visibility
|
||||
self.mission.weather.fog_thickness = fog.thickness
|
||||
|
||||
start_time += timedelta(hours=random.randint(*time_range))
|
||||
self.mission.start_time = start_time
|
||||
|
||||
def _gen_random_weather(self):
|
||||
weather_type = None
|
||||
for k, v in RANDOM_WEATHER.items():
|
||||
if random.randint(0, 100) <= v:
|
||||
weather_type = k
|
||||
break
|
||||
|
||||
print("generated weather {}".format(weather_type))
|
||||
if weather_type == 1:
|
||||
self.mission.weather.heavy_rain()
|
||||
elif weather_type == 2:
|
||||
self.mission.weather.heavy_rain()
|
||||
self.mission.weather.enable_fog = False
|
||||
elif weather_type == 3:
|
||||
self.mission.weather.random(self.mission.start_time, self.conflict.theater.terrain)
|
||||
elif weather_type == 4:
|
||||
pass
|
||||
elif weather_type == 5:
|
||||
self.mission.weather.clouds_base = random.randint(*WEATHER_CLOUD_BASE)
|
||||
self.mission.weather.clouds_density = random.randint(*WEATHER_CLOUD_DENSITY)
|
||||
self.mission.weather.clouds_thickness = random.randint(*WEATHER_CLOUD_THICKNESS)
|
||||
|
||||
wind_direction = random.randint(0, 360)
|
||||
wind_speed = random.randint(0, 13)
|
||||
self.mission.weather.wind_at_ground = Wind(wind_direction, wind_speed)
|
||||
self.mission.weather.wind_at_2000 = Wind(wind_direction, wind_speed * 2)
|
||||
self.mission.weather.wind_at_8000 = Wind(wind_direction, wind_speed * 3)
|
||||
|
||||
if self.mission.weather.clouds_density > 0:
|
||||
self.mission.weather.clouds_base = max(self.mission.weather.clouds_base, WEATHER_CLOUD_BASE_MIN)
|
||||
|
||||
def generate(self) -> EnvironmentSettings:
|
||||
self._gen_random_time()
|
||||
self._gen_random_weather()
|
||||
|
||||
settings = EnvironmentSettings()
|
||||
settings.start_time = self.mission.start_time
|
||||
settings.weather_dict = self.mission.weather.dict()
|
||||
return settings
|
||||
|
||||
def load(self, settings: EnvironmentSettings):
|
||||
self.mission.start_time = settings.start_time
|
||||
self.mission.weather.load_from_dict(settings.weather_dict)
|
||||
def set_wind(self, wind: WindConditions) -> None:
|
||||
self.mission.weather.wind_at_ground = wind.at_0m
|
||||
self.mission.weather.wind_at_2000 = wind.at_2000m
|
||||
self.mission.weather.wind_at_8000 = wind.at_8000m
|
||||
|
||||
def generate(self):
|
||||
self.mission.start_time = self.conditions.start_time
|
||||
self.set_clouds(self.conditions.weather.clouds)
|
||||
self.set_fog(self.conditions.weather.fog)
|
||||
self.set_wind(self.conditions.weather.wind)
|
||||
|
||||
26
gen/fleet/carrier_group.py
Normal file
26
gen/fleet/carrier_group.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import random
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
|
||||
class CarrierGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
# Add carrier
|
||||
if len(self.faction.aircraft_carrier) > 0:
|
||||
carrier_type = random.choice(self.faction.aircraft_carrier)
|
||||
self.add_unit(carrier_type, "Carrier", self.position.x, self.position.y, self.heading)
|
||||
else:
|
||||
return
|
||||
|
||||
# Add destroyers escort
|
||||
if len(self.faction.destroyers) > 0:
|
||||
dd_type = random.choice(self.faction.destroyers)
|
||||
self.add_unit(dd_type, "DD1", self.position.x + 2500, self.position.y + 4500, self.heading)
|
||||
self.add_unit(dd_type, "DD2", self.position.x + 2500, self.position.y - 4500, self.heading)
|
||||
|
||||
self.add_unit(dd_type, "DD3", self.position.x + 4500, self.position.y + 8500, self.heading)
|
||||
self.add_unit(dd_type, "DD4", self.position.x + 4500, self.position.y - 8500, self.heading)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
53
gen/fleet/cn_dd_group.py
Normal file
53
gen/fleet/cn_dd_group.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
from dcs.ships import (
|
||||
Type_052C_Destroyer,
|
||||
Type_052B_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
)
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from gen.fleet.dd_group import DDGroupGenerator
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
|
||||
class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
include_frigate = random.choice([True, True, False])
|
||||
include_dd = random.choice([True, False])
|
||||
|
||||
if include_dd:
|
||||
include_cc = random.choice([True, False])
|
||||
else:
|
||||
include_cc = False
|
||||
|
||||
if include_frigate:
|
||||
self.add_unit(Type_054A_Frigate, "FF1", self.position.x + 1200, self.position.y + 900, self.heading)
|
||||
self.add_unit(Type_054A_Frigate, "FF2", self.position.x + 1200, self.position.y - 900, self.heading)
|
||||
|
||||
if include_dd:
|
||||
dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer])
|
||||
self.add_unit(dd_type, "FF1", self.position.x + 2400, self.position.y + 900, self.heading)
|
||||
self.add_unit(dd_type, "FF2", self.position.x + 2400, self.position.y - 900, self.heading)
|
||||
|
||||
if include_cc:
|
||||
cc_type = random.choice([CGN_1144_2_Pyotr_Velikiy])
|
||||
self.add_unit(cc_type, "CC1", self.position.x, self.position.y, self.heading)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
|
||||
|
||||
class Type54GroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(Type54GroupGenerator, self).__init__(game, ground_object, faction, Type_054A_Frigate)
|
||||
34
gen/fleet/dd_group.py
Normal file
34
gen/fleet/dd_group.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from dcs.unittype import ShipType
|
||||
from dcs.ships import Oliver_Hazzard_Perry_class, USS_Arleigh_Burke_IIa
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
|
||||
class DDGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction, ddtype: ShipType):
|
||||
super(DDGroupGenerator, self).__init__(game, ground_object, faction)
|
||||
self.ddtype = ddtype
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(self.ddtype, "DD1", self.position.x + 500, self.position.y + 900, self.heading)
|
||||
self.add_unit(self.ddtype, "DD2", self.position.x + 500, self.position.y - 900, self.heading)
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
|
||||
|
||||
class OliverHazardPerryGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(OliverHazardPerryGroupGenerator, self).__init__(game, ground_object, faction, Oliver_Hazzard_Perry_class)
|
||||
|
||||
|
||||
class ArleighBurkeGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(ArleighBurkeGroupGenerator, self).__init__(game, ground_object, faction, USS_Arleigh_Burke_IIa)
|
||||
21
gen/fleet/lha_group.py
Normal file
21
gen/fleet/lha_group.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import random
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
|
||||
class LHAGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
# Add carrier
|
||||
if len(self.faction.helicopter_carrier) > 0:
|
||||
carrier_type = random.choice(self.faction.helicopter_carrier)
|
||||
self.add_unit(carrier_type, "LHA", self.position.x, self.position.y, self.heading)
|
||||
|
||||
# Add destroyers escort
|
||||
if len(self.faction.destroyers) > 0:
|
||||
dd_type = random.choice(self.faction.destroyers)
|
||||
self.add_unit(dd_type, "DD1", self.position.x + 1250, self.position.y + 1450, self.heading)
|
||||
self.add_unit(dd_type, "DD2", self.position.x + 1250, self.position.y - 1450, self.heading)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
72
gen/fleet/ru_dd_group.py
Normal file
72
gen/fleet/ru_dd_group.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from __future__ import annotations
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs.ships import (
|
||||
FFL_1124_4_Grisha,
|
||||
FSG_1241_1MP_Molniya,
|
||||
FFG_11540_Neustrashimy,
|
||||
FF_1135M_Rezky,
|
||||
CG_1164_Moskva,
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
SSK_877,
|
||||
SSK_641B
|
||||
)
|
||||
|
||||
from gen.fleet.dd_group import DDGroupGenerator
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from game.factions.faction import Faction
|
||||
from theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
|
||||
class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
include_frigate = random.choice([True, True, False])
|
||||
include_dd = random.choice([True, False])
|
||||
|
||||
if include_dd:
|
||||
include_cc = random.choice([True, False])
|
||||
else:
|
||||
include_cc = False
|
||||
|
||||
if include_frigate:
|
||||
frigate_type = random.choice([FFL_1124_4_Grisha, FSG_1241_1MP_Molniya])
|
||||
self.add_unit(frigate_type, "FF1", self.position.x + 1200, self.position.y + 900, self.heading)
|
||||
self.add_unit(frigate_type, "FF2", self.position.x + 1200, self.position.y - 900, self.heading)
|
||||
|
||||
if include_dd:
|
||||
dd_type = random.choice([FFG_11540_Neustrashimy, FF_1135M_Rezky])
|
||||
self.add_unit(dd_type, "FF1", self.position.x + 2400, self.position.y + 900, self.heading)
|
||||
self.add_unit(dd_type, "FF2", self.position.x + 2400, self.position.y - 900, self.heading)
|
||||
|
||||
if include_cc:
|
||||
cc_type = random.choice([CG_1164_Moskva, CGN_1144_2_Pyotr_Velikiy])
|
||||
self.add_unit(cc_type, "CC1", self.position.x, self.position.y, self.heading)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
|
||||
|
||||
class GrishaGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(GrishaGroupGenerator, self).__init__(game, ground_object, faction, FFL_1124_4_Grisha)
|
||||
|
||||
|
||||
class MolniyaGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(MolniyaGroupGenerator, self).__init__(game, ground_object, faction, FSG_1241_1MP_Molniya)
|
||||
|
||||
|
||||
class KiloSubGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_877)
|
||||
|
||||
|
||||
class TangoSubGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_641B)
|
||||
15
gen/fleet/schnellboot.py
Normal file
15
gen/fleet/schnellboot.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import random
|
||||
|
||||
from dcs.ships import Schnellboot_type_S130
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
|
||||
class SchnellbootGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
for i in range(random.randint(2, 4)):
|
||||
self.add_unit(Schnellboot_type_S130, "Schnellboot" + str(i), self.position.x + i * random.randint(100, 250), self.position.y + (random.randint(100, 200)-100), self.heading)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
72
gen/fleet/ship_group_generator.py
Normal file
72
gen/fleet/ship_group_generator.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import logging
|
||||
import random
|
||||
|
||||
from game import db
|
||||
from gen.fleet.carrier_group import CarrierGroupGenerator
|
||||
from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator
|
||||
from gen.fleet.dd_group import ArleighBurkeGroupGenerator, OliverHazardPerryGroupGenerator
|
||||
from gen.fleet.lha_group import LHAGroupGenerator
|
||||
from gen.fleet.ru_dd_group import RussianNavyGroupGenerator, GrishaGroupGenerator, MolniyaGroupGenerator, \
|
||||
KiloSubGroupGenerator, TangoSubGroupGenerator
|
||||
from gen.fleet.schnellboot import SchnellbootGroupGenerator
|
||||
from gen.fleet.uboat import UBoatGroupGenerator
|
||||
from gen.fleet.ww2lst import WW2LSTGroupGenerator
|
||||
|
||||
|
||||
SHIP_MAP = {
|
||||
"SchnellbootGroupGenerator": SchnellbootGroupGenerator,
|
||||
"WW2LSTGroupGenerator": WW2LSTGroupGenerator,
|
||||
"UBoatGroupGenerator": UBoatGroupGenerator,
|
||||
"OliverHazardPerryGroupGenerator": OliverHazardPerryGroupGenerator,
|
||||
"ArleighBurkeGroupGenerator": ArleighBurkeGroupGenerator,
|
||||
"RussianNavyGroupGenerator": RussianNavyGroupGenerator,
|
||||
"ChineseNavyGroupGenerator": ChineseNavyGroupGenerator,
|
||||
"GrishaGroupGenerator": GrishaGroupGenerator,
|
||||
"MolniyaGroupGenerator": MolniyaGroupGenerator,
|
||||
"KiloSubGroupGenerator": KiloSubGroupGenerator,
|
||||
"TangoSubGroupGenerator": TangoSubGroupGenerator,
|
||||
"Type54GroupGenerator": Type54GroupGenerator
|
||||
}
|
||||
|
||||
|
||||
def generate_ship_group(game, ground_object, faction_name: str):
|
||||
"""
|
||||
This generate a ship group
|
||||
:return: Nothing, but put the group reference inside the ground object
|
||||
"""
|
||||
faction = db.FACTIONS[faction_name]
|
||||
if len(faction.navy_generators) > 0:
|
||||
gen = random.choice(faction.navy_generators)
|
||||
if gen in SHIP_MAP.keys():
|
||||
generator = SHIP_MAP[gen](game, ground_object, faction)
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
else:
|
||||
logging.info("Unable to generate ship group, generator : " + str(gen) + "does not exists")
|
||||
return None
|
||||
|
||||
|
||||
def generate_carrier_group(faction: str, game, ground_object):
|
||||
"""
|
||||
This generate a carrier group
|
||||
:param parentCp: The parent control point
|
||||
:param ground_object: The ground object which will own the ship group
|
||||
:param country: Owner country
|
||||
:return: Nothing, but put the group reference inside the ground object
|
||||
"""
|
||||
generator = CarrierGroupGenerator(game, ground_object, db.FACTIONS[faction])
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
|
||||
|
||||
def generate_lha_group(faction: str, game, ground_object):
|
||||
"""
|
||||
This generate a lha carrier group
|
||||
:param parentCp: The parent control point
|
||||
:param ground_object: The ground object which will own the ship group
|
||||
:param country: Owner country
|
||||
:return: Nothing, but put the group reference inside the ground object
|
||||
"""
|
||||
generator = LHAGroupGenerator(game, ground_object, db.FACTIONS[faction])
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
15
gen/fleet/uboat.py
Normal file
15
gen/fleet/uboat.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import random
|
||||
|
||||
from dcs.ships import Uboat_VIIC_U_flak
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
|
||||
class UBoatGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
for i in range(random.randint(1, 4)):
|
||||
self.add_unit(Uboat_VIIC_U_flak, "Uboat" + str(i), self.position.x + i * random.randint(100, 250), self.position.y + (random.randint(100, 200)-100), self.heading)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
18
gen/fleet/ww2lst.py
Normal file
18
gen/fleet/ww2lst.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import random
|
||||
|
||||
from dcs.ships import LS_Samuel_Chase, LST_Mk_II
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
|
||||
class WW2LSTGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
# Add LS Samuel Chase
|
||||
self.add_unit(LS_Samuel_Chase, "SamuelChase", self.position.x, self.position.y, self.heading)
|
||||
|
||||
for i in range(1, random.randint(3, 4)):
|
||||
self.add_unit(LST_Mk_II, "LST" + str(i), self.position.x + i * random.randint(800, 1200), self.position.y, self.heading)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
535
gen/flights/ai_flight_planner.py
Normal file
535
gen/flights/ai_flight_planner.py
Normal file
@@ -0,0 +1,535 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import operator
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple, Type
|
||||
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.data.radar_db import UNITS_WITH_RADAR
|
||||
from game.infos.information import Information
|
||||
from game.utils import nm_to_meter
|
||||
from gen import Conflict
|
||||
from gen.ato import Package
|
||||
from gen.flights.ai_flight_planner_db import (
|
||||
CAP_CAPABLE,
|
||||
CAP_PREFERRED,
|
||||
CAS_CAPABLE,
|
||||
CAS_PREFERRED,
|
||||
SEAD_CAPABLE,
|
||||
SEAD_PREFERRED,
|
||||
STRIKE_CAPABLE,
|
||||
STRIKE_PREFERRED,
|
||||
)
|
||||
from gen.flights.closestairfields import (
|
||||
ClosestAirfields,
|
||||
ObjectiveDistanceCache,
|
||||
)
|
||||
from gen.flights.flight import (
|
||||
Flight,
|
||||
FlightType,
|
||||
)
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from theater import (
|
||||
ControlPoint,
|
||||
FrontLine,
|
||||
MissionTarget,
|
||||
TheaterGroundObject,
|
||||
SamGroundObject,
|
||||
)
|
||||
|
||||
# Avoid importing some types that cause circular imports unless type checking.
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProposedFlight:
|
||||
"""A flight outline proposed by the mission planner.
|
||||
|
||||
Proposed flights haven't been assigned specific aircraft yet. They have only
|
||||
a task, a required number of aircraft, and a maximum distance allowed
|
||||
between the objective and the departure airfield.
|
||||
"""
|
||||
|
||||
#: The flight's role.
|
||||
task: FlightType
|
||||
|
||||
#: The number of aircraft required.
|
||||
num_aircraft: int
|
||||
|
||||
#: The maximum distance between the objective and the departure airfield.
|
||||
max_distance: int
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.task.name} {self.num_aircraft} ship"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProposedMission:
|
||||
"""A mission outline proposed by the mission planner.
|
||||
|
||||
Proposed missions haven't been assigned aircraft yet. They have only an
|
||||
objective location and a list of proposed flights that are required for the
|
||||
mission.
|
||||
"""
|
||||
|
||||
#: The mission objective.
|
||||
location: MissionTarget
|
||||
|
||||
#: The proposed flights that are required for the mission.
|
||||
flights: List[ProposedFlight]
|
||||
|
||||
def __str__(self) -> str:
|
||||
flights = ', '.join([str(f) for f in self.flights])
|
||||
return f"{self.location.name}: {flights}"
|
||||
|
||||
|
||||
class AircraftAllocator:
|
||||
"""Finds suitable aircraft for proposed missions."""
|
||||
|
||||
def __init__(self, closest_airfields: ClosestAirfields,
|
||||
global_inventory: GlobalAircraftInventory,
|
||||
is_player: bool) -> None:
|
||||
self.closest_airfields = closest_airfields
|
||||
self.global_inventory = global_inventory
|
||||
self.is_player = is_player
|
||||
|
||||
def find_aircraft_for_flight(
|
||||
self, flight: ProposedFlight
|
||||
) -> Optional[Tuple[ControlPoint, UnitType]]:
|
||||
"""Finds aircraft suitable for the given mission.
|
||||
|
||||
Searches for aircraft capable of performing the given mission within the
|
||||
maximum allowed range. If insufficient aircraft are available for the
|
||||
mission, None is returned.
|
||||
|
||||
Airfields are searched ordered nearest to farthest from the target and
|
||||
searched twice. The first search looks for aircraft which prefer the
|
||||
mission type, and the second search looks for any aircraft which are
|
||||
capable of the mission type. For example, an F-14 from a nearby carrier
|
||||
will be preferred for the CAP of an airfield that has only F-16s, but if
|
||||
the carrier has only F/A-18s the F-16s will be used for CAP instead.
|
||||
|
||||
Note that aircraft *will* be removed from the global inventory on
|
||||
success. This is to ensure that the same aircraft are not matched twice
|
||||
on subsequent calls. If the found aircraft are not used, the caller is
|
||||
responsible for returning them to the inventory.
|
||||
"""
|
||||
result = self.find_aircraft_of_type(
|
||||
flight, self.preferred_aircraft_for_task(flight.task)
|
||||
)
|
||||
if result is not None:
|
||||
return result
|
||||
return self.find_aircraft_of_type(
|
||||
flight, self.capable_aircraft_for_task(flight.task)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
|
||||
if task in cap_missions:
|
||||
return CAP_PREFERRED
|
||||
elif task == FlightType.CAS:
|
||||
return CAS_PREFERRED
|
||||
elif task in (FlightType.DEAD, FlightType.SEAD):
|
||||
return SEAD_PREFERRED
|
||||
elif task == FlightType.STRIKE:
|
||||
return STRIKE_PREFERRED
|
||||
elif task == FlightType.ESCORT:
|
||||
return CAP_PREFERRED
|
||||
else:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
|
||||
if task in cap_missions:
|
||||
return CAP_CAPABLE
|
||||
elif task == FlightType.CAS:
|
||||
return CAS_CAPABLE
|
||||
elif task in (FlightType.DEAD, FlightType.SEAD):
|
||||
return SEAD_CAPABLE
|
||||
elif task == FlightType.STRIKE:
|
||||
return STRIKE_CAPABLE
|
||||
elif task == FlightType.ESCORT:
|
||||
return CAP_CAPABLE
|
||||
else:
|
||||
logging.error(f"Unplannable flight type: {task}")
|
||||
return []
|
||||
|
||||
def find_aircraft_of_type(
|
||||
self, flight: ProposedFlight, types: List[Type[FlyingType]],
|
||||
) -> Optional[Tuple[ControlPoint, UnitType]]:
|
||||
airfields_in_range = self.closest_airfields.airfields_within(
|
||||
flight.max_distance
|
||||
)
|
||||
for airfield in airfields_in_range:
|
||||
if not airfield.is_friendly(self.is_player):
|
||||
continue
|
||||
inventory = self.global_inventory.for_control_point(airfield)
|
||||
for aircraft, available in inventory.all_aircraft:
|
||||
if aircraft in types and available >= flight.num_aircraft:
|
||||
inventory.remove_aircraft(aircraft, flight.num_aircraft)
|
||||
return airfield, aircraft
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class PackageBuilder:
|
||||
"""Builds a Package for the flights it receives."""
|
||||
|
||||
def __init__(self, location: MissionTarget,
|
||||
closest_airfields: ClosestAirfields,
|
||||
global_inventory: GlobalAircraftInventory,
|
||||
is_player: bool,
|
||||
start_type: str) -> None:
|
||||
self.package = Package(location)
|
||||
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
|
||||
is_player)
|
||||
self.global_inventory = global_inventory
|
||||
self.start_type = start_type
|
||||
|
||||
def plan_flight(self, plan: ProposedFlight) -> bool:
|
||||
"""Allocates aircraft for the given flight and adds them to the package.
|
||||
|
||||
If no suitable aircraft are available, False is returned. If the failed
|
||||
flight was critical and the rest of the mission will be scrubbed, the
|
||||
caller should return any previously planned flights to the inventory
|
||||
using release_planned_aircraft.
|
||||
"""
|
||||
assignment = self.allocator.find_aircraft_for_flight(plan)
|
||||
if assignment is None:
|
||||
return False
|
||||
airfield, aircraft = assignment
|
||||
flight = Flight(self.package, aircraft, plan.num_aircraft, airfield,
|
||||
plan.task, self.start_type)
|
||||
self.package.add_flight(flight)
|
||||
return True
|
||||
|
||||
def build(self) -> Package:
|
||||
"""Returns the built package."""
|
||||
return self.package
|
||||
|
||||
def release_planned_aircraft(self) -> None:
|
||||
"""Returns any planned flights to the inventory."""
|
||||
flights = list(self.package.flights)
|
||||
for flight in flights:
|
||||
self.global_inventory.return_from_flight(flight)
|
||||
self.package.remove_flight(flight)
|
||||
|
||||
|
||||
class ObjectiveFinder:
|
||||
"""Identifies potential objectives for the mission planner."""
|
||||
|
||||
# TODO: Merge into doctrine.
|
||||
AIRFIELD_THREAT_RANGE = nm_to_meter(150)
|
||||
SAM_THREAT_RANGE = nm_to_meter(100)
|
||||
|
||||
def __init__(self, game: Game, is_player: bool) -> None:
|
||||
self.game = game
|
||||
self.is_player = is_player
|
||||
|
||||
def enemy_sams(self) -> Iterator[TheaterGroundObject]:
|
||||
"""Iterates over all enemy SAM sites."""
|
||||
# Control points might have the same ground object several times, for
|
||||
# some reason.
|
||||
found_targets: Set[str] = set()
|
||||
for cp in self.enemy_control_points():
|
||||
for ground_object in cp.ground_objects:
|
||||
if not isinstance(ground_object, SamGroundObject):
|
||||
continue
|
||||
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
|
||||
if ground_object.name in found_targets:
|
||||
continue
|
||||
|
||||
if not self.object_has_radar(ground_object):
|
||||
continue
|
||||
|
||||
# TODO: Yield in order of most threatening.
|
||||
# Need to sort in order of how close their defensive range comes
|
||||
# to friendly assets. To do that we need to add effective range
|
||||
# information to the database.
|
||||
yield ground_object
|
||||
found_targets.add(ground_object.name)
|
||||
|
||||
def threatening_sams(self) -> Iterator[TheaterGroundObject]:
|
||||
"""Iterates over enemy SAMs in threat range of friendly control points.
|
||||
|
||||
SAM sites are sorted by their closest proximity to any friendly control
|
||||
point (airfield or fleet).
|
||||
"""
|
||||
sams: List[Tuple[TheaterGroundObject, int]] = []
|
||||
for sam in self.enemy_sams():
|
||||
ranges: List[int] = []
|
||||
for cp in self.friendly_control_points():
|
||||
ranges.append(sam.distance_to(cp))
|
||||
sams.append((sam, min(ranges)))
|
||||
|
||||
sams = sorted(sams, key=operator.itemgetter(1))
|
||||
for sam, _range in sams:
|
||||
yield sam
|
||||
|
||||
def strike_targets(self) -> Iterator[TheaterGroundObject]:
|
||||
"""Iterates over enemy strike targets.
|
||||
|
||||
Targets are sorted by their closest proximity to any friendly control
|
||||
point (airfield or fleet).
|
||||
"""
|
||||
targets: List[Tuple[TheaterGroundObject, int]] = []
|
||||
# Control points might have the same ground object several times, for
|
||||
# some reason.
|
||||
found_targets: Set[str] = set()
|
||||
for enemy_cp in self.enemy_control_points():
|
||||
for ground_object in enemy_cp.ground_objects:
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
if ground_object.name in found_targets:
|
||||
continue
|
||||
ranges: List[int] = []
|
||||
for friendly_cp in self.friendly_control_points():
|
||||
ranges.append(ground_object.distance_to(friendly_cp))
|
||||
targets.append((ground_object, min(ranges)))
|
||||
found_targets.add(ground_object.name)
|
||||
targets = sorted(targets, key=operator.itemgetter(1))
|
||||
for target, _range in targets:
|
||||
yield target
|
||||
|
||||
@staticmethod
|
||||
def object_has_radar(ground_object: TheaterGroundObject) -> bool:
|
||||
"""Returns True if the ground object contains a unit with radar."""
|
||||
for group in ground_object.groups:
|
||||
for unit in group.units:
|
||||
if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR:
|
||||
return True
|
||||
return False
|
||||
|
||||
def front_lines(self) -> Iterator[FrontLine]:
|
||||
"""Iterates over all active front lines in the theater."""
|
||||
for cp in self.friendly_control_points():
|
||||
for connected in cp.connected_points:
|
||||
if connected.is_friendly(self.is_player):
|
||||
continue
|
||||
|
||||
if Conflict.has_frontline_between(cp, connected):
|
||||
yield FrontLine(cp, connected)
|
||||
|
||||
def vulnerable_control_points(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over friendly CPs that are vulnerable to enemy CPs.
|
||||
|
||||
Vulnerability is defined as any enemy CP within threat range of of the
|
||||
CP.
|
||||
"""
|
||||
for cp in self.friendly_control_points():
|
||||
airfields_in_proximity = self.closest_airfields_to(cp)
|
||||
airfields_in_threat_range = airfields_in_proximity.airfields_within(
|
||||
self.AIRFIELD_THREAT_RANGE
|
||||
)
|
||||
for airfield in airfields_in_threat_range:
|
||||
if not airfield.is_friendly(self.is_player):
|
||||
yield cp
|
||||
break
|
||||
|
||||
def friendly_control_points(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all friendly control points."""
|
||||
return (c for c in self.game.theater.controlpoints if
|
||||
c.is_friendly(self.is_player))
|
||||
|
||||
def enemy_control_points(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all enemy control points."""
|
||||
return (c for c in self.game.theater.controlpoints if
|
||||
not c.is_friendly(self.is_player))
|
||||
|
||||
def all_possible_targets(self) -> Iterator[MissionTarget]:
|
||||
"""Iterates over all possible mission targets in the theater.
|
||||
|
||||
Valid mission targets are control points (airfields and carriers), front
|
||||
lines, and ground objects (SAM sites, factories, resource extraction
|
||||
sites, etc).
|
||||
"""
|
||||
for cp in self.game.theater.controlpoints:
|
||||
yield cp
|
||||
yield from cp.ground_objects
|
||||
yield from self.front_lines()
|
||||
|
||||
@staticmethod
|
||||
def closest_airfields_to(location: MissionTarget) -> ClosestAirfields:
|
||||
"""Returns the closest airfields to the given location."""
|
||||
return ObjectiveDistanceCache.get_closest_airfields(location)
|
||||
|
||||
|
||||
class CoalitionMissionPlanner:
|
||||
"""Coalition flight planning AI.
|
||||
|
||||
This class is responsible for automatically planning missions for the
|
||||
coalition at the start of the turn.
|
||||
|
||||
The primary goal of the mission planner is to protect existing friendly
|
||||
assets. Missions will be planned with the following priorities:
|
||||
|
||||
1. CAP for airfields/fleets in close proximity to the enemy to prevent heavy
|
||||
losses of friendly aircraft.
|
||||
2. CAP for front line areas to protect ground and CAS units.
|
||||
3. DEAD to reduce necessity of SEAD for future missions.
|
||||
4. CAS to protect friendly ground units.
|
||||
5. Strike missions to reduce the enemy's resources.
|
||||
|
||||
TODO: Anti-ship and airfield strikes to reduce enemy sortie rates.
|
||||
TODO: BAI to prevent enemy forces from reaching the front line.
|
||||
TODO: Should fleets always have a CAP?
|
||||
|
||||
TODO: Stance and doctrine-specific planning behavior.
|
||||
"""
|
||||
|
||||
# TODO: Merge into doctrine, also limit by aircraft.
|
||||
MAX_CAP_RANGE = nm_to_meter(100)
|
||||
MAX_CAS_RANGE = nm_to_meter(50)
|
||||
MAX_SEAD_RANGE = nm_to_meter(150)
|
||||
MAX_STRIKE_RANGE = nm_to_meter(150)
|
||||
|
||||
def __init__(self, game: Game, is_player: bool) -> None:
|
||||
self.game = game
|
||||
self.is_player = is_player
|
||||
self.objective_finder = ObjectiveFinder(self.game, self.is_player)
|
||||
self.ato = self.game.blue_ato if is_player else self.game.red_ato
|
||||
|
||||
def propose_missions(self) -> Iterator[ProposedMission]:
|
||||
"""Identifies and iterates over potential mission in priority order."""
|
||||
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||
for cp in self.objective_finder.vulnerable_control_points():
|
||||
yield ProposedMission(cp, [
|
||||
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||
])
|
||||
|
||||
# Find front lines, plan CAP.
|
||||
for front_line in self.objective_finder.front_lines():
|
||||
yield ProposedMission(front_line, [
|
||||
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE),
|
||||
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
|
||||
])
|
||||
|
||||
# Find enemy SAM sites with ranges that cover friendly CPs, front lines,
|
||||
# or objects, plan DEAD.
|
||||
# Find enemy SAM sites with ranges that extend to within 50 nmi of
|
||||
# friendly CPs, front, lines, or objects, plan DEAD.
|
||||
for sam in self.objective_finder.threatening_sams():
|
||||
yield ProposedMission(sam, [
|
||||
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE),
|
||||
])
|
||||
|
||||
# Plan strike missions.
|
||||
for target in self.objective_finder.strike_targets():
|
||||
yield ProposedMission(target, [
|
||||
ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE),
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE),
|
||||
])
|
||||
|
||||
def plan_missions(self) -> None:
|
||||
"""Identifies and plans mission for the turn."""
|
||||
for proposed_mission in self.propose_missions():
|
||||
self.plan_mission(proposed_mission)
|
||||
|
||||
self.stagger_missions()
|
||||
|
||||
for cp in self.objective_finder.friendly_control_points():
|
||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||
for aircraft, available in inventory.all_aircraft:
|
||||
self.message("Unused aircraft",
|
||||
f"{available} {aircraft.id} from {cp}")
|
||||
|
||||
def plan_mission(self, mission: ProposedMission) -> None:
|
||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||
|
||||
if self.game.settings.perf_ai_parking_start:
|
||||
start_type = "Cold"
|
||||
else:
|
||||
start_type = "Warm"
|
||||
|
||||
builder = PackageBuilder(
|
||||
mission.location,
|
||||
self.objective_finder.closest_airfields_to(mission.location),
|
||||
self.game.aircraft_inventory,
|
||||
self.is_player,
|
||||
start_type
|
||||
)
|
||||
|
||||
missing_types: Set[FlightType] = set()
|
||||
for proposed_flight in mission.flights:
|
||||
if not builder.plan_flight(proposed_flight):
|
||||
missing_types.add(proposed_flight.task)
|
||||
|
||||
if missing_types:
|
||||
missing_types_str = ", ".join(
|
||||
sorted([t.name for t in missing_types]))
|
||||
builder.release_planned_aircraft()
|
||||
self.message(
|
||||
"Insufficient aircraft",
|
||||
f"Not enough aircraft in range for {mission.location.name} "
|
||||
f"capable of: {missing_types_str}")
|
||||
return
|
||||
|
||||
package = builder.build()
|
||||
flight_plan_builder = FlightPlanBuilder(self.game, package,
|
||||
self.is_player)
|
||||
for flight in package.flights:
|
||||
flight_plan_builder.populate_flight_plan(flight)
|
||||
self.ato.add_package(package)
|
||||
|
||||
def stagger_missions(self) -> None:
|
||||
def start_time_generator(count: int, earliest: int, latest: int,
|
||||
margin: int) -> Iterator[timedelta]:
|
||||
interval = (latest - earliest) // count
|
||||
for time in range(earliest, latest, interval):
|
||||
error = random.randint(-margin, margin)
|
||||
yield timedelta(minutes=max(0, time + error))
|
||||
|
||||
dca_types = (FlightType.BARCAP, FlightType.INTERCEPTION)
|
||||
|
||||
non_dca_packages = [p for p in self.ato.packages if
|
||||
p.primary_task not in dca_types]
|
||||
|
||||
start_time = start_time_generator(
|
||||
count=len(non_dca_packages),
|
||||
earliest=5,
|
||||
latest=90,
|
||||
margin=5
|
||||
)
|
||||
for package in self.ato.packages:
|
||||
tot = TotEstimator(package).earliest_tot()
|
||||
if package.primary_task in dca_types:
|
||||
# All CAP missions should be on station ASAP.
|
||||
package.time_over_target = tot
|
||||
else:
|
||||
# But other packages should be spread out a bit. Note that take
|
||||
# times are delayed, but all aircraft will become active at
|
||||
# mission start. This makes it more worthwhile to attack enemy
|
||||
# airfields to hit grounded aircraft, since they're more likely
|
||||
# to be present. Runway and air started aircraft will be
|
||||
# delayed until their takeoff time by AirConflictGenerator.
|
||||
package.time_over_target = next(start_time) + tot
|
||||
|
||||
def message(self, title, text) -> None:
|
||||
"""Emits a planning message to the player.
|
||||
|
||||
If the mission planner belongs to the players coalition, this emits a
|
||||
message to the info panel.
|
||||
"""
|
||||
if self.is_player:
|
||||
self.game.informations.append(
|
||||
Information(title, text, self.game.turn)
|
||||
)
|
||||
else:
|
||||
logging.info(f"{title}: {text}")
|
||||
474
gen/flights/ai_flight_planner_db.py
Normal file
474
gen/flights/ai_flight_planner_db.py
Normal file
@@ -0,0 +1,474 @@
|
||||
from dcs.helicopters import (
|
||||
AH_1W,
|
||||
AH_64A,
|
||||
AH_64D,
|
||||
Ka_50,
|
||||
Mi_24V,
|
||||
Mi_28N,
|
||||
Mi_8MT,
|
||||
OH_58D,
|
||||
SA342L,
|
||||
SA342M,
|
||||
UH_1H,
|
||||
)
|
||||
from dcs.planes import (
|
||||
AJS37,
|
||||
AV8BNA,
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
A_20G,
|
||||
B_17G,
|
||||
B_1B,
|
||||
B_52H,
|
||||
Bf_109K_4,
|
||||
C_101CC,
|
||||
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,
|
||||
F_A_18C,
|
||||
JF_17,
|
||||
J_11A,
|
||||
Ju_88A4,
|
||||
L_39ZA,
|
||||
MQ_9_Reaper,
|
||||
M_2000C,
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
MiG_25PD,
|
||||
MiG_27K,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29K,
|
||||
MiG_29S,
|
||||
MiG_31,
|
||||
Mirage_2000_5,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
RQ_1A_Predator,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_24MR,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_27,
|
||||
Su_30,
|
||||
Su_33,
|
||||
Su_34,
|
||||
Tornado_GR4,
|
||||
Tornado_IDS,
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
WingLoong_I,
|
||||
)
|
||||
|
||||
# Interceptor are the aircraft prioritized for interception tasks
|
||||
# If none is available, the AI will use regular CAP-capable aircraft instead
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M
|
||||
|
||||
# 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.
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
|
||||
INTERCEPT_CAPABLE = [
|
||||
MiG_21Bis,
|
||||
MiG_25PD,
|
||||
MiG_31,
|
||||
MiG_29S,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29K,
|
||||
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
Rafale_M,
|
||||
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
|
||||
]
|
||||
|
||||
# Used for CAP, Escort, and intercept if there is not a specialised aircraft available
|
||||
CAP_CAPABLE = [
|
||||
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
MiG_25PD,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29S,
|
||||
MiG_31,
|
||||
|
||||
Su_27,
|
||||
J_11A,
|
||||
JF_17,
|
||||
Su_30,
|
||||
Su_33,
|
||||
Su_57,
|
||||
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_15E,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_M,
|
||||
]
|
||||
|
||||
CAP_PREFERRED = [
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
MiG_25PD,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29S,
|
||||
MiG_31,
|
||||
|
||||
Su_27,
|
||||
J_11A,
|
||||
Su_30,
|
||||
Su_33,
|
||||
Su_57,
|
||||
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
|
||||
Rafale_M,
|
||||
]
|
||||
|
||||
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
|
||||
CAS_CAPABLE = [
|
||||
|
||||
MiG_15bis,
|
||||
MiG_29A,
|
||||
MiG_27K,
|
||||
MiG_29S,
|
||||
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_24MR,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_34,
|
||||
|
||||
JF_17,
|
||||
|
||||
M_2000C,
|
||||
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_5E_3,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15E,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
|
||||
B_1B,
|
||||
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
|
||||
C_101CC,
|
||||
MB_339PAN,
|
||||
L_39ZA,
|
||||
AJS37,
|
||||
|
||||
SA342M,
|
||||
SA342L,
|
||||
OH_58D,
|
||||
|
||||
AH_64A,
|
||||
AH_64D,
|
||||
AH_1W,
|
||||
|
||||
UH_1H,
|
||||
|
||||
Mi_8MT,
|
||||
Mi_28N,
|
||||
Mi_24V,
|
||||
Ka_50,
|
||||
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
A_20G,
|
||||
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S,
|
||||
|
||||
WingLoong_I,
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator
|
||||
]
|
||||
|
||||
CAS_PREFERRED = [
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_24MR,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_34,
|
||||
|
||||
JF_17,
|
||||
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
|
||||
F_15E,
|
||||
|
||||
Tornado_GR4,
|
||||
|
||||
C_101CC,
|
||||
MB_339PAN,
|
||||
L_39ZA,
|
||||
AJS37,
|
||||
|
||||
SA342M,
|
||||
SA342L,
|
||||
OH_58D,
|
||||
|
||||
AH_64A,
|
||||
AH_64D,
|
||||
AH_1W,
|
||||
|
||||
UH_1H,
|
||||
|
||||
Mi_8MT,
|
||||
Mi_28N,
|
||||
Mi_24V,
|
||||
Ka_50,
|
||||
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
A_20G,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S,
|
||||
|
||||
WingLoong_I,
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator
|
||||
]
|
||||
|
||||
# Aircraft used for SEAD / DEAD tasks
|
||||
SEAD_CAPABLE = [
|
||||
F_4E,
|
||||
FA_18C_hornet,
|
||||
F_15E,
|
||||
F_16C_50,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
|
||||
Su_24M,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_17M4,
|
||||
Su_30,
|
||||
Su_34,
|
||||
MiG_27K,
|
||||
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S
|
||||
]
|
||||
|
||||
SEAD_PREFERRED = [
|
||||
F_4E,
|
||||
Su_25T,
|
||||
Tornado_IDS,
|
||||
]
|
||||
|
||||
# Aircraft used for Strike mission
|
||||
STRIKE_CAPABLE = [
|
||||
MiG_15bis,
|
||||
MiG_27K,
|
||||
MB_339PAN,
|
||||
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_24MR,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_34,
|
||||
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
|
||||
JF_17,
|
||||
|
||||
M_2000C,
|
||||
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_5E_3,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15E,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
|
||||
B_1B,
|
||||
B_52H,
|
||||
F_117A,
|
||||
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
AJS37,
|
||||
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
A_20G,
|
||||
B_17G,
|
||||
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S
|
||||
|
||||
]
|
||||
|
||||
STRIKE_PREFERRED = [
|
||||
AJS37,
|
||||
A_20G,
|
||||
B_17G,
|
||||
B_1B,
|
||||
B_52H,
|
||||
F_117A,
|
||||
F_15E,
|
||||
Tornado_GR4,
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
]
|
||||
|
||||
ANTISHIP_CAPABLE = [
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
F_A_18C,
|
||||
F_15E,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
A_10A,
|
||||
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
|
||||
Ju_88A4,
|
||||
Rafale_A_S
|
||||
]
|
||||
|
||||
DRONES = [
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator,
|
||||
WingLoong_I
|
||||
]
|
||||
51
gen/flights/closestairfields.py
Normal file
51
gen/flights/closestairfields.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Objective adjacency lists."""
|
||||
from typing import Dict, Iterator, List, Optional
|
||||
|
||||
from theater import ConflictTheater, ControlPoint, MissionTarget
|
||||
|
||||
|
||||
class ClosestAirfields:
|
||||
"""Precalculates which control points are closes to the given target."""
|
||||
|
||||
def __init__(self, target: MissionTarget,
|
||||
all_control_points: List[ControlPoint]) -> None:
|
||||
self.target = target
|
||||
self.closest_airfields: List[ControlPoint] = sorted(
|
||||
all_control_points, key=lambda c: self.target.distance_to(c)
|
||||
)
|
||||
|
||||
def airfields_within(self, meters: int) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all airfields within the given range of the target.
|
||||
|
||||
Note that this iterates over *all* airfields, not just friendly
|
||||
airfields.
|
||||
"""
|
||||
for cp in self.closest_airfields:
|
||||
if cp.distance_to(self.target) < meters:
|
||||
yield cp
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
class ObjectiveDistanceCache:
|
||||
theater: Optional[ConflictTheater] = None
|
||||
closest_airfields: Dict[str, ClosestAirfields] = {}
|
||||
|
||||
@classmethod
|
||||
def set_theater(cls, theater: ConflictTheater) -> None:
|
||||
if cls.theater is not None:
|
||||
cls.closest_airfields = {}
|
||||
cls.theater = theater
|
||||
|
||||
@classmethod
|
||||
def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields:
|
||||
if cls.theater is None:
|
||||
raise RuntimeError(
|
||||
"Call ObjectiveDistanceCache.set_theater before using"
|
||||
)
|
||||
|
||||
if location.name not in cls.closest_airfields:
|
||||
cls.closest_airfields[location.name] = ClosestAirfields(
|
||||
location, cls.theater.controlpoints
|
||||
)
|
||||
return cls.closest_airfields[location.name]
|
||||
162
gen/flights/flight.py
Normal file
162
gen/flights/flight.py
Normal file
@@ -0,0 +1,162 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from theater.controlpoint import ControlPoint, MissionTarget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.ato import Package
|
||||
from gen.flights.flightplan import FlightPlan
|
||||
|
||||
|
||||
class FlightType(Enum):
|
||||
CAP = 0 # Do not use. Use BARCAP or TARCAP.
|
||||
TARCAP = 1
|
||||
BARCAP = 2
|
||||
CAS = 3
|
||||
INTERCEPTION = 4
|
||||
STRIKE = 5
|
||||
ANTISHIP = 6
|
||||
SEAD = 7
|
||||
DEAD = 8
|
||||
ESCORT = 9
|
||||
BAI = 10
|
||||
|
||||
# Helos
|
||||
TROOP_TRANSPORT = 11
|
||||
LOGISTICS = 12
|
||||
EVAC = 13
|
||||
|
||||
ELINT = 14
|
||||
RECON = 15
|
||||
EWAR = 16
|
||||
|
||||
|
||||
class FlightWaypointType(Enum):
|
||||
TAKEOFF = 0 # Take off point
|
||||
ASCEND_POINT = 1 # Ascension point after take off
|
||||
PATROL = 2 # Patrol point
|
||||
PATROL_TRACK = 3 # Patrol race track
|
||||
NAV = 4 # Nav point
|
||||
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
|
||||
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
|
||||
INGRESS_CAS = 7 # Ingress cas (should start CAS task)
|
||||
CAS = 8 # Should do CAS there
|
||||
EGRESS = 9 # Should stop attack
|
||||
DESCENT_POINT = 10 # Should start descending to pattern alt
|
||||
LANDING_POINT = 11 # Should land there
|
||||
TARGET_POINT = 12 # A target building or static object, position
|
||||
TARGET_GROUP_LOC = 13 # A target group approximate location
|
||||
TARGET_SHIP = 14 # A target ship known location
|
||||
CUSTOM = 15 # User waypoint (no specific behaviour)
|
||||
JOIN = 16
|
||||
SPLIT = 17
|
||||
LOITER = 18
|
||||
INGRESS_ESCORT = 19
|
||||
INGRESS_DEAD = 20
|
||||
|
||||
|
||||
class FlightWaypoint:
|
||||
|
||||
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
|
||||
alt: int = 0) -> None:
|
||||
"""Creates a flight waypoint.
|
||||
|
||||
Args:
|
||||
waypoint_type: The waypoint type.
|
||||
x: X cooidinate of the waypoint.
|
||||
y: Y coordinate of the waypoint.
|
||||
alt: Altitude of the waypoint. By default this is AGL, but it can be
|
||||
changed to MSL by setting alt_type to "RADIO".
|
||||
"""
|
||||
self.waypoint_type = waypoint_type
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.alt = alt
|
||||
self.alt_type = "BARO"
|
||||
self.name = ""
|
||||
self.description = ""
|
||||
self.targets: List[MissionTarget] = []
|
||||
self.obj_name = ""
|
||||
self.pretty_name = ""
|
||||
self.only_for_player = False
|
||||
|
||||
# These are set very late by the air conflict generator (part of mission
|
||||
# generation). We do it late so that we don't need to propagate changes
|
||||
# to waypoint times whenever the player alters the package TOT or the
|
||||
# flight's offset in the UI.
|
||||
self.tot: Optional[timedelta] = None
|
||||
self.departure_time: Optional[timedelta] = None
|
||||
|
||||
@property
|
||||
def position(self) -> Point:
|
||||
return Point(self.x, self.y)
|
||||
|
||||
@classmethod
|
||||
def from_pydcs(cls, point: MovingPoint,
|
||||
from_cp: ControlPoint) -> "FlightWaypoint":
|
||||
waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x,
|
||||
point.position.y, point.alt)
|
||||
waypoint.alt_type = point.alt_type
|
||||
# Other actions exist... but none of them *should* be the first
|
||||
# waypoint for a flight.
|
||||
waypoint.waypoint_type = {
|
||||
PointAction.TurningPoint: FlightWaypointType.NAV,
|
||||
PointAction.FlyOverPoint: FlightWaypointType.NAV,
|
||||
PointAction.FromParkingArea: FlightWaypointType.TAKEOFF,
|
||||
PointAction.FromParkingAreaHot: FlightWaypointType.TAKEOFF,
|
||||
PointAction.FromRunway: FlightWaypointType.TAKEOFF,
|
||||
}[point.action]
|
||||
if waypoint.waypoint_type == FlightWaypointType.NAV:
|
||||
waypoint.name = "NAV"
|
||||
waypoint.pretty_name = "Nav"
|
||||
waypoint.description = "Nav"
|
||||
else:
|
||||
waypoint.name = "TAKEOFF"
|
||||
waypoint.pretty_name = "Takeoff"
|
||||
waypoint.description = "Takeoff"
|
||||
waypoint.description = f"Takeoff from {from_cp.name}"
|
||||
return waypoint
|
||||
|
||||
|
||||
class Flight:
|
||||
|
||||
def __init__(self, package: Package, unit_type: FlyingType, count: int,
|
||||
from_cp: ControlPoint, flight_type: FlightType,
|
||||
start_type: str) -> None:
|
||||
self.package = package
|
||||
self.unit_type = unit_type
|
||||
self.count = count
|
||||
self.from_cp = from_cp
|
||||
self.flight_type = flight_type
|
||||
# TODO: Replace with FlightPlan.
|
||||
self.targets: List[MissionTarget] = []
|
||||
self.loadout: Dict[str, str] = {}
|
||||
self.start_type = start_type
|
||||
self.use_custom_loadout = False
|
||||
self.client_count = 0
|
||||
|
||||
# Will be replaced with a more appropriate FlightPlan by
|
||||
# FlightPlanBuilder, but an empty flight plan the flight begins with an
|
||||
# empty flight plan.
|
||||
from gen.flights.flightplan import CustomFlightPlan
|
||||
self.flight_plan: FlightPlan = CustomFlightPlan(
|
||||
package=package,
|
||||
flight=self,
|
||||
custom_waypoints=[]
|
||||
)
|
||||
|
||||
@property
|
||||
def points(self) -> List[FlightWaypoint]:
|
||||
return self.flight_plan.waypoints[1:]
|
||||
|
||||
def __repr__(self):
|
||||
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
|
||||
+ " (" + str(len(self.points)) + " wpt)"
|
||||
1021
gen/flights/flightplan.py
Normal file
1021
gen/flights/flightplan.py
Normal file
File diff suppressed because it is too large
Load Diff
212
gen/flights/traveltime.py
Normal file
212
gen/flights/traveltime.py
Normal file
@@ -0,0 +1,212 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from datetime import timedelta
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.utils import meter_to_nm
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.ato import Package
|
||||
|
||||
|
||||
class GroundSpeed:
|
||||
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, altitude: int) -> int:
|
||||
if not issubclass(flight.unit_type, FlyingType):
|
||||
raise TypeError("Flight has non-flying unit")
|
||||
|
||||
# TODO: Expose both a cruise speed and target speed.
|
||||
# The cruise speed can be used for ascent, hold, join, and RTB to save
|
||||
# on fuel, but mission speed will be fast enough to keep the flight
|
||||
# safer.
|
||||
|
||||
c_sound_sea_level = 661.5
|
||||
|
||||
# DCS's max speed is in kph at 0 MSL. Convert to knots.
|
||||
max_speed = flight.unit_type.max_speed * 0.539957
|
||||
if max_speed > c_sound_sea_level:
|
||||
# Aircraft is supersonic. Limit to mach 0.8 to conserve fuel and
|
||||
# account for heavily loaded jets.
|
||||
return int(cls.from_mach(0.8, altitude))
|
||||
|
||||
# For subsonic aircraft, assume the aircraft can reasonably perform at
|
||||
# 80% of its maximum, and that it can maintain the same mach at altitude
|
||||
# as it can at sea level. This probably isn't great assumption, but
|
||||
# might. be sufficient given the wiggle room. We can come up with
|
||||
# another heuristic if needed.
|
||||
mach = max_speed * 0.8 / c_sound_sea_level
|
||||
return int(cls.from_mach(mach, altitude)) # knots
|
||||
|
||||
@staticmethod
|
||||
def from_mach(mach: float, altitude: int) -> float:
|
||||
"""Returns the ground speed in knots for the given mach and altitude.
|
||||
|
||||
Args:
|
||||
mach: The mach number to convert to ground speed.
|
||||
altitude: The altitude in feet.
|
||||
|
||||
Returns:
|
||||
The ground speed corresponding to the given altitude and mach number
|
||||
in knots.
|
||||
"""
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
|
||||
if altitude <= 36152:
|
||||
temperature_f = 59 - 0.00356 * altitude
|
||||
else:
|
||||
# There's another formula for altitudes over 82k feet, but we better
|
||||
# not be planning waypoints that high...
|
||||
temperature_f = -70
|
||||
|
||||
temperature_k = (temperature_f + 459.67) * (5 / 9)
|
||||
|
||||
# https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html
|
||||
# Dependent on temperature, but varies very little (+/-0.001)
|
||||
# between -40F and 180F.
|
||||
heat_capacity_ratio = 1.4
|
||||
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html
|
||||
gas_constant = 286 # m^2/s^2/K
|
||||
c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k)
|
||||
# c_sound is in m/s, convert to knots.
|
||||
return (c_sound * 1.944) * mach
|
||||
|
||||
|
||||
class TravelTime:
|
||||
@staticmethod
|
||||
def between_points(a: Point, b: Point, speed: float) -> timedelta:
|
||||
error_factor = 1.1
|
||||
distance = meter_to_nm(a.distance_to_point(b))
|
||||
return timedelta(hours=distance / speed * error_factor)
|
||||
|
||||
|
||||
class TotEstimator:
|
||||
# An extra five minutes given as wiggle room. Expected to be spent at the
|
||||
# hold point performing any last minute configuration.
|
||||
HOLD_TIME = timedelta(minutes=5)
|
||||
|
||||
def __init__(self, package: Package) -> None:
|
||||
self.package = package
|
||||
|
||||
def mission_start_time(self, flight: Flight) -> timedelta:
|
||||
takeoff_time = self.takeoff_time_for_flight(flight)
|
||||
if takeoff_time is None:
|
||||
# Could not determine takeoff time, probably due to a custom flight
|
||||
# plan. Start immediately.
|
||||
return timedelta()
|
||||
|
||||
startup_time = self.estimate_startup(flight)
|
||||
ground_ops_time = self.estimate_ground_ops(flight)
|
||||
start_time = takeoff_time - startup_time - ground_ops_time
|
||||
# In case FP math has given us some barely below zero time, round to
|
||||
# zero.
|
||||
if math.isclose(start_time.total_seconds(), 0):
|
||||
return 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.
|
||||
return timedelta(seconds=math.floor(start_time.total_seconds()))
|
||||
|
||||
def takeoff_time_for_flight(self, flight: Flight) -> Optional[timedelta]:
|
||||
travel_time = self.travel_time_to_rendezvous_or_target(flight)
|
||||
if travel_time is None:
|
||||
from gen.flights.flightplan import CustomFlightPlan
|
||||
if not isinstance(flight.flight_plan, CustomFlightPlan):
|
||||
logging.warning(
|
||||
"Found no rendezvous or target point. Cannot estimate "
|
||||
f"takeoff time takeoff time for {flight}.")
|
||||
return None
|
||||
|
||||
from gen.flights.flightplan import FormationFlightPlan
|
||||
if isinstance(flight.flight_plan, FormationFlightPlan):
|
||||
tot = flight.flight_plan.tot_for_waypoint(
|
||||
flight.flight_plan.join)
|
||||
if tot is None:
|
||||
logging.warning(
|
||||
"Could not determine the TOT of the join point. Takeoff "
|
||||
f"time for {flight} will be immediate.")
|
||||
return None
|
||||
else:
|
||||
tot = self.package.time_over_target
|
||||
return tot - travel_time - self.HOLD_TIME
|
||||
|
||||
def earliest_tot(self) -> timedelta:
|
||||
earliest_tot = max((
|
||||
self.earliest_tot_for_flight(f) for f in self.package.flights
|
||||
)) + self.HOLD_TIME
|
||||
|
||||
# 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 up so we don't get negative start times.
|
||||
return timedelta(seconds=math.ceil(earliest_tot.total_seconds()))
|
||||
|
||||
def earliest_tot_for_flight(self, flight: Flight) -> timedelta:
|
||||
"""Estimate fastest time from mission start to the target position.
|
||||
|
||||
For BARCAP flights, this is time to race track start. This ensures that
|
||||
they are on station at the same time any other package members reach
|
||||
their ingress point.
|
||||
|
||||
For other mission types this is the time to the mission target.
|
||||
|
||||
Args:
|
||||
flight: The flight to get the earliest TOT time for.
|
||||
|
||||
Returns:
|
||||
The earliest possible TOT for the given flight in seconds. Returns 0
|
||||
if an ingress point cannot be found.
|
||||
"""
|
||||
time_to_target = self.travel_time_to_target(flight)
|
||||
if time_to_target is None:
|
||||
logging.warning(f"Cannot estimate TOT for {flight}")
|
||||
# Return 0 so this flight's travel time does not affect the rest
|
||||
# of the package.
|
||||
return timedelta()
|
||||
startup = self.estimate_startup(flight)
|
||||
ground_ops = self.estimate_ground_ops(flight)
|
||||
return startup + ground_ops + time_to_target
|
||||
|
||||
@staticmethod
|
||||
def estimate_startup(flight: Flight) -> timedelta:
|
||||
if flight.start_type == "Cold":
|
||||
if flight.client_count:
|
||||
return timedelta(minutes=10)
|
||||
else:
|
||||
# The AI doesn't seem to have a real startup procedure.
|
||||
return timedelta(minutes=2)
|
||||
return timedelta()
|
||||
|
||||
@staticmethod
|
||||
def estimate_ground_ops(flight: Flight) -> timedelta:
|
||||
if flight.start_type in ("Runway", "In Flight"):
|
||||
return timedelta()
|
||||
if flight.from_cp.is_fleet:
|
||||
return timedelta(minutes=2)
|
||||
else:
|
||||
return timedelta(minutes=5)
|
||||
|
||||
@staticmethod
|
||||
def travel_time_to_target(flight: Flight) -> Optional[timedelta]:
|
||||
if flight.flight_plan is None:
|
||||
return None
|
||||
return flight.flight_plan.travel_time_to_target
|
||||
|
||||
@staticmethod
|
||||
def travel_time_to_rendezvous_or_target(
|
||||
flight: Flight) -> Optional[timedelta]:
|
||||
if flight.flight_plan is None:
|
||||
return None
|
||||
from gen.flights.flightplan import FormationFlightPlan
|
||||
if isinstance(flight.flight_plan, FormationFlightPlan):
|
||||
return flight.flight_plan.travel_time_to_rendezvous
|
||||
return flight.flight_plan.travel_time_to_target
|
||||
312
gen/flights/waypointbuilder.py
Normal file
312
gen/flights/waypointbuilder.py
Normal file
@@ -0,0 +1,312 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.weather import Conditions
|
||||
from theater import ControlPoint, MissionTarget, TheaterGroundObject
|
||||
from .flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StrikeTarget:
|
||||
name: str
|
||||
target: Union[TheaterGroundObject, Unit]
|
||||
|
||||
|
||||
class WaypointBuilder:
|
||||
def __init__(self, conditions: Conditions, flight: Flight,
|
||||
doctrine: Doctrine,
|
||||
targets: Optional[List[StrikeTarget]] = None) -> None:
|
||||
self.conditions = conditions
|
||||
self.flight = flight
|
||||
self.doctrine = doctrine
|
||||
self.targets = targets
|
||||
|
||||
@property
|
||||
def is_helo(self) -> bool:
|
||||
return getattr(self.flight.unit_type, "helicopter", False)
|
||||
|
||||
@staticmethod
|
||||
def takeoff(departure: ControlPoint) -> FlightWaypoint:
|
||||
"""Create takeoff waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Note that the takeoff waypoint will automatically be created by pydcs
|
||||
when we create the group, but creating our own before generation makes
|
||||
the planning code simpler.
|
||||
|
||||
Args:
|
||||
departure: Departure airfield or carrier.
|
||||
"""
|
||||
position = departure.position
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TAKEOFF,
|
||||
position.x,
|
||||
position.y,
|
||||
0
|
||||
)
|
||||
waypoint.name = "TAKEOFF"
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Takeoff"
|
||||
waypoint.pretty_name = "Takeoff"
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def land(arrival: ControlPoint) -> FlightWaypoint:
|
||||
"""Create descent waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Args:
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
position = arrival.position
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.LANDING_POINT,
|
||||
position.x,
|
||||
position.y,
|
||||
0
|
||||
)
|
||||
waypoint.name = "LANDING"
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Land"
|
||||
waypoint.pretty_name = "Land"
|
||||
return waypoint
|
||||
|
||||
def hold(self, position: Point) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.LOITER,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
)
|
||||
waypoint.pretty_name = "Hold"
|
||||
waypoint.description = "Wait until push time"
|
||||
waypoint.name = "HOLD"
|
||||
return waypoint
|
||||
|
||||
def join(self, position: Point) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.JOIN,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "Join"
|
||||
waypoint.description = "Rendezvous with package"
|
||||
waypoint.name = "JOIN"
|
||||
return waypoint
|
||||
|
||||
def split(self, position: Point) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.SPLIT,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "Split"
|
||||
waypoint.description = "Depart from package"
|
||||
waypoint.name = "SPLIT"
|
||||
return waypoint
|
||||
|
||||
def ingress_cas(self, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_CAS, position,
|
||||
objective)
|
||||
|
||||
def ingress_escort(self, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_ESCORT, position,
|
||||
objective)
|
||||
|
||||
def ingress_dead(self, position:Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_DEAD, position,
|
||||
objective)
|
||||
|
||||
def ingress_sead(self, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_SEAD, position,
|
||||
objective)
|
||||
|
||||
def ingress_strike(self, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_STRIKE, position,
|
||||
objective)
|
||||
|
||||
def _ingress(self, ingress_type: FlightWaypointType, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
ingress_type,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "INGRESS on " + objective.name
|
||||
waypoint.description = "INGRESS on " + objective.name
|
||||
waypoint.name = "INGRESS"
|
||||
# TODO: This seems wrong, but it's what was there before.
|
||||
waypoint.targets.append(objective)
|
||||
return waypoint
|
||||
|
||||
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.EGRESS,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "EGRESS from " + target.name
|
||||
waypoint.description = "EGRESS from " + target.name
|
||||
waypoint.name = "EGRESS"
|
||||
return waypoint
|
||||
|
||||
def dead_point(self, target: StrikeTarget) -> FlightWaypoint:
|
||||
return self._target_point(target, f"STRIKE {target.name}")
|
||||
|
||||
def sead_point(self, target: StrikeTarget) -> FlightWaypoint:
|
||||
return self._target_point(target, f"STRIKE {target.name}")
|
||||
|
||||
def strike_point(self, target: StrikeTarget) -> FlightWaypoint:
|
||||
return self._target_point(target, f"STRIKE {target.name}")
|
||||
|
||||
@staticmethod
|
||||
def _target_point(target: StrikeTarget, description: str) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
target.target.position.x,
|
||||
target.target.position.y,
|
||||
0
|
||||
)
|
||||
waypoint.description = description
|
||||
waypoint.pretty_name = description
|
||||
waypoint.name = target.name
|
||||
waypoint.alt_type = "RADIO"
|
||||
# The target waypoints are only for the player's benefit. AI tasks for
|
||||
# the target are set on the ingress point so they begin their attack
|
||||
# *before* reaching the target.
|
||||
waypoint.only_for_player = True
|
||||
return waypoint
|
||||
|
||||
def strike_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"STRIKE {target.name}", target)
|
||||
|
||||
def sead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"SEAD on {target.name}", target)
|
||||
|
||||
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"DEAD on {target.name}", target)
|
||||
|
||||
@staticmethod
|
||||
def _target_area(name: str, location: MissionTarget) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
location.position.x,
|
||||
location.position.y,
|
||||
0
|
||||
)
|
||||
waypoint.description = name
|
||||
waypoint.pretty_name = name
|
||||
waypoint.name = name
|
||||
waypoint.alt_type = "RADIO"
|
||||
# The target waypoints are only for the player's benefit. AI tasks for
|
||||
# the target are set on the ingress point so they begin their attack
|
||||
# *before* reaching the target.
|
||||
waypoint.only_for_player = True
|
||||
return waypoint
|
||||
|
||||
def cas(self, position: Point) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.CAS,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else 1000
|
||||
)
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Provide CAS"
|
||||
waypoint.name = "CAS"
|
||||
waypoint.pretty_name = "CAS"
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def race_track_start(position: Point, altitude: int) -> FlightWaypoint:
|
||||
"""Creates a racetrack start waypoint.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack in meters.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
)
|
||||
waypoint.name = "RACETRACK START"
|
||||
waypoint.description = "Orbit between this point and the next point"
|
||||
waypoint.pretty_name = "Race-track start"
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def race_track_end(position: Point, altitude: int) -> FlightWaypoint:
|
||||
"""Creates a racetrack end waypoint.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack in meters.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.PATROL,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
)
|
||||
waypoint.name = "RACETRACK END"
|
||||
waypoint.description = "Orbit between this point and the previous point"
|
||||
waypoint.pretty_name = "Race-track end"
|
||||
return waypoint
|
||||
|
||||
def race_track(self, start: Point, end: Point,
|
||||
altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates two waypoint for a racetrack orbit.
|
||||
|
||||
Args:
|
||||
start: The beginning racetrack waypoint.
|
||||
end: The ending racetrack waypoint.
|
||||
altitude: The racetrack altitude.
|
||||
"""
|
||||
return (self.race_track_start(start, altitude),
|
||||
self.race_track_end(end, altitude))
|
||||
|
||||
def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \
|
||||
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates the waypoints needed to escort the package.
|
||||
|
||||
Args:
|
||||
ingress: The package ingress point.
|
||||
target: The mission target.
|
||||
egress: The package egress point.
|
||||
"""
|
||||
# This would preferably be no points at all, and instead the Escort task
|
||||
# would begin on the join point and end on the split point, however the
|
||||
# escort task does not appear to work properly (see the longer
|
||||
# description in gen.aircraft.JoinPointBuilder), so instead we give
|
||||
# the escort flights a flight plan including the ingress point, target
|
||||
# area, and egress point.
|
||||
ingress = self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
|
||||
target)
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
target.position.x,
|
||||
target.position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.name = "TARGET"
|
||||
waypoint.description = "Escort the package"
|
||||
waypoint.pretty_name = "Target area"
|
||||
|
||||
egress = self.egress(egress, target)
|
||||
return ingress, waypoint, egress
|
||||
55
gen/forcedoptionsgen.py
Normal file
55
gen/forcedoptionsgen.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import logging
|
||||
import typing
|
||||
from enum import IntEnum
|
||||
|
||||
from dcs.mission import Mission
|
||||
from dcs.forcedoptions import ForcedOptions
|
||||
|
||||
from .conflictgen import *
|
||||
|
||||
|
||||
class Labels(IntEnum):
|
||||
Off = 0
|
||||
Full = 1
|
||||
Abbreviated = 2
|
||||
Dot = 3
|
||||
|
||||
|
||||
class ForcedOptionsGenerator:
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game):
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
|
||||
def _set_options_view(self):
|
||||
|
||||
if self.game.settings.map_coalition_visibility == ForcedOptions.Views.All:
|
||||
self.mission.forced_options.options_view = ForcedOptions.Views.All
|
||||
elif self.game.settings.map_coalition_visibility == ForcedOptions.Views.Allies:
|
||||
self.mission.forced_options.options_view = ForcedOptions.Views.Allies
|
||||
elif self.game.settings.map_coalition_visibility == ForcedOptions.Views.OnlyAllies:
|
||||
self.mission.forced_options.options_view = ForcedOptions.Views.OnlyAllies
|
||||
elif self.game.settings.map_coalition_visibility == ForcedOptions.Views.MyAircraft:
|
||||
self.mission.forced_options.options_view = ForcedOptions.Views.MyAircraft
|
||||
elif self.game.settings.map_coalition_visibility == ForcedOptions.Views.OnlyMap:
|
||||
self.mission.forced_options.options_view = ForcedOptions.Views.OnlyMap
|
||||
|
||||
def _set_external_views(self):
|
||||
if not self.game.settings.external_views_allowed:
|
||||
self.mission.forced_options.external_views = self.game.settings.external_views_allowed
|
||||
|
||||
def _set_labels(self):
|
||||
if self.game.settings.labels == "Abbreviated":
|
||||
self.mission.forced_options.labels = int(Labels.Abbreviated)
|
||||
elif self.game.settings.labels == "Dot Only":
|
||||
self.mission.forced_options.labels = int(Labels.Dot)
|
||||
elif self.game.settings.labels == "Off":
|
||||
self.mission.forced_options.labels = int(Labels.Off)
|
||||
|
||||
def generate(self):
|
||||
self._set_options_view()
|
||||
self._set_external_views()
|
||||
self._set_labels()
|
||||
|
||||
|
||||
|
||||
320
gen/ground_forces/ai_ground_planner.py
Normal file
320
gen/ground_forces/ai_ground_planner.py
Normal file
@@ -0,0 +1,320 @@
|
||||
import random
|
||||
from enum import Enum
|
||||
from typing import Dict, List
|
||||
|
||||
from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
import pydcs_extensions.frenchpack.frenchpack as frenchpack
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from theater import ControlPoint
|
||||
|
||||
TYPE_TANKS = [
|
||||
Armor.MBT_T_55,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.MBT_T_80U,
|
||||
Armor.MBT_T_90,
|
||||
Armor.MBT_Leopard_2,
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.MBT_Leclerc,
|
||||
Armor.MBT_Challenger_II,
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.MBT_Merkava_Mk__4,
|
||||
Armor.ZTZ_96B,
|
||||
|
||||
# WW2
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||
Armor.MT_Pz_Kpfw_IV_Ausf_H,
|
||||
Armor.HT_Pz_Kpfw_VI_Tiger_I,
|
||||
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
|
||||
Armor.MT_M4_Sherman,
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
Armor.StuG_IV,
|
||||
Armor.CT_Centaur_IV,
|
||||
Armor.CT_Cromwell_IV,
|
||||
Armor.HIT_Churchill_VII,
|
||||
Armor.LT_Mk_VII_Tetrarch,
|
||||
|
||||
# Mods
|
||||
frenchpack.DIM__TOYOTA_BLUE,
|
||||
frenchpack.DIM__TOYOTA_GREEN,
|
||||
frenchpack.DIM__TOYOTA_DESERT,
|
||||
frenchpack.DIM__KAMIKAZE,
|
||||
|
||||
frenchpack.AMX_10RCR,
|
||||
frenchpack.AMX_10RCR_SEPAR,
|
||||
frenchpack.AMX_30B2,
|
||||
frenchpack.Leclerc_Serie_XXI,
|
||||
|
||||
]
|
||||
|
||||
TYPE_ATGM = [
|
||||
Armor.ATGM_M1045_HMMWV_TOW,
|
||||
Armor.ATGM_M1134_Stryker,
|
||||
Armor.IFV_BMP_2,
|
||||
|
||||
# WW2 (Tank Destroyers)
|
||||
Armor.M30_Cargo_Carrier,
|
||||
Armor.TD_Jagdpanzer_IV,
|
||||
Armor.TD_Jagdpanther_G1,
|
||||
Armor.TD_M10_GMC,
|
||||
|
||||
# Mods
|
||||
frenchpack.VBAE_CRAB_MMP,
|
||||
frenchpack.VAB_MEPHISTO,
|
||||
frenchpack.TRM_2000_PAMELA,
|
||||
|
||||
]
|
||||
|
||||
TYPE_IFV = [
|
||||
Armor.IFV_BMP_3,
|
||||
Armor.IFV_BMP_2,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.IFV_Marder,
|
||||
Armor.IFV_MCV_80,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.ZBD_04A,
|
||||
|
||||
# WW2
|
||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
||||
Armor.LAC_M8_Greyhound,
|
||||
Armor.Daimler_Armoured_Car,
|
||||
|
||||
# Mods
|
||||
frenchpack.ERC_90,
|
||||
frenchpack.VBAE_CRAB,
|
||||
frenchpack.VAB_T20_13
|
||||
|
||||
]
|
||||
|
||||
TYPE_APC = [
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
Armor.APC_M1126_Stryker_ICV,
|
||||
Armor.APC_M113,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.APC_MTLB,
|
||||
Armor.APC_M2A1,
|
||||
Armor.APC_Cobra,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
Armor.APC_AAV_7,
|
||||
Armor.TPz_Fuchs,
|
||||
Armor.ARV_BRDM_2,
|
||||
Armor.ARV_BTR_RD,
|
||||
Armor.FDDM_Grad,
|
||||
|
||||
# WW2
|
||||
Armor.APC_M2A1,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
|
||||
# Mods
|
||||
frenchpack.VAB__50,
|
||||
frenchpack.VBL__50,
|
||||
frenchpack.VBL_AANF1,
|
||||
|
||||
]
|
||||
|
||||
TYPE_ARTILLERY = [
|
||||
Artillery.MLRS_9A52_Smerch,
|
||||
Artillery.SPH_2S1_Gvozdika,
|
||||
Artillery.SPH_2S3_Akatsia,
|
||||
Artillery.MLRS_BM_21_Grad,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27,
|
||||
Artillery.SPH_M109_Paladin,
|
||||
Artillery.MLRS_M270,
|
||||
Artillery.SPH_2S9_Nona,
|
||||
Artillery.SpGH_Dana,
|
||||
Artillery.SPH_2S19_Msta,
|
||||
Artillery.MLRS_FDDM,
|
||||
|
||||
# WW2
|
||||
Artillery.Sturmpanzer_IV_Brummbär,
|
||||
Artillery.M12_GMC
|
||||
]
|
||||
|
||||
TYPE_LOGI = [
|
||||
Unarmed.Transport_M818,
|
||||
Unarmed.Transport_KAMAZ_43101,
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_GAZ_66,
|
||||
Unarmed.Transport_GAZ_3307,
|
||||
Unarmed.Transport_GAZ_3308,
|
||||
Unarmed.Transport_Ural_4320_31_Armored,
|
||||
Unarmed.Transport_Ural_4320T,
|
||||
Unarmed.Blitz_3_6_6700A,
|
||||
Unarmed.Kübelwagen_82,
|
||||
Unarmed.Sd_Kfz_7,
|
||||
Unarmed.Sd_Kfz_2,
|
||||
Unarmed.Willys_MB,
|
||||
Unarmed.Land_Rover_109_S3,
|
||||
Unarmed.Land_Rover_101_FC,
|
||||
|
||||
# Mods
|
||||
frenchpack.VBL,
|
||||
frenchpack.VAB,
|
||||
|
||||
]
|
||||
|
||||
TYPE_INFANTRY = [
|
||||
Infantry.Infantry_Soldier_Insurgents,
|
||||
Infantry.Soldier_AK,
|
||||
Infantry.Infantry_M1_Garand,
|
||||
Infantry.Infantry_Mauser_98,
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
Infantry.Georgian_soldier_with_M4,
|
||||
Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Soldier_M249,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_RPG,
|
||||
]
|
||||
|
||||
MAX_COMBAT_GROUP_PER_CP = 10
|
||||
|
||||
class CombatGroupRole(Enum):
|
||||
TANK = 1
|
||||
APC = 2
|
||||
IFV = 3
|
||||
ARTILLERY = 4
|
||||
SHORAD = 5
|
||||
LOGI = 6
|
||||
INFANTRY = 7
|
||||
ATGM = 8
|
||||
|
||||
|
||||
DISTANCE_FROM_FRONTLINE = {
|
||||
CombatGroupRole.TANK:3200,
|
||||
CombatGroupRole.APC:8000,
|
||||
CombatGroupRole.IFV:3700,
|
||||
CombatGroupRole.ARTILLERY:18000,
|
||||
CombatGroupRole.SHORAD:13000,
|
||||
CombatGroupRole.LOGI:20000,
|
||||
CombatGroupRole.INFANTRY:3000,
|
||||
CombatGroupRole.ATGM:6200
|
||||
}
|
||||
|
||||
GROUP_SIZES_BY_COMBAT_STANCE = {
|
||||
CombatStance.DEFENSIVE: [2, 4, 6],
|
||||
CombatStance.AGGRESSIVE: [2, 4, 6],
|
||||
CombatStance.RETREAT: [2, 4, 6, 8],
|
||||
CombatStance.BREAKTHROUGH: [4, 6, 6, 8],
|
||||
CombatStance.ELIMINATION: [2, 4, 4, 4, 6],
|
||||
CombatStance.AMBUSH: [1, 1, 2, 2, 2, 2, 4]
|
||||
}
|
||||
|
||||
|
||||
class CombatGroup:
|
||||
|
||||
def __init__(self, role: CombatGroupRole):
|
||||
self.units: List[VehicleType] = []
|
||||
self.role = role
|
||||
self.assigned_enemy_cp = None
|
||||
self.start_position = None
|
||||
|
||||
def __str__(self):
|
||||
s = ""
|
||||
s += "ROLE : " + str(self.role) + "\n"
|
||||
if len(self.units) > 0:
|
||||
s += "UNITS " + self.units[0].name + " * " + str(len(self.units))
|
||||
return s
|
||||
|
||||
class GroundPlanner:
|
||||
|
||||
def __init__(self, cp:ControlPoint, game):
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
self.connected_enemy_cp = [cp for cp in self.cp.connected_points if cp.captured != self.cp.captured]
|
||||
self.tank_groups: List[CombatGroup] = []
|
||||
self.apc_group: List[CombatGroup] = []
|
||||
self.ifv_group: List[CombatGroup] = []
|
||||
self.art_group: List[CombatGroup] = []
|
||||
self.atgm_group: List[CombatGroup] = []
|
||||
self.logi_groups: List[CombatGroup] = []
|
||||
self.shorad_groups: List[CombatGroup] = []
|
||||
|
||||
self.units_per_cp: Dict[int, List[CombatGroup]] = {}
|
||||
for cp in self.connected_enemy_cp:
|
||||
self.units_per_cp[cp.id] = []
|
||||
self.reserve: List[CombatGroup] = []
|
||||
|
||||
|
||||
def plan_groundwar(self):
|
||||
|
||||
if hasattr(self.cp, 'stance'):
|
||||
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
|
||||
else:
|
||||
self.cp.stance = CombatStance.DEFENSIVE
|
||||
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
|
||||
|
||||
# Create combat groups and assign them randomly to each enemy CP
|
||||
for key in self.cp.base.armor.keys():
|
||||
|
||||
role = None
|
||||
collection = None
|
||||
if key in TYPE_TANKS:
|
||||
collection = self.tank_groups
|
||||
role = CombatGroupRole.TANK
|
||||
elif key in TYPE_APC:
|
||||
collection = self.apc_group
|
||||
role = CombatGroupRole.APC
|
||||
elif key in TYPE_ARTILLERY:
|
||||
collection = self.art_group
|
||||
role = CombatGroupRole.ARTILLERY
|
||||
elif key in TYPE_IFV:
|
||||
collection = self.ifv_group
|
||||
role = CombatGroupRole.IFV
|
||||
elif key in TYPE_LOGI:
|
||||
collection = self.logi_groups
|
||||
role = CombatGroupRole.LOGI
|
||||
elif key in TYPE_ATGM:
|
||||
collection = self.atgm_group
|
||||
role = CombatGroupRole.ATGM
|
||||
else:
|
||||
print("Warning unit type not handled by ground generator")
|
||||
print(key)
|
||||
continue
|
||||
|
||||
available = self.cp.base.armor[key]
|
||||
while available > 0:
|
||||
n = random.choice(group_size_choice)
|
||||
if n > available:
|
||||
if available >= 2:
|
||||
n = 2
|
||||
else:
|
||||
n = 1
|
||||
available -= n
|
||||
|
||||
group = CombatGroup(role)
|
||||
if len(self.connected_enemy_cp) > 0:
|
||||
enemy_cp = random.choice(self.connected_enemy_cp).id
|
||||
self.units_per_cp[enemy_cp].append(group)
|
||||
group.assigned_enemy_cp = enemy_cp
|
||||
else:
|
||||
self.reserve.append(group)
|
||||
group.assigned_enemy_cp = "__reserve__"
|
||||
|
||||
for i in range(n):
|
||||
group.units.append(key)
|
||||
collection.append(group)
|
||||
|
||||
print("------------------")
|
||||
print("Ground Planner : ")
|
||||
print(self.cp.name)
|
||||
print("------------------")
|
||||
for key in self.units_per_cp.keys():
|
||||
print("For : #" + str(key))
|
||||
for group in self.units_per_cp[key]:
|
||||
print(str(group))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
11
gen/ground_forces/combat_stance.py
Normal file
11
gen/ground_forces/combat_stance.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class CombatStance(Enum):
|
||||
DEFENSIVE = 0 # Unit will adopt defensive stance with medium group of units
|
||||
AGGRESSIVE = 1 # Unit will attempt to make progress with medium sized group of units
|
||||
RETREAT = 2 # Unit will retreat
|
||||
BREAKTHROUGH = 3 # Unit will attempt a breakthrough, rushing forward very aggresively with big group of armored units, and even less armored units will move aggresively
|
||||
ELIMINATION = 4 # Unit will progress aggresively toward anemy units, attempting to eliminate the ennemy force
|
||||
AMBUSH = 5 # Units will adopt a defensive stance a bit different from 'DEFENSIVE', ATGM & INFANTRY with RPG will be located on frontline with the armored units. (The groups of units will be smaller)
|
||||
|
||||
421
gen/groundobjectsgen.py
Normal file
421
gen/groundobjectsgen.py
Normal file
@@ -0,0 +1,421 @@
|
||||
"""Generators for creating the groups for ground objectives.
|
||||
|
||||
The classes in this file are responsible for creating the vehicle groups, ship
|
||||
groups, statics, missile sites, and AA sites for the mission. Each of these
|
||||
objectives is defined in the Theater by a TheaterGroundObject. These classes
|
||||
create the pydcs groups and statics for those areas and add them to the mission.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from typing import Dict, Iterator, Optional, TYPE_CHECKING
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.country import Country
|
||||
from dcs.statics import fortification_map, warehouse_map
|
||||
from dcs.task import (
|
||||
ActivateBeaconCommand,
|
||||
ActivateICLSCommand,
|
||||
EPLRS,
|
||||
OptAlarmState,
|
||||
)
|
||||
from dcs.unit import Ship, Vehicle, Unit
|
||||
from dcs.unitgroup import Group, ShipGroup, StaticGroup
|
||||
from dcs.unittype import StaticType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
|
||||
from game.db import unit_type_from_name
|
||||
from theater import ControlPoint, TheaterGroundObject
|
||||
from theater.theatergroundobject import (
|
||||
BuildingGroundObject, CarrierGroundObject,
|
||||
GenericCarrierGroundObject,
|
||||
LhaGroundObject, ShipGroundObject,
|
||||
)
|
||||
from .conflictgen import Conflict
|
||||
from .radios import RadioFrequency, RadioRegistry
|
||||
from .runways import RunwayData
|
||||
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
FARP_FRONTLINE_DISTANCE = 10000
|
||||
AA_CP_MIN_DISTANCE = 40000
|
||||
|
||||
|
||||
class GenericGroundObjectGenerator:
|
||||
"""An unspecialized ground object generator.
|
||||
|
||||
Currently used only for SAM and missile (V1/V2) sites.
|
||||
"""
|
||||
def __init__(self, ground_object: TheaterGroundObject, country: Country,
|
||||
game: Game, mission: Mission) -> None:
|
||||
self.ground_object = ground_object
|
||||
self.country = country
|
||||
self.game = game
|
||||
self.m = mission
|
||||
|
||||
def generate(self) -> None:
|
||||
if self.game.position_culled(self.ground_object.position):
|
||||
return
|
||||
|
||||
for group in self.ground_object.groups:
|
||||
if not group.units:
|
||||
logging.warning(f"Found empty group in {self.ground_object}")
|
||||
continue
|
||||
|
||||
unit_type = unit_type_from_name(group.units[0].type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(
|
||||
f"Unrecognized unit type: {group.units[0].type}")
|
||||
|
||||
vg = self.m.vehicle_group(self.country, group.name, unit_type,
|
||||
position=group.position,
|
||||
heading=group.units[0].heading)
|
||||
vg.units[0].name = self.m.string(group.units[0].name)
|
||||
vg.units[0].player_can_drive = True
|
||||
for i, u in enumerate(group.units):
|
||||
if i > 0:
|
||||
vehicle = Vehicle(self.m.next_unit_id(),
|
||||
self.m.string(u.name), u.type)
|
||||
vehicle.position.x = u.position.x
|
||||
vehicle.position.y = u.position.y
|
||||
vehicle.heading = u.heading
|
||||
vehicle.player_can_drive = True
|
||||
vg.add_unit(vehicle)
|
||||
|
||||
self.enable_eplrs(vg, unit_type)
|
||||
self.set_alarm_state(vg)
|
||||
|
||||
@staticmethod
|
||||
def enable_eplrs(group: Group, unit_type: UnitType) -> None:
|
||||
if hasattr(unit_type, 'eplrs'):
|
||||
if unit_type.eplrs:
|
||||
group.points[0].tasks.append(EPLRS(group.id))
|
||||
|
||||
def set_alarm_state(self, group: Group) -> None:
|
||||
if self.game.settings.perf_red_alert_state:
|
||||
group.points[0].tasks.append(OptAlarmState(2))
|
||||
else:
|
||||
group.points[0].tasks.append(OptAlarmState(1))
|
||||
|
||||
|
||||
class BuildingSiteGenerator(GenericGroundObjectGenerator):
|
||||
"""Generator for building sites.
|
||||
|
||||
Building sites are the primary type of non-airbase objective locations that
|
||||
appear on the map. They come in a handful of variants each with different
|
||||
types of buildings and ground units.
|
||||
"""
|
||||
|
||||
def generate(self) -> None:
|
||||
if self.game.position_culled(self.ground_object.position):
|
||||
return
|
||||
|
||||
if self.ground_object.dcs_identifier in warehouse_map:
|
||||
static_type = warehouse_map[self.ground_object.dcs_identifier]
|
||||
self.generate_static(static_type)
|
||||
elif self.ground_object.dcs_identifier in fortification_map:
|
||||
static_type = fortification_map[self.ground_object.dcs_identifier]
|
||||
self.generate_static(static_type)
|
||||
elif self.ground_object.dcs_identifier in FORTIFICATION_UNITS_ID:
|
||||
for f in FORTIFICATION_UNITS:
|
||||
if f.id == self.ground_object.dcs_identifier:
|
||||
unit_type = f
|
||||
self.generate_vehicle_group(unit_type)
|
||||
break
|
||||
else:
|
||||
logging.error(
|
||||
f"{self.ground_object.dcs_identifier} not found in static maps")
|
||||
|
||||
def generate_vehicle_group(self, unit_type: UnitType) -> None:
|
||||
if not self.ground_object.is_dead:
|
||||
self.m.vehicle_group(
|
||||
country=self.country,
|
||||
name=self.ground_object.group_name,
|
||||
_type=unit_type,
|
||||
position=self.ground_object.position,
|
||||
heading=self.ground_object.heading,
|
||||
)
|
||||
|
||||
def generate_static(self, static_type: StaticType) -> None:
|
||||
self.m.static_group(
|
||||
country=self.country,
|
||||
name=self.ground_object.group_name,
|
||||
_type=static_type,
|
||||
position=self.ground_object.position,
|
||||
heading=self.ground_object.heading,
|
||||
dead=self.ground_object.is_dead,
|
||||
)
|
||||
|
||||
|
||||
class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
"""Base type for carrier group generation.
|
||||
|
||||
Used by both CV(N) groups and LHA groups.
|
||||
"""
|
||||
def __init__(self, ground_object: GenericCarrierGroundObject,
|
||||
control_point: ControlPoint, country: Country, game: Game,
|
||||
mission: Mission, radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry, icls_alloc: Iterator[int],
|
||||
runways: Dict[str, RunwayData]) -> None:
|
||||
super().__init__(ground_object, country, game, mission)
|
||||
self.ground_object = ground_object
|
||||
self.control_point = control_point
|
||||
self.radio_registry = radio_registry
|
||||
self.tacan_registry = tacan_registry
|
||||
self.icls_alloc = icls_alloc
|
||||
self.runways = runways
|
||||
|
||||
def generate(self) -> None:
|
||||
# TODO: Require single group?
|
||||
for group in self.ground_object.groups:
|
||||
if not group.units:
|
||||
logging.warning(
|
||||
f"Found empty carrier group in {self.control_point}")
|
||||
continue
|
||||
|
||||
atc = self.radio_registry.alloc_uhf()
|
||||
ship_group = self.configure_carrier(group, atc)
|
||||
for unit in group.units[1:]:
|
||||
ship_group.add_unit(self.create_ship(unit, atc))
|
||||
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.X)
|
||||
tacan_callsign = self.tacan_callsign()
|
||||
icls = next(self.icls_alloc)
|
||||
|
||||
brc = self.steam_into_wind(ship_group)
|
||||
self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
|
||||
self.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls)
|
||||
|
||||
def get_carrier_type(self, group: Group) -> UnitType:
|
||||
unit_type = unit_type_from_name(group.units[0].type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(
|
||||
f"Unrecognized carrier name: {group.units[0].type}")
|
||||
return unit_type
|
||||
|
||||
def configure_carrier(self, group: Group,
|
||||
atc_channel: RadioFrequency) -> ShipGroup:
|
||||
unit_type = self.get_carrier_type(group)
|
||||
|
||||
ship_group = self.m.ship_group(self.country, group.name, unit_type,
|
||||
position=group.position,
|
||||
heading=group.units[0].heading)
|
||||
ship_group.set_frequency(atc_channel.hertz)
|
||||
ship_group.units[0].name = self.m.string(group.units[0].name)
|
||||
return ship_group
|
||||
|
||||
def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship:
|
||||
ship = Ship(self.m.next_unit_id(),
|
||||
self.m.string(unit.name),
|
||||
unit_type_from_name(unit.type))
|
||||
ship.position.x = unit.position.x
|
||||
ship.position.y = unit.position.y
|
||||
ship.heading = unit.heading
|
||||
# TODO: Verify.
|
||||
ship.set_frequency(atc_channel.hertz)
|
||||
return ship
|
||||
|
||||
def steam_into_wind(self, group: ShipGroup) -> Optional[int]:
|
||||
brc = self.m.weather.wind_at_ground.direction + 180
|
||||
for attempt in range(5):
|
||||
point = group.points[0].position.point_from_heading(
|
||||
brc, 100000 - attempt * 20000)
|
||||
if self.game.theater.is_in_sea(point):
|
||||
group.add_waypoint(point)
|
||||
return brc
|
||||
return None
|
||||
|
||||
def tacan_callsign(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def activate_beacons(group: ShipGroup, tacan: TacanChannel,
|
||||
callsign: str, icls: int) -> None:
|
||||
group.points[0].tasks.append(ActivateBeaconCommand(
|
||||
channel=tacan.number,
|
||||
modechannel=tacan.band.value,
|
||||
callsign=callsign,
|
||||
unit_id=group.units[0].id,
|
||||
aa=False
|
||||
))
|
||||
group.points[0].tasks.append(ActivateICLSCommand(
|
||||
icls, unit_id=group.units[0].id
|
||||
))
|
||||
|
||||
def add_runway_data(self, brc: int, atc: RadioFrequency,
|
||||
tacan: TacanChannel, callsign: str, icls: int) -> None:
|
||||
# TODO: Make unit name usable.
|
||||
# This relies on one control point mapping exactly
|
||||
# to one LHA, carrier, or other usable "runway".
|
||||
# This isn't wholly true, since the DD escorts of
|
||||
# the carrier group are valid for helicopters, but
|
||||
# they aren't exposed as such to the game. Should
|
||||
# clean this up so that's possible. We can't use the
|
||||
# unit name since it's an arbitrary ID.
|
||||
self.runways[self.control_point.name] = RunwayData(
|
||||
self.control_point.name,
|
||||
brc,
|
||||
"N/A",
|
||||
atc=atc,
|
||||
tacan=tacan,
|
||||
tacan_callsign=callsign,
|
||||
icls=icls,
|
||||
)
|
||||
|
||||
|
||||
class CarrierGenerator(GenericCarrierGenerator):
|
||||
"""Generator for CV(N) groups."""
|
||||
|
||||
def get_carrier_type(self, group: Group) -> UnitType:
|
||||
unit_type = super().get_carrier_type(group)
|
||||
if self.game.settings.supercarrier:
|
||||
unit_type = db.upgrade_to_supercarrier(unit_type,
|
||||
self.control_point.name)
|
||||
return unit_type
|
||||
|
||||
def tacan_callsign(self) -> str:
|
||||
# TODO: Assign these properly.
|
||||
return random.choice([
|
||||
"STE",
|
||||
"CVN",
|
||||
"CVH",
|
||||
"CCV",
|
||||
"ACC",
|
||||
"ARC",
|
||||
"GER",
|
||||
"ABR",
|
||||
"LIN",
|
||||
"TRU",
|
||||
])
|
||||
|
||||
|
||||
class LhaGenerator(GenericCarrierGenerator):
|
||||
"""Generator for LHA groups."""
|
||||
|
||||
def tacan_callsign(self) -> str:
|
||||
# TODO: Assign these properly.
|
||||
return random.choice([
|
||||
"LHD",
|
||||
"LHA",
|
||||
"LHB",
|
||||
"LHC",
|
||||
"LHD",
|
||||
"LDS",
|
||||
])
|
||||
|
||||
|
||||
class ShipObjectGenerator(GenericGroundObjectGenerator):
|
||||
"""Generator for non-carrier naval groups."""
|
||||
|
||||
def generate(self) -> None:
|
||||
if self.game.position_culled(self.ground_object.position):
|
||||
return
|
||||
|
||||
for group in self.ground_object.groups:
|
||||
if not group.units:
|
||||
logging.warning(f"Found empty group in {self.ground_object}")
|
||||
continue
|
||||
|
||||
unit_type = unit_type_from_name(group.units[0].type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(
|
||||
f"Unrecognized unit type: {group.units[0].type}")
|
||||
|
||||
self.generate_group(group, unit_type)
|
||||
|
||||
def generate_group(self, group_def: Group, unit_type: UnitType):
|
||||
group = self.m.ship_group(self.country, group_def.name, unit_type,
|
||||
position=group_def.position,
|
||||
heading=group_def.units[0].heading)
|
||||
group.units[0].name = self.m.string(group_def.units[0].name)
|
||||
# TODO: Skipping the first unit looks like copy pasta from the carrier.
|
||||
for unit in group_def.units[1:]:
|
||||
unit_type = unit_type_from_name(unit.type)
|
||||
ship = Ship(self.m.next_unit_id(),
|
||||
self.m.string(unit.name), unit_type)
|
||||
ship.position.x = unit.position.x
|
||||
ship.position.y = unit.position.y
|
||||
ship.heading = unit.heading
|
||||
group.add_unit(ship)
|
||||
self.set_alarm_state(group)
|
||||
|
||||
|
||||
class GroundObjectsGenerator:
|
||||
"""Creates DCS groups and statics for the theater during mission generation.
|
||||
|
||||
Most of the work of group/static generation is delegated to the other
|
||||
generator classes. This class is responsible for finding each of the
|
||||
locations for spawning ground objects, determining their types, and creating
|
||||
the appropriate generators.
|
||||
"""
|
||||
FARP_CAPACITY = 4
|
||||
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game,
|
||||
radio_registry: RadioRegistry, tacan_registry: TacanRegistry):
|
||||
self.m = mission
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
self.radio_registry = radio_registry
|
||||
self.tacan_registry = tacan_registry
|
||||
self.icls_alloc = iter(range(1, 21))
|
||||
self.runways: Dict[str, RunwayData] = {}
|
||||
|
||||
def generate_farps(self, number_of_units=1) -> Iterator[StaticGroup]:
|
||||
if self.conflict.is_vector:
|
||||
center = self.conflict.center
|
||||
heading = self.conflict.heading - 90
|
||||
else:
|
||||
center, heading = self.conflict.frontline_position(self.conflict.theater, self.conflict.from_cp, self.conflict.to_cp)
|
||||
heading -= 90
|
||||
|
||||
initial_position = center.point_from_heading(heading, FARP_FRONTLINE_DISTANCE)
|
||||
position = self.conflict.find_ground_position(initial_position, heading)
|
||||
if not position:
|
||||
position = initial_position
|
||||
|
||||
for i, _ in enumerate(range(0, number_of_units, self.FARP_CAPACITY)):
|
||||
position = position.point_from_heading(0, i * 275)
|
||||
|
||||
yield self.m.farp(
|
||||
country=self.m.country(self.game.player_country),
|
||||
name="FARP",
|
||||
position=position,
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.captured:
|
||||
country_name = self.game.player_country
|
||||
else:
|
||||
country_name = self.game.enemy_country
|
||||
country = self.m.country(country_name)
|
||||
|
||||
for ground_object in cp.ground_objects:
|
||||
if isinstance(ground_object, BuildingGroundObject):
|
||||
generator = BuildingSiteGenerator(ground_object, country,
|
||||
self.game, self.m)
|
||||
elif isinstance(ground_object, CarrierGroundObject):
|
||||
generator = CarrierGenerator(ground_object, cp, country,
|
||||
self.game, self.m,
|
||||
self.radio_registry,
|
||||
self.tacan_registry,
|
||||
self.icls_alloc, self.runways)
|
||||
elif isinstance(ground_object, LhaGroundObject):
|
||||
generator = CarrierGenerator(ground_object, cp, country,
|
||||
self.game, self.m,
|
||||
self.radio_registry,
|
||||
self.tacan_registry,
|
||||
self.icls_alloc, self.runways)
|
||||
elif isinstance(ground_object, ShipGroundObject):
|
||||
generator = ShipObjectGenerator(ground_object, country,
|
||||
self.game, self.m)
|
||||
else:
|
||||
generator = GenericGroundObjectGenerator(ground_object,
|
||||
country, self.game,
|
||||
self.m)
|
||||
generator.generate()
|
||||
359
gen/kneeboard.py
Normal file
359
gen/kneeboard.py
Normal file
@@ -0,0 +1,359 @@
|
||||
"""Generates kneeboard pages relevant to the player's mission.
|
||||
|
||||
The player kneeboard includes the following information:
|
||||
|
||||
* Airfield (departure, arrival, divert) info.
|
||||
* Flight plan (waypoint numbers, names, altitudes).
|
||||
* Comm channels.
|
||||
* AWACS info.
|
||||
* Tanker info.
|
||||
* JTAC info.
|
||||
|
||||
Things we should add:
|
||||
|
||||
* Flight plan ToT and fuel ladder (current have neither available).
|
||||
* Support for planning an arrival/divert airfield separate from departure.
|
||||
* Mission package infrastructure to include information about the larger
|
||||
mission, i.e. information about the escort flight for a strike package.
|
||||
* Target information. Steerpoints, preplanned objectives, ToT, etc.
|
||||
|
||||
For multiplayer missions, a kneeboard will be generated per flight.
|
||||
https://forums.eagle.ru/showthread.php?t=206360 claims that kneeboard pages can
|
||||
only be added per airframe, so PvP missions where each side have the same
|
||||
aircraft will be able to see the enemy's kneeboard for the same airframe.
|
||||
"""
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from dcs.mission import Mission
|
||||
from dcs.unittype import FlyingType
|
||||
from tabulate import tabulate
|
||||
|
||||
from game.utils import meter_to_nm
|
||||
from . import units
|
||||
from .aircraft import AIRCRAFT_DATA, FlightData
|
||||
from .airsupportgen import AwacsInfo, TankerInfo
|
||||
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
|
||||
from .flights.flight import FlightWaypoint, FlightWaypointType
|
||||
from .radios import RadioFrequency
|
||||
from .runways import RunwayData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
class KneeboardPageWriter:
|
||||
"""Creates kneeboard images."""
|
||||
|
||||
def __init__(self, page_margin: int = 24, line_spacing: int = 12) -> None:
|
||||
self.image = Image.new('RGB', (768, 1024), (0xff, 0xff, 0xff))
|
||||
# These font sizes create a relatively full page for current sorties. If
|
||||
# we start generating more complicated flight plans, or start including
|
||||
# more information in the comm ladder (the latter of which we should
|
||||
# probably do), we'll need to split some of this information off into a
|
||||
# second page.
|
||||
self.title_font = ImageFont.truetype("arial.ttf", 32)
|
||||
self.heading_font = ImageFont.truetype("arial.ttf", 24)
|
||||
self.content_font = ImageFont.truetype("arial.ttf", 20)
|
||||
self.table_font = ImageFont.truetype(
|
||||
"resources/fonts/Inconsolata.otf", 20)
|
||||
self.draw = ImageDraw.Draw(self.image)
|
||||
self.x = page_margin
|
||||
self.y = page_margin
|
||||
self.line_spacing = line_spacing
|
||||
|
||||
@property
|
||||
def position(self) -> Tuple[int, int]:
|
||||
return self.x, self.y
|
||||
|
||||
def text(self, text: str, font=None,
|
||||
fill: Tuple[int, int, int] = (0, 0, 0)) -> None:
|
||||
if font is None:
|
||||
font = self.content_font
|
||||
|
||||
self.draw.text(self.position, text, font=font, fill=fill)
|
||||
width, height = self.draw.textsize(text, font=font)
|
||||
self.y += height + self.line_spacing
|
||||
|
||||
def title(self, title: str) -> None:
|
||||
self.text(title, font=self.title_font)
|
||||
|
||||
def heading(self, text: str) -> None:
|
||||
self.text(text, font=self.heading_font)
|
||||
|
||||
def table(self, cells: List[List[str]],
|
||||
headers: Optional[List[str]] = None) -> None:
|
||||
if headers is None:
|
||||
headers = []
|
||||
table = tabulate(cells, headers=headers, numalign="right")
|
||||
self.text(table, font=self.table_font)
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
self.image.save(path)
|
||||
|
||||
|
||||
class KneeboardPage:
|
||||
"""Base class for all kneeboard pages."""
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
"""Writes the kneeboard page to the given path."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NumberedWaypoint:
|
||||
number: int
|
||||
waypoint: FlightWaypoint
|
||||
|
||||
|
||||
class FlightPlanBuilder:
|
||||
def __init__(self, start_time: datetime.datetime) -> None:
|
||||
self.start_time = start_time
|
||||
self.rows: List[List[str]] = []
|
||||
self.target_points: List[NumberedWaypoint] = []
|
||||
self.last_waypoint: Optional[FlightWaypoint] = None
|
||||
|
||||
def add_waypoint(self, waypoint_num: int, waypoint: FlightWaypoint) -> None:
|
||||
if waypoint.waypoint_type == FlightWaypointType.TARGET_POINT:
|
||||
self.target_points.append(NumberedWaypoint(waypoint_num, waypoint))
|
||||
return
|
||||
|
||||
if self.target_points:
|
||||
self.coalesce_target_points()
|
||||
self.target_points = []
|
||||
|
||||
self.add_waypoint_row(NumberedWaypoint(waypoint_num, waypoint))
|
||||
self.last_waypoint = waypoint
|
||||
|
||||
def coalesce_target_points(self) -> None:
|
||||
if len(self.target_points) <= 4:
|
||||
for steerpoint in self.target_points:
|
||||
self.add_waypoint_row(steerpoint)
|
||||
return
|
||||
|
||||
first_waypoint_num = self.target_points[0].number
|
||||
last_waypoint_num = self.target_points[-1].number
|
||||
|
||||
self.rows.append([
|
||||
f"{first_waypoint_num}-{last_waypoint_num}",
|
||||
"Target points",
|
||||
"0",
|
||||
self._waypoint_distance(self.target_points[0].waypoint),
|
||||
self._ground_speed(self.target_points[0].waypoint),
|
||||
self._format_time(self.target_points[0].waypoint.tot),
|
||||
self._format_time(self.target_points[0].waypoint.departure_time),
|
||||
])
|
||||
self.last_waypoint = self.target_points[-1].waypoint
|
||||
|
||||
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
|
||||
self.rows.append([
|
||||
str(waypoint.number),
|
||||
waypoint.waypoint.pretty_name,
|
||||
str(int(units.meters_to_feet(waypoint.waypoint.alt))),
|
||||
self._waypoint_distance(waypoint.waypoint),
|
||||
self._ground_speed(waypoint.waypoint),
|
||||
self._format_time(waypoint.waypoint.tot),
|
||||
self._format_time(waypoint.waypoint.departure_time),
|
||||
])
|
||||
|
||||
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
|
||||
if time is None:
|
||||
return ""
|
||||
local_time = self.start_time + time
|
||||
return local_time.strftime(f"%H:%M:%S")
|
||||
|
||||
def _waypoint_distance(self, waypoint: FlightWaypoint) -> str:
|
||||
if self.last_waypoint is None:
|
||||
return "-"
|
||||
|
||||
distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
|
||||
waypoint.position
|
||||
))
|
||||
return f"{distance} NM"
|
||||
|
||||
def _ground_speed(self, waypoint: FlightWaypoint) -> str:
|
||||
if self.last_waypoint is None:
|
||||
return "-"
|
||||
|
||||
if waypoint.tot is None:
|
||||
return "-"
|
||||
|
||||
if self.last_waypoint.departure_time is not None:
|
||||
last_time = self.last_waypoint.departure_time
|
||||
elif self.last_waypoint.tot is not None:
|
||||
last_time = self.last_waypoint.tot
|
||||
else:
|
||||
return "-"
|
||||
|
||||
distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
|
||||
waypoint.position
|
||||
))
|
||||
duration = (waypoint.tot - last_time).total_seconds() / 3600
|
||||
return f"{int(distance / duration)} kt"
|
||||
|
||||
def build(self) -> List[List[str]]:
|
||||
return self.rows
|
||||
|
||||
|
||||
class BriefingPage(KneeboardPage):
|
||||
"""A kneeboard page containing briefing information."""
|
||||
def __init__(self, flight: FlightData, comms: List[CommInfo],
|
||||
awacs: List[AwacsInfo], tankers: List[TankerInfo],
|
||||
jtacs: List[JtacInfo], start_time: datetime.datetime) -> None:
|
||||
self.flight = flight
|
||||
self.comms = list(comms)
|
||||
self.awacs = awacs
|
||||
self.tankers = tankers
|
||||
self.jtacs = jtacs
|
||||
self.start_time = start_time
|
||||
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
writer = KneeboardPageWriter()
|
||||
writer.title(f"{self.flight.callsign} Mission Info")
|
||||
|
||||
# TODO: Handle carriers.
|
||||
writer.heading("Airfield Info")
|
||||
writer.table([
|
||||
self.airfield_info_row("Departure", self.flight.departure),
|
||||
self.airfield_info_row("Arrival", self.flight.arrival),
|
||||
self.airfield_info_row("Divert", self.flight.divert),
|
||||
], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"])
|
||||
|
||||
writer.heading("Flight Plan")
|
||||
flight_plan_builder = FlightPlanBuilder(self.start_time)
|
||||
for num, waypoint in enumerate(self.flight.waypoints):
|
||||
flight_plan_builder.add_waypoint(num, waypoint)
|
||||
writer.table(flight_plan_builder.build(), headers=[
|
||||
"#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"
|
||||
])
|
||||
|
||||
writer.heading("Comm Ladder")
|
||||
comms = []
|
||||
for comm in self.comms:
|
||||
comms.append([comm.name, self.format_frequency(comm.freq)])
|
||||
writer.table(comms, headers=["Name", "UHF"])
|
||||
|
||||
writer.heading("AWACS")
|
||||
awacs = []
|
||||
for a in self.awacs:
|
||||
awacs.append([a.callsign, self.format_frequency(a.freq)])
|
||||
writer.table(awacs, headers=["Callsign", "UHF"])
|
||||
|
||||
writer.heading("Tankers")
|
||||
tankers = []
|
||||
for tanker in self.tankers:
|
||||
tankers.append([
|
||||
tanker.callsign,
|
||||
tanker.variant,
|
||||
str(tanker.tacan),
|
||||
self.format_frequency(tanker.freq),
|
||||
])
|
||||
writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"])
|
||||
|
||||
writer.heading("JTAC")
|
||||
jtacs = []
|
||||
for jtac in self.jtacs:
|
||||
jtacs.append([jtac.callsign, jtac.region, jtac.code])
|
||||
writer.table(jtacs, headers=["Callsign", "Region", "Laser Code"])
|
||||
|
||||
writer.write(path)
|
||||
|
||||
def airfield_info_row(self, row_title: str,
|
||||
runway: Optional[RunwayData]) -> List[str]:
|
||||
"""Creates a table row for a given airfield.
|
||||
|
||||
Args:
|
||||
row_title: Purpose of the airfield. e.g. "Departure", "Arrival" or
|
||||
"Divert".
|
||||
runway: The runway described by this row.
|
||||
|
||||
Returns:
|
||||
A list of strings to be used as a row of the airfield table.
|
||||
"""
|
||||
if runway is None:
|
||||
return [row_title, "", "", "", "", ""]
|
||||
|
||||
atc = ""
|
||||
if runway.atc is not None:
|
||||
atc = self.format_frequency(runway.atc)
|
||||
if runway.tacan is None:
|
||||
tacan = ""
|
||||
else:
|
||||
tacan = str(runway.tacan)
|
||||
if runway.ils is not None:
|
||||
ils = str(runway.ils)
|
||||
elif runway.icls is not None:
|
||||
ils = str(runway.icls)
|
||||
else:
|
||||
ils = ""
|
||||
return [
|
||||
row_title,
|
||||
runway.airfield_name,
|
||||
atc,
|
||||
tacan,
|
||||
ils,
|
||||
runway.runway_name,
|
||||
]
|
||||
|
||||
def format_frequency(self, frequency: RadioFrequency) -> str:
|
||||
channel = self.flight.channel_for(frequency)
|
||||
if channel is None:
|
||||
return str(frequency)
|
||||
|
||||
namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer
|
||||
channel_name = namer.channel_name(channel.radio_id, channel.channel)
|
||||
return f"{channel_name} {frequency}"
|
||||
|
||||
|
||||
class KneeboardGenerator(MissionInfoGenerator):
|
||||
"""Creates kneeboard pages for each client flight in the mission."""
|
||||
|
||||
def __init__(self, mission: Mission, game: "Game") -> None:
|
||||
super().__init__(mission, game)
|
||||
|
||||
def generate(self) -> None:
|
||||
"""Generates a kneeboard per client flight."""
|
||||
temp_dir = Path("kneeboards")
|
||||
temp_dir.mkdir(exist_ok=True)
|
||||
for aircraft, pages in self.pages_by_airframe().items():
|
||||
aircraft_dir = temp_dir / aircraft.id
|
||||
aircraft_dir.mkdir(exist_ok=True)
|
||||
for idx, page in enumerate(pages):
|
||||
page_path = aircraft_dir / f"page{idx:02}.png"
|
||||
page.write(page_path)
|
||||
self.mission.add_aircraft_kneeboard(aircraft, page_path)
|
||||
|
||||
def pages_by_airframe(self) -> Dict[FlyingType, List[KneeboardPage]]:
|
||||
"""Returns a list of kneeboard pages per airframe in the mission.
|
||||
|
||||
Only client flights will be included, but because DCS does not support
|
||||
group-specific kneeboard pages, flights (possibly from opposing sides)
|
||||
will be able to see the kneeboards of all aircraft of the same type.
|
||||
|
||||
Returns:
|
||||
A dict mapping aircraft types to the list of kneeboard pages for
|
||||
that aircraft.
|
||||
"""
|
||||
all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list)
|
||||
for flight in self.flights:
|
||||
if not flight.client_units:
|
||||
continue
|
||||
all_flights[flight.aircraft_type].extend(
|
||||
self.generate_flight_kneeboard(flight))
|
||||
return all_flights
|
||||
|
||||
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
|
||||
"""Returns a list of kneeboard pages for the given flight."""
|
||||
return [
|
||||
BriefingPage(
|
||||
flight,
|
||||
self.comms,
|
||||
self.awacs,
|
||||
self.tankers,
|
||||
self.jtacs,
|
||||
self.mission.start_time
|
||||
),
|
||||
]
|
||||
22
gen/locations/preset_control_point_locations.py
Normal file
22
gen/locations/preset_control_point_locations.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from typing import List
|
||||
|
||||
from gen.locations.preset_locations import PresetLocation
|
||||
|
||||
|
||||
@dataclass
|
||||
class PresetControlPointLocations:
|
||||
"""A repository of preset locations for a given control point"""
|
||||
|
||||
# List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7)
|
||||
ashore_locations: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
# List of possible offshore locations to generate ship groups (Represented in miz file by an Oliver Hazard Perry)
|
||||
offshore_locations: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
# Possible antiship missiles sites locations (Represented in miz file by Iranian Silkworm missiles)
|
||||
antiship_locations: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
# List of possible powerplants locations (Represented in miz file by static Workshop A object, USA)
|
||||
powerplant_locations: List[PresetLocation] = field(default_factory=list)
|
||||
59
gen/locations/preset_location_finder.py
Normal file
59
gen/locations/preset_location_finder.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from dcs import Mission, ships
|
||||
from dcs.vehicles import MissilesSS
|
||||
|
||||
from gen.locations.preset_control_point_locations import PresetControlPointLocations
|
||||
from gen.locations.preset_locations import PresetLocation
|
||||
|
||||
|
||||
class PresetLocationFinder:
|
||||
|
||||
@staticmethod
|
||||
def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations:
|
||||
"""
|
||||
Extract the list of preset locations from miz data
|
||||
:param terrain_name: Terrain/Map name
|
||||
:param cp_name: Control Point / Airbase name
|
||||
:return:
|
||||
"""
|
||||
|
||||
miz_file = Path("./resources/mizdata/", terrain_name.lower(), cp_name + ".miz")
|
||||
|
||||
offshore_locations: List[PresetLocation] = []
|
||||
ashore_locations: List[PresetLocation] = []
|
||||
powerplants_locations: List[PresetLocation] = []
|
||||
antiship_locations: List[PresetLocation] = []
|
||||
|
||||
if miz_file.exists():
|
||||
m = Mission()
|
||||
m.load_file(miz_file.absolute())
|
||||
|
||||
for vehicle_group in m.country("USA").vehicle_group:
|
||||
if len(vehicle_group.units) > 0:
|
||||
ashore_locations.append(PresetLocation(vehicle_group.position,
|
||||
vehicle_group.units[0].heading,
|
||||
vehicle_group.name))
|
||||
|
||||
for ship_group in m.country("USA").ship_group:
|
||||
if len(ship_group.units) > 0 and ship_group.units[0].type == ships.Oliver_Hazzard_Perry_class.id:
|
||||
offshore_locations.append(PresetLocation(ship_group.position,
|
||||
ship_group.units[0].heading,
|
||||
ship_group.name))
|
||||
|
||||
for static_group in m.country("USA").static_group:
|
||||
if len(static_group.units) > 0:
|
||||
powerplants_locations.append(PresetLocation(static_group.position,
|
||||
static_group.units[0].heading,
|
||||
static_group.name))
|
||||
|
||||
if m.country("Iran") is not None:
|
||||
for vehicle_group in m.country("Iran").vehicle_group:
|
||||
if len(vehicle_group.units) > 0 and vehicle_group.units[0].type == MissilesSS.SS_N_2_Silkworm.id:
|
||||
antiship_locations.append(PresetLocation(vehicle_group.position,
|
||||
vehicle_group.units[0].heading,
|
||||
vehicle_group.name))
|
||||
|
||||
return PresetControlPointLocations(ashore_locations, offshore_locations,
|
||||
antiship_locations, powerplants_locations)
|
||||
15
gen/locations/preset_locations.py
Normal file
15
gen/locations/preset_locations.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from dcs import Point
|
||||
|
||||
|
||||
@dataclass
|
||||
class PresetLocation:
|
||||
"""A preset location"""
|
||||
position: Point
|
||||
heading: int
|
||||
id: str
|
||||
|
||||
def __str__(self):
|
||||
return "-" * 10 + "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(self.position.x, self.position.y, self.heading,
|
||||
self.id) + "-" * 10
|
||||
29
gen/missiles/missiles_group_generator.py
Normal file
29
gen/missiles/missiles_group_generator.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import logging
|
||||
import random
|
||||
from game import db
|
||||
from gen.missiles.scud_site import ScudGenerator
|
||||
from gen.missiles.v1_group import V1GroupGenerator
|
||||
|
||||
MISSILES_MAP = {
|
||||
"V1GroupGenerator": V1GroupGenerator,
|
||||
"ScudGenerator": ScudGenerator
|
||||
}
|
||||
|
||||
|
||||
def generate_missile_group(game, ground_object, faction_name: str):
|
||||
"""
|
||||
This generate a ship group
|
||||
:return: Nothing, but put the group reference inside the ground object
|
||||
"""
|
||||
faction = db.FACTIONS[faction_name]
|
||||
if len(faction.missiles) > 0:
|
||||
generators = faction.missiles
|
||||
if len(generators) > 0:
|
||||
gen = random.choice(generators)
|
||||
if gen in MISSILES_MAP.keys():
|
||||
generator = MISSILES_MAP[gen](game, ground_object, faction)
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
else:
|
||||
logging.info("Unable to generate missile group, generator : " + str(gen) + "does not exists")
|
||||
return None
|
||||
30
gen/missiles/scud_site.py
Normal file
30
gen/missiles/scud_site.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import random
|
||||
|
||||
from dcs.vehicles import Unarmed, MissilesSS, AirDefence
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class ScudGenerator(GroupGenerator):
|
||||
|
||||
def __init__(self, game, ground_object, faction):
|
||||
super(ScudGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
|
||||
def generate(self):
|
||||
|
||||
# Scuds
|
||||
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#0", self.position.x, self.position.y + random.randint(1, 8), self.heading)
|
||||
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#1", self.position.x + 50, self.position.y + random.randint(1, 8), self.heading)
|
||||
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#2", self.position.x + 100, self.position.y + random.randint(1, 8), self.heading)
|
||||
|
||||
# Commander
|
||||
self.add_unit(Unarmed.Transport_UAZ_469, "Kubel#0", self.position.x - 35, self.position.y - 20,
|
||||
self.heading)
|
||||
|
||||
# Shorad
|
||||
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "SHILKA#0", self.position.x - 55, self.position.y - 38,
|
||||
self.heading)
|
||||
|
||||
self.add_unit(AirDefence.SAM_SA_9_Strela_1_9P31, "STRELA#0",
|
||||
self.position.x + 200, self.position.y + 15, 90)
|
||||
32
gen/missiles/v1_group.py
Normal file
32
gen/missiles/v1_group.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import random
|
||||
|
||||
from dcs.vehicles import Unarmed, MissilesSS, AirDefence
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class V1GroupGenerator(GroupGenerator):
|
||||
|
||||
def __init__(self, game, ground_object, faction):
|
||||
super(V1GroupGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
|
||||
def generate(self):
|
||||
|
||||
# Ramps
|
||||
self.add_unit(MissilesSS.V_1_ramp, "V1#0", self.position.x, self.position.y + random.randint(1, 8), self.heading)
|
||||
self.add_unit(MissilesSS.V_1_ramp, "V1#1", self.position.x + 50, self.position.y + random.randint(1, 8), self.heading)
|
||||
self.add_unit(MissilesSS.V_1_ramp, "V1#2", self.position.x + 100, self.position.y + random.randint(1, 8), self.heading)
|
||||
|
||||
# Commander
|
||||
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#0", self.position.x - 35, self.position.y - 20,
|
||||
self.heading)
|
||||
|
||||
# Self defense flak
|
||||
flak_unit = random.choice([AirDefence.AAA_Flak_Vierling_38, AirDefence.AAA_Flak_38])
|
||||
|
||||
self.add_unit(flak_unit, "FLAK#0", self.position.x - 55, self.position.y - 38,
|
||||
self.heading)
|
||||
|
||||
self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#0",
|
||||
self.position.x + 200, self.position.y + 15, 90)
|
||||
@@ -1,38 +1,84 @@
|
||||
from game import db
|
||||
import random
|
||||
|
||||
ALPHA_MILITARY = ["Alpha","Bravo","Charlie","Delta","Echo","Foxtrot",
|
||||
"Golf","Hotel","India","Juliet","Kilo","Lima","Mike",
|
||||
"November","Oscar","Papa","Quebec","Romeo","Sierra",
|
||||
"Tango","Uniform","Victor","Whisky","XRay","Yankee",
|
||||
"Zulu","Zero"]
|
||||
|
||||
class NameGenerator:
|
||||
number = 0
|
||||
|
||||
def next_armor_group_name(self):
|
||||
self.number += 1
|
||||
return "Armor Unit {}".format(self.number)
|
||||
ANIMALS = [
|
||||
"SHARK", "TORTOISE", "BAT", "PANGOLIN", "AARDWOLF",
|
||||
"MONKEY", "BUFFALO", "DOG", "BOBCAT", "LYNX", "PANTHER", "TIGER",
|
||||
"LION", "OWL", "BUTTERFLY", "BISON", "DUCK", "COBRA", "MAMBA",
|
||||
"DOLPHIN", "PHEASANT", "ARMADILLLO", "RACOON", "ZEBRA", "COW", "COYOTE", "FOX",
|
||||
"LIGHTFOOT", "COTTONMOUTH", "TAURUS", "VIPER", "CASTOR", "GIRAFFE", "SNAKE",
|
||||
"MONSTER", "ALBATROSS", "HAWK", "DOVE", "MOCKINGBIRD", "GECKO", "ORYX", "GORILLA",
|
||||
"HARAMBE", "GOOSE", "MAVERICK", "HARE", "JACKAL", "LEOPARD", "CAT", "MUSK", "ORCA",
|
||||
"OCELOT", "BEAR", "PANDA", "GULL", "PENGUIN", "PYTHON", "RAVEN", "DEER", "MOOSE",
|
||||
"REINDEER", "SHEEP", "GAZELLE", "INSECT", "VULTURE", "WALLABY", "KANGAROO", "KOALA",
|
||||
"KIWI", "WHALE", "FISH", "RHINO", "HIPPO", "RAT", "WOODPECKER", "WORM", "BABOON",
|
||||
"YAK", "SCORPIO", "HORSE", "POODLE", "CENTIPEDE", "CHICKEN", "CHEETAH", "CHAMELEON",
|
||||
"CATFISH", "CATERPILLAR", "CARACAL", "CAMEL", "CAIMAN", "BARRACUDA", "BANDICOOT",
|
||||
"ALLIGATOR", "BONGO", "CORAL", "ELEPHANT", "ANTELOPE", "CRAB", "DACHSHUND", "DODO",
|
||||
"FLAMINGO", "FERRET", "FALCON", "BULLDOG", "DONKEY", "IGUANA", "TAMARIN", "HARRIER",
|
||||
"GRIZZLY", "GREYHOUND", "GRASSHOPPER", "JAGUAR", "LADYBUG", "KOMODO", "DRAGON", "LIZARD",
|
||||
"LLAMA", "LOBSTER", "OCTOPUS", "MANATEE", "MAGPIE", "MACAW", "OSTRICH", "OYSTER",
|
||||
"MOLE", "MULE", "MOTH", "MONGOOSE", "MOLLY", "MEERKAT", "MOUSE", "PEACOCK", "PIKE", "ROBIN",
|
||||
"RAGDOLL", "PLATYPUS", "PELICAN", "PARROT", "PORCUPINE", "PIRANHA", "PUMA", "PUG", "TAPIR",
|
||||
"TERMITE", "URCHIN", "SHRIMP", "TURKEY", "TOUCAN", "TETRA", "HUSKY", "STARFISH", "SWAN",
|
||||
"FROG", "SQUIRREL", "WALRUS", "WARTHOG", "CORGI", "WEASEL", "WOMBAT", "WOLVERINE", "MAMMOTH",
|
||||
"TOAD", "WOLF", "ZEBU", "SEAL", "SKATE", "JELLYFISH", "MOSQUITO", "LOCUST", "SLUG", "SNAIL",
|
||||
"HEDGEHOG", "PIGLET", "FENNEC", "BADGER", "ALPACA", "DINGO", "COLT", "SKUNK", "BUNNY", "IMPALA",
|
||||
"GUANACO", "CAPYBARA", "ELK", "MINK", "PRONGHORN", "CROW", "BUMBLEBEE", "FAWN", "OTTER", "WATERBUCK",
|
||||
"JERBOA", "KITTEN", "ARGALI", "OX", "MARE", "FINCH", "BASILISK", "GOPHER", "HAMSTER", "CANARY", "WOODCHUCK",
|
||||
"ANACONDA"
|
||||
]
|
||||
|
||||
def next_cas_group_name(self):
|
||||
self.number += 1
|
||||
return "CAS Unit {}".format(self.number)
|
||||
def __init__(self):
|
||||
self.number = 0
|
||||
self.ANIMALS = NameGenerator.ANIMALS.copy()
|
||||
|
||||
def next_escort_group_name(self):
|
||||
self.number += 1
|
||||
return "Escort Unit {}".format(self.number)
|
||||
def reset(self):
|
||||
self.number = 0
|
||||
self.ANIMALS = NameGenerator.ANIMALS.copy()
|
||||
|
||||
def next_intercept_group_name(self):
|
||||
def next_unit_name(self, country, parent_base_id, unit_type):
|
||||
self.number += 1
|
||||
return "Intercept Unit {}".format(self.number)
|
||||
|
||||
def next_ground_group_name(self):
|
||||
self.number += 1
|
||||
return "AA Unit {}".format(self.number)
|
||||
return "unit|{}|{}|{}|{}|".format(country.id, self.number, parent_base_id, db.unit_type_name(unit_type))
|
||||
|
||||
def next_transport_group_name(self):
|
||||
def next_infantry_name(self, country, parent_base_id, unit_type):
|
||||
self.number += 1
|
||||
return "Transport Unit {}".format(self.number)
|
||||
return "infantry|{}|{}|{}|{}|".format(country.id, self.number, parent_base_id, db.unit_type_name(unit_type))
|
||||
|
||||
def next_awacs_group_name(self):
|
||||
self.number += 1
|
||||
return "AWACS Unit {}".format(self.number)
|
||||
def next_basedefense_name(self):
|
||||
return "basedefense_aa|0|0|"
|
||||
|
||||
def next_passenger_group_name(self):
|
||||
def next_awacs_name(self, country):
|
||||
self.number += 1
|
||||
return "Infantry Unit {}".format(self.number)
|
||||
return "awacs|{}|{}|0|".format(country.id, self.number)
|
||||
|
||||
def next_tanker_name(self, country, unit_type):
|
||||
self.number += 1
|
||||
return "tanker|{}|{}|0|{}".format(country.id, self.number, db.unit_type_name(unit_type))
|
||||
|
||||
def next_carrier_name(self, country):
|
||||
self.number += 1
|
||||
return "carrier|{}|{}|0|".format(country.id, self.number)
|
||||
|
||||
def random_objective_name(self):
|
||||
if len(self.ANIMALS) == 0:
|
||||
return random.choice(ALPHA_MILITARY).upper() + "#" + str(random.randint(0, 100))
|
||||
else:
|
||||
animal = random.choice(self.ANIMALS)
|
||||
self.ANIMALS.remove(animal)
|
||||
return animal
|
||||
|
||||
|
||||
namegen = NameGenerator()
|
||||
|
||||
|
||||
|
||||
|
||||
245
gen/radios.py
Normal file
245
gen/radios.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""Radio frequency types and allocators."""
|
||||
import itertools
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Iterator, List, Set
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RadioFrequency:
|
||||
"""A radio frequency.
|
||||
|
||||
Not currently concerned with tracking modulation, just the frequency.
|
||||
"""
|
||||
|
||||
#: The frequency in kilohertz.
|
||||
hertz: int
|
||||
|
||||
def __str__(self):
|
||||
if self.hertz >= 1000000:
|
||||
return self.format("MHz", 1000000)
|
||||
return self.format("kHz", 1000)
|
||||
|
||||
def format(self, units: str, divisor: int) -> str:
|
||||
converted = self.hertz / divisor
|
||||
if converted.is_integer():
|
||||
return f"{int(converted)} {units}"
|
||||
return f"{converted:0.3f} {units}"
|
||||
|
||||
@property
|
||||
def mhz(self) -> float:
|
||||
"""Returns the frequency in megahertz.
|
||||
|
||||
Returns:
|
||||
The frequency in megahertz.
|
||||
"""
|
||||
return self.hertz / 1000000
|
||||
|
||||
|
||||
def MHz(num: int, khz: int = 0) -> RadioFrequency:
|
||||
return RadioFrequency(num * 1000000 + khz * 1000)
|
||||
|
||||
|
||||
def kHz(num: int) -> RadioFrequency:
|
||||
return RadioFrequency(num * 1000)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Radio:
|
||||
"""A radio.
|
||||
|
||||
Defines the minimum (inclusive) and maximum (exclusive) range of the radio.
|
||||
"""
|
||||
|
||||
#: The name of the radio.
|
||||
name: str
|
||||
|
||||
#: The minimum (inclusive) frequency tunable by this radio.
|
||||
minimum: RadioFrequency
|
||||
|
||||
#: The maximum (exclusive) frequency tunable by this radio.
|
||||
maximum: RadioFrequency
|
||||
|
||||
#: The spacing between adjacent frequencies.
|
||||
step: RadioFrequency
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def range(self) -> Iterator[RadioFrequency]:
|
||||
"""Returns an iterator over the usable frequencies of this radio."""
|
||||
return (RadioFrequency(x) for x in range(
|
||||
self.minimum.hertz, self.maximum.hertz, self.step.hertz
|
||||
))
|
||||
|
||||
|
||||
class OutOfChannelsError(RuntimeError):
|
||||
"""Raised when all channels usable by this radio have been allocated."""
|
||||
|
||||
def __init__(self, radio: Radio) -> None:
|
||||
super().__init__(f"No available channels for {radio}")
|
||||
|
||||
|
||||
class ChannelInUseError(RuntimeError):
|
||||
"""Raised when attempting to reserve an in-use frequency."""
|
||||
|
||||
def __init__(self, frequency: RadioFrequency) -> None:
|
||||
super().__init__(f"{frequency} is already in use")
|
||||
|
||||
|
||||
# TODO: Figure out appropriate steps for each radio. These are just guesses.
|
||||
#: List of all known radios used by aircraft in the game.
|
||||
RADIOS: List[Radio] = [
|
||||
Radio("AN/ARC-164", MHz(225), MHz(400), step=MHz(1)),
|
||||
Radio("AN/ARC-186(V) AM", MHz(116), MHz(152), step=MHz(1)),
|
||||
Radio("AN/ARC-186(V) FM", MHz(30), MHz(76), step=MHz(1)),
|
||||
# The AN/ARC-210 can also use [30, 88) and [108, 118), but the current
|
||||
# implementation can't implement the gap and the radio can't transmit on the
|
||||
# latter. There's still plenty of channels between 118 MHz and 400 MHz, so
|
||||
# not worth worrying about.
|
||||
Radio("AN/ARC-210", MHz(118), MHz(400), step=MHz(1)),
|
||||
Radio("AN/ARC-222", MHz(116), MHz(174), step=MHz(1)),
|
||||
Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)),
|
||||
Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)),
|
||||
Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)),
|
||||
|
||||
# Note: The M2000C V/UHF can operate in both ranges, but has a gap between
|
||||
# 150 MHz and 225 MHz. We can't allocate in that gap, and the current
|
||||
# system doesn't model gaps, so just pretend it ends at 150 MHz for now. We
|
||||
# can model gaps later if needed.
|
||||
Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(150), step=MHz(1)),
|
||||
Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)),
|
||||
|
||||
# Tomcat radios
|
||||
# # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio
|
||||
Radio("AN/ARC-159", MHz(225), MHz(400), step=MHz(1)),
|
||||
# AN/ARC-182 can also operate from 30 MHz to 88 MHz, as well as from 225 MHz
|
||||
# to 400 MHz range, but we can't model gaps with the current implementation.
|
||||
# https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio
|
||||
Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)),
|
||||
|
||||
# Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps.
|
||||
Radio("FR 22", MHz(225), MHz(400), step=kHz(50)),
|
||||
|
||||
# P-51 / P-47 Radio
|
||||
# 4 preset channels (A/B/C/D)
|
||||
Radio("SCR522", MHz(100), MHz(156), step=kHz(25)),
|
||||
|
||||
Radio("R&S M3AR VHF", MHz(120), MHz(174), step=MHz(1)),
|
||||
Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)),
|
||||
|
||||
# MiG-15bis
|
||||
Radio("RSI-6K HF", MHz(3, 750), MHz(5), step=kHz(25)),
|
||||
|
||||
# MiG-19P
|
||||
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
|
||||
|
||||
# MiG-21bis
|
||||
Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
|
||||
|
||||
# Ka-50
|
||||
# Note: Also capable of 100MHz-150MHz, but we can't model gaps.
|
||||
Radio("R-800L1", MHz(220), MHz(400), step=kHz(25)),
|
||||
Radio("R-828", MHz(20), MHz(60), step=kHz(25)),
|
||||
|
||||
# UH-1H
|
||||
Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)),
|
||||
Radio("AN/ARC-131", MHz(30), MHz(76), step=kHz(50)),
|
||||
Radio("AN/ARC-134", MHz(116), MHz(150), step=kHz(25)),
|
||||
]
|
||||
|
||||
|
||||
def get_radio(name: str) -> Radio:
|
||||
"""Returns the radio with the given name.
|
||||
|
||||
Args:
|
||||
name: Name of the radio to return.
|
||||
|
||||
Returns:
|
||||
The radio matching name.
|
||||
|
||||
Raises:
|
||||
KeyError: No matching radio was found.
|
||||
"""
|
||||
for radio in RADIOS:
|
||||
if radio.name == name:
|
||||
return radio
|
||||
raise KeyError
|
||||
|
||||
|
||||
class RadioRegistry:
|
||||
"""Manages allocation of radio channels.
|
||||
|
||||
There's some room for improvement here. We could prefer to allocate
|
||||
frequencies that are available to the fewest number of radios first, so
|
||||
radios with wide bands like the AN/ARC-210 don't exhaust all the channels
|
||||
available to narrower radios like the AN/ARC-186(V). In practice there are
|
||||
probably plenty of channels, so we can deal with that later if we need to.
|
||||
|
||||
We could also allocate using a larger increment, returning to smaller
|
||||
increments each time the range is exhausted. This would help with the
|
||||
previous problem, as the AN/ARC-186(V) would still have plenty of 25 kHz
|
||||
increment channels left after the AN/ARC-210 moved on to the higher
|
||||
frequencies. This would also look a little nicer than having every flight
|
||||
allocated in the 30 MHz range.
|
||||
"""
|
||||
|
||||
# Not a real radio, but useful for allocating a channel usable for
|
||||
# inter-flight communications.
|
||||
BLUFOR_UHF = Radio("BLUFOR UHF", MHz(225), MHz(400), step=MHz(1))
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.allocated_channels: Set[RadioFrequency] = set()
|
||||
self.radio_allocators: Dict[Radio, Iterator[RadioFrequency]] = {}
|
||||
|
||||
radios = itertools.chain(RADIOS, [self.BLUFOR_UHF])
|
||||
for radio in radios:
|
||||
self.radio_allocators[radio] = radio.range()
|
||||
|
||||
def alloc_for_radio(self, radio: Radio) -> RadioFrequency:
|
||||
"""Allocates a radio channel tunable by the given radio.
|
||||
|
||||
Args:
|
||||
radio: The radio to allocate a channel for.
|
||||
|
||||
Returns:
|
||||
A radio channel compatible with the given radio.
|
||||
|
||||
Raises:
|
||||
OutOfChannelsError: All channels compatible with the given radio are
|
||||
already allocated.
|
||||
"""
|
||||
allocator = self.radio_allocators[radio]
|
||||
try:
|
||||
while (channel := next(allocator)) in self.allocated_channels:
|
||||
pass
|
||||
self.reserve(channel)
|
||||
return channel
|
||||
except StopIteration:
|
||||
raise OutOfChannelsError(radio)
|
||||
|
||||
def alloc_uhf(self) -> RadioFrequency:
|
||||
"""Allocates a UHF radio channel suitable for inter-flight comms.
|
||||
|
||||
Returns:
|
||||
A UHF radio channel suitable for inter-flight comms.
|
||||
|
||||
Raises:
|
||||
OutOfChannelsError: All channels compatible with the given radio are
|
||||
already allocated.
|
||||
"""
|
||||
return self.alloc_for_radio(self.BLUFOR_UHF)
|
||||
|
||||
def reserve(self, frequency: RadioFrequency) -> None:
|
||||
"""Reserves the given channel.
|
||||
|
||||
Reserving a channel ensures that it will not be allocated in the future.
|
||||
|
||||
Args:
|
||||
frequency: The channel to reserve.
|
||||
|
||||
Raises:
|
||||
ChannelInUseError: The given frequency is already in use.
|
||||
"""
|
||||
if frequency in self.allocated_channels:
|
||||
raise ChannelInUseError(frequency)
|
||||
self.allocated_channels.add(frequency)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user