mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
1530 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
787c93b9d4 | ||
|
|
63953992a9 | ||
|
|
567cb0c0b6 | ||
|
|
f37999a3ef | ||
|
|
545761e3d4 | ||
|
|
561d679a62 | ||
|
|
991cd91dd4 | ||
|
|
9e51ff0253 | ||
|
|
1a1e55e16c | ||
|
|
2fe4a39784 | ||
|
|
7d81b9ef5c | ||
|
|
b355b6dc60 | ||
|
|
819148762b | ||
|
|
4112a86fe9 | ||
|
|
7838c9b49b | ||
|
|
ca5a70e3bc | ||
|
|
e134143f16 | ||
|
|
0f1577d314 | ||
|
|
a36858f3ea | ||
|
|
aaa6637435 | ||
|
|
f0f6739cf8 | ||
|
|
2c38ce910c | ||
|
|
edba923f2f | ||
|
|
4b0d2f7abc | ||
|
|
dd28781b69 | ||
|
|
ece56032f1 | ||
|
|
8e3039dd37 | ||
|
|
1d1c130d19 | ||
|
|
1fd3f70eec | ||
|
|
d9ea33cbb9 | ||
|
|
25b72e1af4 | ||
|
|
ff0446cc12 | ||
|
|
6455c38ff4 | ||
|
|
f62c2fbabb | ||
|
|
4c0fc5a407 | ||
|
|
f608cd5aef | ||
|
|
0371b62acb | ||
|
|
eddd66b5c4 | ||
|
|
c8e71d269b | ||
|
|
ae034d5387 | ||
|
|
817d6a0e15 | ||
|
|
dff98f0b53 | ||
|
|
d0856ff279 | ||
|
|
d6b762efa7 | ||
|
|
b476a26759 | ||
|
|
1a062e2170 | ||
|
|
fe658eb877 | ||
|
|
feed55186f | ||
|
|
56591b8655 | ||
|
|
7c52ca15f3 | ||
|
|
06c751f214 | ||
|
|
9d774eaad8 | ||
|
|
babfd4abda | ||
|
|
a029c165a0 | ||
|
|
9b51533d96 | ||
|
|
e1b7e0eb00 | ||
|
|
508a5693c9 | ||
|
|
f687a30c7e | ||
|
|
15615a1077 | ||
|
|
905175c210 | ||
|
|
409e070887 | ||
|
|
f659dc1f76 | ||
|
|
b8922b39fd | ||
|
|
8137d57cdf | ||
|
|
bf290ac1a9 | ||
|
|
77fda00233 | ||
|
|
0255088e30 | ||
|
|
b74d8b12d0 | ||
|
|
2834f2982c | ||
|
|
7d07faa5fb | ||
|
|
fd70f0fc4a | ||
|
|
441ef79aa4 | ||
|
|
c54f6ba4d2 | ||
|
|
61ecdfc48c | ||
|
|
5e2b259af1 | ||
|
|
1258f3e17c | ||
|
|
44ed895277 | ||
|
|
c74e18e449 | ||
|
|
2ea3f914f0 | ||
|
|
d6e4a50064 | ||
|
|
6296896471 | ||
|
|
473cda971a | ||
|
|
cf570adabe | ||
|
|
02196f2883 | ||
|
|
8b49752401 | ||
|
|
8c64867918 | ||
|
|
e544063c40 | ||
|
|
edfaaacd04 | ||
|
|
84b8613cf5 | ||
|
|
aea82e2266 | ||
|
|
b0b9c1c8e6 | ||
|
|
b5ff32c5b6 | ||
|
|
e0223ded54 | ||
|
|
2012ad0aa3 | ||
|
|
15d72a8dcb | ||
|
|
e525b11695 | ||
|
|
8f30e60e1b | ||
|
|
ce977ac937 | ||
|
|
aa9ffa0855 | ||
|
|
6a8ca810ff | ||
|
|
f2d2fd7014 | ||
|
|
ddd06b3162 | ||
|
|
d519aa1dad | ||
|
|
7226359e64 | ||
|
|
f396ff7f12 | ||
|
|
1adee0af17 | ||
|
|
bac47dad83 | ||
|
|
f1a2602cfd | ||
|
|
b8e64d4369 | ||
|
|
72ac8ca872 | ||
|
|
ccb41829c9 | ||
|
|
90697194a1 | ||
|
|
13f4baa34e | ||
|
|
76840ff5c2 | ||
|
|
72ac806cb8 | ||
|
|
bf275fe564 | ||
|
|
68818ae50d | ||
|
|
7315d097c2 | ||
|
|
cdf28700cf | ||
|
|
948c1d0bb0 | ||
|
|
de0a3f929c | ||
|
|
c3023a9f99 | ||
|
|
4f37610dfb | ||
|
|
aef4316f72 | ||
|
|
378dbf254a | ||
|
|
f0b6a37ce2 | ||
|
|
ff12a120e6 | ||
|
|
d04be4d71b | ||
|
|
581aaaad28 | ||
|
|
453eb9feb4 | ||
|
|
4059ee44b8 | ||
|
|
7a222ecfa0 | ||
|
|
be15e9adf2 | ||
|
|
2fd097c613 | ||
|
|
66ee5f5392 | ||
|
|
3bb08f8d30 | ||
|
|
d7787adddc | ||
|
|
1f37b879b1 | ||
|
|
45ce28f9bf | ||
|
|
4e87bed4e5 | ||
|
|
a7421fc670 | ||
|
|
208a7550ef | ||
|
|
37e23f70d6 | ||
|
|
7daefa8ae5 | ||
|
|
292ac42003 | ||
|
|
c501c45c52 | ||
|
|
29b894f8b0 | ||
|
|
55573bf40a | ||
|
|
f2c2ef82c5 | ||
|
|
d6b33d353c | ||
|
|
2ed1c36c54 | ||
|
|
7cbcbc1171 | ||
|
|
b74dcfa053 | ||
|
|
62bf7eb227 | ||
|
|
07bfe8e29a | ||
|
|
0b258997dd | ||
|
|
69421ad7a1 | ||
|
|
fdf571c016 | ||
|
|
bd60760f9d | ||
|
|
e8aa9839b0 | ||
|
|
fcdb22db5b | ||
|
|
e73cf68def | ||
|
|
fa5b842cc7 | ||
|
|
43a21cb341 | ||
|
|
45361b57a7 | ||
|
|
046c7a662a | ||
|
|
a9f1de13b1 | ||
|
|
edbe2d86f2 | ||
|
|
4e12a1cdad | ||
|
|
d24c7ea93e | ||
|
|
484f1e8d51 | ||
|
|
8d5abb877c | ||
|
|
fd454dce74 | ||
|
|
5d4fccd438 | ||
|
|
9f078e1483 | ||
|
|
0e807d84c2 | ||
|
|
3ad57d995b | ||
|
|
28cf42aeb8 | ||
|
|
7fcf74a8ed | ||
|
|
a0d38f7465 | ||
|
|
cd97526d2b | ||
|
|
87fdc16f9b | ||
|
|
b69eb02766 | ||
|
|
7636234649 | ||
|
|
2bd673a531 | ||
|
|
a1b64bc72d | ||
|
|
80bc9d6b23 | ||
|
|
ee768b9147 | ||
|
|
7dfb0c67e5 | ||
|
|
75ea5cc462 | ||
|
|
afabf6fd00 | ||
|
|
0eb4519797 | ||
|
|
611f04ab5a | ||
|
|
a9ba2deafa | ||
|
|
0c4e920af3 | ||
|
|
ef0e565337 | ||
|
|
02e7ab41b4 | ||
|
|
59bd4541c4 | ||
|
|
ca30af4238 | ||
|
|
718b3f2623 | ||
|
|
6e153c6451 | ||
|
|
4a1809d56e | ||
|
|
a2bf0c1bea | ||
|
|
f0d9dae33b | ||
|
|
b99462b628 | ||
|
|
65ac30acda | ||
|
|
2072b6fa63 | ||
|
|
4f604ba687 | ||
|
|
b9fe559b42 | ||
|
|
efcdbebda5 | ||
|
|
8886850c60 | ||
|
|
20276e5230 | ||
|
|
ed96bc83b4 | ||
|
|
c0147f5eb7 | ||
|
|
5bf5f024cb | ||
|
|
4628e8320a | ||
|
|
9c1d36d18a | ||
|
|
789b618e37 | ||
|
|
d0804a6f9e | ||
|
|
17fe977b06 | ||
|
|
60783ca390 | ||
|
|
34a7a37409 | ||
|
|
e68d2b5deb | ||
|
|
b0317055e7 | ||
|
|
6e0af7c144 | ||
|
|
9394ed663a | ||
|
|
967574820f | ||
|
|
da17d1e5d1 | ||
|
|
63bdbebcaa | ||
|
|
c67263662d | ||
|
|
2484457183 | ||
|
|
d7b328b887 | ||
|
|
493e53c28f | ||
|
|
7438c30885 | ||
|
|
fac43ba20b | ||
|
|
c2eb243026 | ||
|
|
2adaee8671 | ||
|
|
57edc5678c | ||
|
|
730130b19e | ||
|
|
17b0cee507 | ||
|
|
2557383946 | ||
|
|
29d3b5dfc6 | ||
|
|
f6fad30852 | ||
|
|
d5a081a15f | ||
|
|
6147e9ac96 | ||
|
|
b32ca4f92f | ||
|
|
b57dd51f86 | ||
|
|
fc6ca162af | ||
|
|
58ba9e9d1d | ||
|
|
939b6c468d | ||
|
|
bf7df6721a | ||
|
|
f6e0dbbb6a | ||
|
|
851c2d88a9 | ||
|
|
200c13dc31 | ||
|
|
8889e35f9e | ||
|
|
398fa1e73d | ||
|
|
316f73138c | ||
|
|
708b615ad7 | ||
|
|
866ff78518 | ||
|
|
f0480b033f | ||
|
|
4d19548736 | ||
|
|
fcf45554ef | ||
|
|
799b0fae94 | ||
|
|
0d95716545 | ||
|
|
8c5b808eba | ||
|
|
007dcf548e | ||
|
|
9a640bf7eb | ||
|
|
edd02d9dd6 | ||
|
|
75edbb62f1 | ||
|
|
2b6227f3b1 | ||
|
|
c4b8a41742 | ||
|
|
f8b2dbe283 | ||
|
|
20091292f4 | ||
|
|
a594f45aae | ||
|
|
d394d01ea8 | ||
|
|
70d982b0ed | ||
|
|
ae68a35a1a | ||
|
|
a9fcfe60f4 | ||
|
|
c3b028ef4b | ||
|
|
833399f068 | ||
|
|
5695cf4ac5 | ||
|
|
1553e5efd5 | ||
|
|
0f1b396dd2 | ||
|
|
976ee51bf5 | ||
|
|
7c22f6e83b | ||
|
|
18b6f7b84c | ||
|
|
206d09f7f8 | ||
|
|
4e910c4b09 | ||
|
|
bc3cd50a6c | ||
|
|
cecf611f91 | ||
|
|
5e4802f05e | ||
|
|
1e70e654ed | ||
|
|
20054b9825 | ||
|
|
5fb6a53cbd | ||
|
|
ff751c30f9 | ||
|
|
c1614ad5a7 | ||
|
|
13e372159a | ||
|
|
1ee0aafd9a | ||
|
|
df80ec635f | ||
|
|
6524286f04 | ||
|
|
87248fec53 | ||
|
|
6470d25d18 | ||
|
|
b7634a8ac3 | ||
|
|
434755a620 | ||
|
|
3eb2529b0b | ||
|
|
e6e4cca076 | ||
|
|
8d57bbc777 | ||
|
|
483db564f9 | ||
|
|
63d5862319 | ||
|
|
365b379798 | ||
|
|
20f97e48a9 | ||
|
|
fd473f0a46 | ||
|
|
94c5ed8bdc | ||
|
|
4b7b4bf110 | ||
|
|
a816877d08 | ||
|
|
2d56ae1cb6 | ||
|
|
a223da8f99 | ||
|
|
216adcc35a | ||
|
|
8d485d5fa2 | ||
|
|
fa549fcf94 | ||
|
|
98fd707aea | ||
|
|
5928f29f11 | ||
|
|
c3ebabbe44 | ||
|
|
29fd094dd0 | ||
|
|
f7966b8d8c | ||
|
|
ee2f4ecbc8 | ||
|
|
b08f6cad1d | ||
|
|
e851223733 | ||
|
|
1f12546ff4 | ||
|
|
8345063e84 | ||
|
|
482bedd739 | ||
|
|
7d7a334418 | ||
|
|
69dbe62b70 | ||
|
|
f61167cedf | ||
|
|
2ac92a75a4 | ||
|
|
fe80a9fd08 | ||
|
|
505af7635f | ||
|
|
426dc69e1d | ||
|
|
082e8c062c | ||
|
|
9f2409bb9e | ||
|
|
14dd8e43a4 | ||
|
|
9fb33526a7 | ||
|
|
8bd00bf450 | ||
|
|
f3553ced78 | ||
|
|
a52dc43c9e | ||
|
|
0b6b40a358 | ||
|
|
f6371d2ef1 | ||
|
|
ecd073e31d | ||
|
|
b7d160631a | ||
|
|
dc235f36c8 | ||
|
|
7503c1e1e9 | ||
|
|
253e8a209c | ||
|
|
e26b692631 | ||
|
|
658d808524 | ||
|
|
28e00055ab | ||
|
|
8eef1eaa7c | ||
|
|
e361a857a4 | ||
|
|
d369ce8847 | ||
|
|
d05897edcb | ||
|
|
1d98432c57 | ||
|
|
e60166dc89 | ||
|
|
87afc2fcef | ||
|
|
bd1457c435 | ||
|
|
c1f88b4a5f | ||
|
|
a080d4b692 | ||
|
|
c20e9e19cb | ||
|
|
d9056acc6d | ||
|
|
21cd764f66 | ||
|
|
dfc31dfd5c | ||
|
|
8ffbf32677 | ||
|
|
6e2124252c | ||
|
|
3dd07b8c23 | ||
|
|
2e067aada6 | ||
|
|
78cd60f3df | ||
|
|
2891649531 | ||
|
|
4b40739918 | ||
|
|
96c401e1b9 | ||
|
|
fad132dcca | ||
|
|
fae9650f56 | ||
|
|
e26e7f53c5 | ||
|
|
696710bf41 | ||
|
|
9019cbfd2b | ||
|
|
f8735927bf | ||
|
|
16cfc4e945 | ||
|
|
85f931316a | ||
|
|
717ea05d38 | ||
|
|
3987274764 | ||
|
|
73a97f9c2a | ||
|
|
80612ba97d | ||
|
|
c4d2b92e34 | ||
|
|
169fba9ab8 | ||
|
|
0b902e19ee | ||
|
|
ab26a76789 | ||
|
|
1f43fbe16e | ||
|
|
f29cb99530 | ||
|
|
ef84703da9 | ||
|
|
9e32ea7413 | ||
|
|
75769df8e2 | ||
|
|
95fd4cab05 | ||
|
|
a81254cd18 | ||
|
|
4cff838de0 | ||
|
|
3838b3ca4f | ||
|
|
7dc3e041c8 | ||
|
|
33b92423d8 | ||
|
|
6237fffa5a | ||
|
|
0b4e2d3b6b | ||
|
|
c4d08fa7b7 | ||
|
|
cec28351e7 | ||
|
|
398630d51e | ||
|
|
61400ba726 | ||
|
|
33885e2216 | ||
|
|
5719b136fe | ||
|
|
ede5ee60c3 | ||
|
|
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 |
53
.github/workflows/build.yml
vendored
Normal file
53
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
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: 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/
|
||||
108
.github/workflows/release.yml
vendored
Normal file
108
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
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: Build binaries
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
$env:PYTHONPATH=".;./pydcs"
|
||||
pyinstaller pyinstaller.spec
|
||||
|
||||
- name: Create Installer
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref }}
|
||||
run: |
|
||||
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.${{ 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,8 +1,40 @@
|
||||
[DCS World](https://www.digitalcombatsimulator.com/en/products/world/) single-player liberation dynamic campaign.
|
||||

|
||||
|
||||
[Installation instructions/Manual](https://github.com/shdwp/dcs_liberation/wiki)
|
||||
[](https://www.paypal.com/paypalme/KhopaDCSL)
|
||||
[](https://patreon.com/khopa)
|
||||
|
||||
Inspired by *ARMA Liberation* mission.
|
||||
[](https://github.com/Khopa/dcs_liberation/releases)
|
||||
|
||||
Uses [pydcs](http://github.com/pydcs/dcs) for mission generation.
|
||||
[](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()
|
||||
|
||||
372
changelog.md
Normal file
372
changelog.md
Normal file
@@ -0,0 +1,372 @@
|
||||
# 2.3.0
|
||||
|
||||
# Features/Improvements
|
||||
* **[Campaign Map]** Overhauled the campaign model
|
||||
* **[Campaign Map]** Possible to add FOB as control points
|
||||
* **[Campaign Map]** Added off-map spawn locations
|
||||
* **[Campaign AI]** Overhauled AI recruiting behaviour
|
||||
* **[Campaign AI]** Added AI proucurement for Blue
|
||||
* **[Campaign]** New Campaign: "Black Sea"
|
||||
* **[Mission Planner]** Possible to move carrier and tarawa on the campaign map
|
||||
* **[Mission Generator]** Infantry squads on frontline can have manpads
|
||||
* **[Mission Generator]** Unused aircraft now spawned to allow for OCA strikes
|
||||
* **[Mission Generator]** Opfor now obeys parking limits
|
||||
* **[Flight Planner]** Added fighter sweep missions.
|
||||
* **[Flight Planner]** Added BAI missions.
|
||||
* **[Flight Planner]** Added anti-ship missions.
|
||||
* **[Flight Planner]** Differentiated BARCAP and TARCAP. TARCAP is now for hostile areas and will arrive before the package.
|
||||
* **[Flight Planner]** Added OCA missions
|
||||
* **[Flight Planner]** Added Alternate/divert airfields
|
||||
* **[Culling]** Added possibility to include/exclude carriers from culling zones
|
||||
* **[QOL]** On liberation startup, your latest save game is loaded automatically
|
||||
* **[Units]** Reduced starting fuel load for C101
|
||||
* **[UI]** Inform the user of the weather
|
||||
* **[Game]** Added new Economy options for adjusting income multipliers and starting budgets.
|
||||
|
||||
## Fixes :
|
||||
* **[Map]** Missiles sites now have a proper icon and will not re-use the SAM sites icon
|
||||
* **[Mission Generator]** Ground unit waypoints improperly set to "On Road" - fixed
|
||||
* **[Mission Generator]** Target waypoints not at ground level - fixed
|
||||
* **[Mission Generator]** Selected skill not applied to Helicopters - fixed
|
||||
* **[Mission Generator]** Ground units do not always spawn - fixed
|
||||
* **[Kneeboard]** Briefing waypoints off by one - fixed
|
||||
* **[Game]** Destroyed buildings still granting budget - fixed
|
||||
|
||||
# 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', 'fob']
|
||||
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'fob']
|
||||
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'fob']
|
||||
|
||||
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,
|
||||
|
||||
]
|
||||
124
game/data/doctrine.py
Normal file
124
game/data/doctrine.py
Normal file
@@ -0,0 +1,124 @@
|
||||
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
|
||||
|
||||
sweep_distance: int
|
||||
|
||||
|
||||
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),
|
||||
sweep_distance=nm_to_meter(60),
|
||||
)
|
||||
|
||||
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),
|
||||
sweep_distance=nm_to_meter(40),
|
||||
)
|
||||
|
||||
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),
|
||||
sweep_distance=nm_to_meter(10),
|
||||
)
|
||||
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
|
||||
]
|
||||
1430
game/db.py
1430
game/db.py
File diff suppressed because it is too large
Load Diff
272
game/debriefing.py
Normal file
272
game/debriefing.py
Normal file
@@ -0,0 +1,272 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Type,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.theater import Airfield, ControlPoint
|
||||
from game.unitmap import Building, FrontLineUnit, GroundObjectUnit, UnitMap
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
DEBRIEFING_LOG_EXTENSION = "log"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirLosses:
|
||||
player: List[Flight]
|
||||
enemy: List[Flight]
|
||||
|
||||
@property
|
||||
def losses(self) -> Iterator[Flight]:
|
||||
return itertools.chain(self.player, self.enemy)
|
||||
|
||||
def by_type(self, player: bool) -> Dict[Type[FlyingType], int]:
|
||||
losses_by_type: Dict[Type[FlyingType], int] = defaultdict(int)
|
||||
losses = self.player if player else self.enemy
|
||||
for loss in losses:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def surviving_flight_members(self, flight: Flight) -> int:
|
||||
losses = 0
|
||||
for loss in self.losses:
|
||||
if loss == flight:
|
||||
losses += 1
|
||||
return flight.count - losses
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroundLosses:
|
||||
player_front_line: List[FrontLineUnit] = field(default_factory=list)
|
||||
enemy_front_line: List[FrontLineUnit] = field(default_factory=list)
|
||||
|
||||
player_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
|
||||
enemy_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
|
||||
|
||||
player_buildings: List[Building] = field(default_factory=list)
|
||||
enemy_buildings: List[Building] = field(default_factory=list)
|
||||
|
||||
player_airfields: List[Airfield] = field(default_factory=list)
|
||||
enemy_airfields: List[Airfield] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StateData:
|
||||
#: True if the mission ended. If False, the mission exited abnormally.
|
||||
mission_ended: bool
|
||||
|
||||
#: Names of aircraft units that were killed during the mission.
|
||||
killed_aircraft: List[str]
|
||||
|
||||
#: Names of vehicle (and ship) units that were killed during the mission.
|
||||
killed_ground_units: List[str]
|
||||
|
||||
#: Names of static units that were destroyed during the mission.
|
||||
destroyed_statics: List[str]
|
||||
|
||||
#: Mangled names of bases that were captured during the mission.
|
||||
base_capture_events: List[str]
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data: Dict[str, Any]) -> StateData:
|
||||
return cls(
|
||||
mission_ended=data["mission_ended"],
|
||||
killed_aircraft=data["killed_aircrafts"],
|
||||
# Airfields emit a new "dead" event every time a bomb is dropped on
|
||||
# them when they've already dead. Dedup.
|
||||
killed_ground_units=list(set(data["killed_ground_units"])),
|
||||
destroyed_statics=data["destroyed_objects_positions"],
|
||||
base_capture_events=data["base_capture_events"]
|
||||
)
|
||||
|
||||
|
||||
class Debriefing:
|
||||
def __init__(self, state_data: Dict[str, Any], game: Game,
|
||||
unit_map: UnitMap) -> None:
|
||||
self.state_data = StateData.from_json(state_data)
|
||||
self.unit_map = unit_map
|
||||
|
||||
self.player_country = game.player_country
|
||||
self.enemy_country = game.enemy_country
|
||||
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.air_losses = self.dead_aircraft()
|
||||
self.ground_losses = self.dead_ground_units()
|
||||
|
||||
@property
|
||||
def front_line_losses(self) -> Iterator[FrontLineUnit]:
|
||||
yield from self.ground_losses.player_front_line
|
||||
yield from self.ground_losses.enemy_front_line
|
||||
|
||||
@property
|
||||
def ground_object_losses(self) -> Iterator[GroundObjectUnit]:
|
||||
yield from self.ground_losses.player_ground_objects
|
||||
yield from self.ground_losses.enemy_ground_objects
|
||||
|
||||
@property
|
||||
def building_losses(self) -> Iterator[Building]:
|
||||
yield from self.ground_losses.player_buildings
|
||||
yield from self.ground_losses.enemy_buildings
|
||||
|
||||
@property
|
||||
def damaged_runways(self) -> Iterator[Airfield]:
|
||||
yield from self.ground_losses.player_airfields
|
||||
yield from self.ground_losses.enemy_airfields
|
||||
|
||||
def casualty_count(self, control_point: ControlPoint) -> int:
|
||||
return len(
|
||||
[x for x in self.front_line_losses if x.origin == control_point]
|
||||
)
|
||||
|
||||
def front_line_losses_by_type(
|
||||
self, player: bool) -> Dict[Type[UnitType], int]:
|
||||
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
|
||||
if player:
|
||||
losses = self.ground_losses.player_front_line
|
||||
else:
|
||||
losses = self.ground_losses.enemy_front_line
|
||||
for loss in losses:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def building_losses_by_type(self, player: bool) -> Dict[str, int]:
|
||||
losses_by_type: Dict[str, int] = defaultdict(int)
|
||||
if player:
|
||||
losses = self.ground_losses.player_buildings
|
||||
else:
|
||||
losses = self.ground_losses.enemy_buildings
|
||||
for loss in losses:
|
||||
if loss.ground_object.control_point.captured != player:
|
||||
continue
|
||||
|
||||
losses_by_type[loss.ground_object.dcs_identifier] += 1
|
||||
return losses_by_type
|
||||
|
||||
def dead_aircraft(self) -> AirLosses:
|
||||
player_losses = []
|
||||
enemy_losses = []
|
||||
for unit_name in self.state_data.killed_aircraft:
|
||||
flight = self.unit_map.flight(unit_name)
|
||||
if flight is None:
|
||||
logging.error(f"Could not find Flight matching {unit_name}")
|
||||
continue
|
||||
if flight.departure.captured:
|
||||
player_losses.append(flight)
|
||||
else:
|
||||
enemy_losses.append(flight)
|
||||
return AirLosses(player_losses, enemy_losses)
|
||||
|
||||
def dead_ground_units(self) -> GroundLosses:
|
||||
losses = GroundLosses()
|
||||
for unit_name in self.state_data.killed_ground_units:
|
||||
front_line_unit = self.unit_map.front_line_unit(unit_name)
|
||||
if front_line_unit is not None:
|
||||
if front_line_unit.origin.captured:
|
||||
losses.player_front_line.append(front_line_unit)
|
||||
else:
|
||||
losses.enemy_front_line.append(front_line_unit)
|
||||
continue
|
||||
|
||||
ground_object_unit = self.unit_map.ground_object_unit(unit_name)
|
||||
if ground_object_unit is not None:
|
||||
if ground_object_unit.ground_object.control_point.captured:
|
||||
losses.player_ground_objects.append(ground_object_unit)
|
||||
else:
|
||||
losses.enemy_ground_objects.append(ground_object_unit)
|
||||
continue
|
||||
|
||||
building = self.unit_map.building_or_fortification(unit_name)
|
||||
if building is not None:
|
||||
if building.ground_object.control_point.captured:
|
||||
losses.player_buildings.append(building)
|
||||
else:
|
||||
losses.enemy_buildings.append(building)
|
||||
continue
|
||||
|
||||
airfield = self.unit_map.airfield(unit_name)
|
||||
if airfield is not None:
|
||||
if airfield.captured:
|
||||
losses.player_airfields.append(airfield)
|
||||
else:
|
||||
losses.enemy_airfields.append(airfield)
|
||||
continue
|
||||
|
||||
# Only logging as debug because we don't currently track infantry
|
||||
# deaths, so we expect to see quite a few unclaimed dead ground
|
||||
# units. We should start tracking those and covert this to a
|
||||
# warning.
|
||||
logging.debug(f"Death of untracked ground unit {unit_name} will "
|
||||
"have no effect. This may be normal behavior.")
|
||||
|
||||
return losses
|
||||
|
||||
@property
|
||||
def base_capture_events(self):
|
||||
"""Keeps only the last instance of a base capture event for each base ID."""
|
||||
reversed_captures = list(reversed(self.state_data.base_capture_events))
|
||||
last_base_cap_indexes = []
|
||||
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
|
||||
if base not in [x[1] for x in last_base_cap_indexes]:
|
||||
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: Callable[[Debriefing], None],
|
||||
game: Game, unit_map: UnitMap) -> None:
|
||||
super().__init__()
|
||||
self._stop_event = threading.Event()
|
||||
self.callback = callback
|
||||
self.game = game
|
||||
self.unit_map = unit_map
|
||||
|
||||
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.unit_map)
|
||||
self.callback(debriefing)
|
||||
break
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def wait_for_debriefing(callback: Callable[[Debriefing], None],
|
||||
game: Game, unit_map) -> PollDebriefingFileThread:
|
||||
thread = PollDebriefingFileThread(callback, game, unit_map)
|
||||
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 *
|
||||
|
||||
14
game/event/airwar.py
Normal file
14
game/event/airwar.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .event import Event
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater
|
||||
|
||||
|
||||
class AirWarEvent(Event):
|
||||
"""Event handler for the air battle"""
|
||||
|
||||
def __str__(self):
|
||||
return "AirWar"
|
||||
@@ -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,101 +1,384 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, List, TYPE_CHECKING, Type
|
||||
|
||||
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 persistency
|
||||
from game.debriefing import AirLosses, Debriefing
|
||||
from game.infos.information import Information
|
||||
from game.operation.operation import Operation
|
||||
from game.theater import ControlPoint
|
||||
from gen import AirTaskingOrder
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from ..unitmap import UnitMap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..game import Game
|
||||
|
||||
from userdata.debriefing import Debriefing
|
||||
from userdata import persistency
|
||||
|
||||
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
|
||||
operation = None # type: Operation
|
||||
difficulty = 1 # type: int
|
||||
|
||||
game = None # type: Game
|
||||
environment_settings = None # type: EnvironmentSettings
|
||||
location = None # type: Point
|
||||
from_cp = None # type: ControlPoint
|
||||
to_cp = None # type: ControlPoint
|
||||
difficulty = 1 # type: int
|
||||
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.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:
|
||||
return self.to_cp
|
||||
else:
|
||||
return self.from_cp
|
||||
|
||||
@property
|
||||
def threat_description(self) -> str:
|
||||
return ""
|
||||
def tasks(self) -> List[Type[Task]]:
|
||||
return []
|
||||
|
||||
def bonus(self) -> int:
|
||||
return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE)
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing) -> bool:
|
||||
return self.operation.is_successfull(debriefing)
|
||||
def generate(self) -> UnitMap:
|
||||
Operation.prepare(self.game)
|
||||
unit_map = Operation.generate()
|
||||
Operation.current_mission.save(
|
||||
persistency.mission_path_for("liberation_nextturn.miz"))
|
||||
return unit_map
|
||||
|
||||
def generate(self):
|
||||
self.operation.is_awacs_enabled = self.is_awacs_enabled
|
||||
@staticmethod
|
||||
def _transfer_aircraft(ato: AirTaskingOrder, losses: AirLosses,
|
||||
for_player: bool) -> None:
|
||||
for package in ato.packages:
|
||||
for flight in package.flights:
|
||||
# No need to transfer to the same location.
|
||||
if flight.departure == flight.arrival:
|
||||
continue
|
||||
|
||||
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.environment_settings = self.operation.environment_settings
|
||||
# Don't transfer to bases that were captured. Note that if the
|
||||
# airfield was back-filling transfers it may overflow. We could
|
||||
# attempt to be smarter in the future by performing transfers in
|
||||
# order up a graph to prevent transfers to full airports and
|
||||
# send overflow off-map, but overflow is fine for now.
|
||||
if flight.arrival.captured != for_player:
|
||||
logging.info(
|
||||
f"Not transferring {flight} because {flight.arrival} "
|
||||
"was captured")
|
||||
continue
|
||||
|
||||
def generate_quick(self):
|
||||
self.operation.is_awacs_enabled = self.is_awacs_enabled
|
||||
self.operation.environment_settings = self.environment_settings
|
||||
transfer_count = losses.surviving_flight_members(flight)
|
||||
if transfer_count < 0:
|
||||
logging.error(f"{flight} had {flight.count} aircraft but "
|
||||
f"{transfer_count} losses were recorded.")
|
||||
continue
|
||||
|
||||
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"))
|
||||
aircraft = flight.unit_type
|
||||
available = flight.departure.base.total_units_of_type(aircraft)
|
||||
if available < transfer_count:
|
||||
logging.error(
|
||||
f"Found killed {aircraft} from {flight.departure} but "
|
||||
f"that airbase has only {available} available.")
|
||||
continue
|
||||
|
||||
flight.departure.base.aircraft[aircraft] -= transfer_count
|
||||
if aircraft not in flight.arrival.base.aircraft:
|
||||
# TODO: Should use defaultdict.
|
||||
flight.arrival.base.aircraft[aircraft] = 0
|
||||
flight.arrival.base.aircraft[aircraft] += transfer_count
|
||||
|
||||
def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
|
||||
self._transfer_aircraft(self.game.blue_ato, debriefing.air_losses,
|
||||
for_player=True)
|
||||
self._transfer_aircraft(self.game.red_ato, debriefing.air_losses,
|
||||
for_player=False)
|
||||
|
||||
@staticmethod
|
||||
def commit_air_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.air_losses.losses:
|
||||
aircraft = loss.unit_type
|
||||
cp = loss.departure
|
||||
available = cp.base.total_units_of_type(aircraft)
|
||||
if available <= 0:
|
||||
logging.error(
|
||||
f"Found killed {aircraft} from {cp} but that airbase has "
|
||||
"none available.")
|
||||
continue
|
||||
|
||||
logging.info(f"{aircraft} destroyed from {cp}")
|
||||
cp.base.aircraft[aircraft] -= 1
|
||||
|
||||
@staticmethod
|
||||
def commit_front_line_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.front_line_losses:
|
||||
unit_type = loss.unit_type
|
||||
control_point = loss.origin
|
||||
available = control_point.base.total_units_of_type(unit_type)
|
||||
if available <= 0:
|
||||
logging.error(
|
||||
f"Found killed {unit_type} from {control_point} but that "
|
||||
"airbase has none available.")
|
||||
continue
|
||||
|
||||
logging.info(f"{unit_type} destroyed from {control_point}")
|
||||
control_point.base.armor[unit_type] -= 1
|
||||
|
||||
@staticmethod
|
||||
def commit_ground_object_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.ground_object_losses:
|
||||
# TODO: This should be stored in the TGO, not in the pydcs Group.
|
||||
if not hasattr(loss.group, "units_losts"):
|
||||
loss.group.units_losts = []
|
||||
|
||||
loss.group.units.remove(loss.unit)
|
||||
loss.group.units_losts.append(loss.unit)
|
||||
if not loss.ground_object.alive_unit_count:
|
||||
loss.ground_object.is_dead = True
|
||||
|
||||
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.building_losses:
|
||||
loss.ground_object.is_dead = True
|
||||
self.game.informations.append(Information(
|
||||
"Building destroyed",
|
||||
f"{loss.ground_object.dcs_identifier} has been destroyed at "
|
||||
f"location {loss.ground_object.obj_name}", self.game.turn
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
def commit_damaged_runways(debriefing: Debriefing) -> None:
|
||||
for damaged_runway in debriefing.damaged_runways:
|
||||
damaged_runway.damage_runway()
|
||||
|
||||
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
|
||||
logging.info("Committing mission results")
|
||||
|
||||
cp.base.commit_losses(losses)
|
||||
self.commit_air_losses(debriefing)
|
||||
self.commit_front_line_losses(debriefing)
|
||||
self.commit_ground_object_losses(debriefing)
|
||||
self.commit_building_losses(debriefing)
|
||||
self.commit_damaged_runways(debriefing)
|
||||
|
||||
# ------------------------------
|
||||
# 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:
|
||||
logging.exception(f"Could not process base capture {captured}")
|
||||
|
||||
self.complete_aircraft_transfers(debriefing)
|
||||
|
||||
# Destroyed units carcass
|
||||
# -------------------------
|
||||
for destroyed_unit in debriefing.state_data.destroyed_statics:
|
||||
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 = debriefing.casualty_count(cp)
|
||||
enemy_casualties = debriefing.casualty_count(enemy_cp)
|
||||
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,
|
||||
def __init__(self, attacker_name: str, defender_name: str,
|
||||
from_cp: ControlPoint, to_cp: ControlPoint,
|
||||
game: Game) -> None:
|
||||
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[Type[UnitType], int] = {}
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return "Pending delivery to {}".format(self.to_cp)
|
||||
|
||||
def deliver(self, units: typing.Dict[UnitType, int]):
|
||||
def deliver(self, units: Dict[Type[UnitType], int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] = self.units.get(k, 0) + v
|
||||
|
||||
def skip(self):
|
||||
def skip(self) -> None:
|
||||
for k, v in self.units.items():
|
||||
if self.to_cp.captured:
|
||||
name = "Ally "
|
||||
else:
|
||||
name = "Enemy "
|
||||
self.game.message(
|
||||
f"{name} reinforcements: {k.id} x {v} at {self.to_cp.name}")
|
||||
|
||||
self.to_cp.base.commision_units(self.units)
|
||||
|
||||
11
game/event/frontlineattack.py
Normal file
11
game/event/frontlineattack.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .event import Event
|
||||
|
||||
|
||||
class FrontlineAttackEvent(Event):
|
||||
"""
|
||||
An event centered on a FrontLine Conflict.
|
||||
Currently the same as its parent, but here for legacy compatibility as well as to allow for
|
||||
future unique Event handling
|
||||
"""
|
||||
def __str__(self):
|
||||
return "Frontline attack"
|
||||
@@ -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
|
||||
284
game/factions/faction.py
Normal file
284
game/factions/faction.py
Normal file
@@ -0,0 +1,284 @@
|
||||
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[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available awacs aircraft
|
||||
awacs: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available tanker aircraft
|
||||
tankers: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available frontline units
|
||||
frontline_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Available artillery units
|
||||
artillery_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Infantry units used
|
||||
infantry_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Logistics units used
|
||||
logistics_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Possible SAMS site generators for this faction
|
||||
air_defenses: 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[Type[UnitType]] = field(default_factory=list)
|
||||
|
||||
# possible helicopter carrier units
|
||||
helicopter_carrier: List[Type[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[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# Available cruisers
|
||||
cruisers: List[Type[ShipType]] = 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[Type[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[Type[UnitType], List[str]] = field(
|
||||
default_factory=dict)
|
||||
|
||||
#: Set to True if the faction should force the "Unrestricted satnav" option
|
||||
#: for the mission. This option enables GPS for capable aircraft regardless
|
||||
#: of the time period or operator. For example, the CJTF "countries" don't
|
||||
#: appear to have GPS capability, so they need this.
|
||||
#:
|
||||
#: Note that this option cannot be set per-side. If either faction needs it,
|
||||
#: both will use it.
|
||||
unrestricted_satnav: bool = False
|
||||
|
||||
@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.ewrs = json.get("ewrs", [])
|
||||
|
||||
faction.air_defenses = json.get("air_defenses", [])
|
||||
# Compatibility for older factions. All air defenses now belong to a
|
||||
# single group and the generator decides what belongs where.
|
||||
faction.air_defenses.extend(json.get("sams", []))
|
||||
faction.air_defenses.extend(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]
|
||||
|
||||
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
|
||||
|
||||
return faction
|
||||
|
||||
@property
|
||||
def units(self) -> List[Type[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[Type[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[Type[FlyingType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(
|
||||
name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]
|
||||
))
|
||||
|
||||
|
||||
def load_all_aircraft(data) -> List[Type[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[Type[VehicleType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(
|
||||
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
|
||||
))
|
||||
|
||||
|
||||
def load_all_vehicles(data) -> List[Type[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[Type[ShipType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(name, [dcs.ships]))
|
||||
|
||||
|
||||
def load_all_ships(data) -> List[Type[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())
|
||||
533
game/game.py
533
game/game.py
@@ -1,16 +1,36 @@
|
||||
import typing
|
||||
import logging
|
||||
import random
|
||||
import math
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta
|
||||
from enum import Enum
|
||||
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
|
||||
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 . 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 .procurement import ProcurementAi
|
||||
from .settings import Settings
|
||||
from .event import *
|
||||
from .theater import ConflictTheater, ControlPoint
|
||||
from .unitmap import UnitMap
|
||||
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
|
||||
@@ -64,170 +62,397 @@ ENEMY_BASE_STRENGTH_RECOVERY = 0.05
|
||||
# cost of AWACS for single operation
|
||||
AWACS_BUDGET_COST = 4
|
||||
|
||||
# Initial budget value
|
||||
PLAYER_BUDGET_INITIAL = 120
|
||||
# Base post-turn bonus value
|
||||
PLAYER_BUDGET_BASE = 10
|
||||
# Bonus multiplier logarithm base
|
||||
PLAYER_BUDGET_IMPORTANCE_LOG = 2
|
||||
|
||||
class TurnState(Enum):
|
||||
WIN = 0
|
||||
LOSS = 1
|
||||
CONTINUE = 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, player_budget: int,
|
||||
enemy_budget: int) -> None:
|
||||
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: List[Point] = []
|
||||
self.__destroyed_units: List[str] = []
|
||||
self.savepath = ""
|
||||
self.budget = player_budget
|
||||
self.enemy_budget = enemy_budget
|
||||
self.current_unit_id = 0
|
||||
self.current_group_id = 0
|
||||
|
||||
def _roll(self, prob, mult):
|
||||
return random.randint(1, 100) <= prob * mult
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
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
|
||||
self.blue_ato = AirTaskingOrder()
|
||||
self.red_ato = AirTaskingOrder()
|
||||
|
||||
def _generate_events(self):
|
||||
enemy_cap_generated = False
|
||||
enemy_generated_types = []
|
||||
self.aircraft_inventory = GlobalAircraftInventory(
|
||||
self.theater.controlpoints
|
||||
)
|
||||
|
||||
for player_cp, enemy_cp in self.theater.conflicts(True):
|
||||
if player_cp.is_global or enemy_cp.is_global:
|
||||
continue
|
||||
for cp in self.theater.controlpoints:
|
||||
cp.pending_unit_deliveries = self.units_delivery_event(cp)
|
||||
|
||||
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
|
||||
self.sanitize_sides()
|
||||
|
||||
if player_cp in self.ignored_cps:
|
||||
continue
|
||||
self.on_load()
|
||||
|
||||
if enemy_cp.base.total_planes == 0:
|
||||
continue
|
||||
# Turn 0 procurement. We don't actually have any missions to plan, but
|
||||
# the planner will tell us what it would like to plan so we can use that
|
||||
# to drive purchase decisions.
|
||||
blue_planner = CoalitionMissionPlanner(self, is_player=True)
|
||||
blue_planner.plan_missions()
|
||||
|
||||
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
|
||||
red_planner = CoalitionMissionPlanner(self, is_player=False)
|
||||
red_planner.plan_missions()
|
||||
|
||||
enemy_generated_types.append(event_class)
|
||||
self.events.append(event_class(self.enemy, self.player, enemy_cp, player_cp, self))
|
||||
self.plan_procurement(blue_planner, red_planner)
|
||||
|
||||
def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> typing.Collection[UnitType]:
|
||||
importance_factor = (cp.importance - IMPORTANCE_LOW) / (IMPORTANCE_HIGH - IMPORTANCE_LOW)
|
||||
def generate_conditions(self) -> Conditions:
|
||||
return Conditions.generate(self.theater, self.date,
|
||||
self.current_turn_time_of_day, self.settings)
|
||||
|
||||
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)
|
||||
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 budget_reward_amount(self):
|
||||
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)
|
||||
else:
|
||||
return 0
|
||||
def player_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.player_name]
|
||||
|
||||
def _budget_player(self):
|
||||
@property
|
||||
def enemy_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.enemy_name]
|
||||
|
||||
def _roll(self, 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_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):
|
||||
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) -> int:
|
||||
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 += REWARDS[g.category]
|
||||
return int(reward * self.settings.player_income_multiplier)
|
||||
|
||||
def process_player_income(self):
|
||||
self.budget += self.budget_reward_amount
|
||||
|
||||
def awacs_expense_commit(self):
|
||||
self.budget -= AWACS_BUDGET_COST
|
||||
def process_enemy_income(self):
|
||||
# TODO: Clean up save compat.
|
||||
if not hasattr(self, "enemy_budget"):
|
||||
self.enemy_budget = 0
|
||||
|
||||
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]
|
||||
|
||||
self.enemy_budget += production * self.settings.enemy_income_multiplier
|
||||
|
||||
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)
|
||||
self.events.append(event)
|
||||
return event
|
||||
|
||||
def units_delivery_remove(self, event: Event):
|
||||
if event in self.events:
|
||||
self.events.remove(event)
|
||||
|
||||
def initiate_event(self, event: Event):
|
||||
assert event in self.events
|
||||
|
||||
event.generate()
|
||||
event.generate_quick()
|
||||
def initiate_event(self, event: Event) -> UnitMap:
|
||||
#assert event in self.events
|
||||
logging.info("Generating {} (regular)".format(event))
|
||||
return event.generate()
|
||||
|
||||
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()
|
||||
self.budget += int(event.bonus() *
|
||||
self.settings.player_income_multiplier)
|
||||
|
||||
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:
|
||||
raise RuntimeError(f"{event} was passed when an Event type was expected")
|
||||
|
||||
def on_load(self) -> None:
|
||||
LuaPluginManager.load_settings(self.settings)
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
self.compute_conflicts_position()
|
||||
|
||||
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 control_point in self.theater.controlpoints:
|
||||
control_point.process_turn()
|
||||
|
||||
for cp in self.theater.enemy_points():
|
||||
self._commision_units(cp)
|
||||
self.process_enemy_income()
|
||||
|
||||
self.process_player_income()
|
||||
|
||||
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 check_win_loss(self):
|
||||
captured_states = {i.captured for i in self.theater.controlpoints}
|
||||
if True not in captured_states:
|
||||
return TurnState.LOSS
|
||||
if False not in captured_states:
|
||||
return TurnState.WIN
|
||||
return TurnState.CONTINUE
|
||||
|
||||
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:
|
||||
cp.pending_unit_deliveries = self.units_delivery_event(cp)
|
||||
self.aircraft_inventory.set_from_control_point(cp)
|
||||
|
||||
# Check for win or loss condition
|
||||
turn_state = self.check_win_loss()
|
||||
if turn_state in (TurnState.LOSS,TurnState.WIN):
|
||||
return self.process_win_loss(turn_state)
|
||||
|
||||
# Plan flights & combat for next turn
|
||||
self.compute_conflicts_position()
|
||||
self.ground_planners = {}
|
||||
self.blue_ato.clear()
|
||||
self.red_ato.clear()
|
||||
|
||||
blue_planner = CoalitionMissionPlanner(self, is_player=True)
|
||||
blue_planner.plan_missions()
|
||||
|
||||
red_planner = CoalitionMissionPlanner(self, is_player=False)
|
||||
red_planner.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
|
||||
|
||||
self.plan_procurement(blue_planner, red_planner)
|
||||
|
||||
def plan_procurement(self, blue_planner: CoalitionMissionPlanner,
|
||||
red_planner: CoalitionMissionPlanner) -> None:
|
||||
self.budget = ProcurementAi(
|
||||
self,
|
||||
for_player=True,
|
||||
faction=self.player_faction,
|
||||
manage_runways=self.settings.automate_runway_repair,
|
||||
manage_front_line=self.settings.automate_front_line_reinforcements,
|
||||
manage_aircraft=self.settings.automate_aircraft_reinforcements
|
||||
).spend_budget(self.budget, blue_planner.procurement_requests)
|
||||
|
||||
self.enemy_budget = ProcurementAi(
|
||||
self,
|
||||
for_player=False,
|
||||
faction=self.enemy_faction,
|
||||
manage_runways=True,
|
||||
manage_front_line=True,
|
||||
manage_aircraft=True
|
||||
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
|
||||
|
||||
def message(self, text: str) -> None:
|
||||
self.informations.append(Information(text, turn=self.turn))
|
||||
|
||||
@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(front_line.control_point_a,
|
||||
front_line.control_point_b,
|
||||
self.theater)
|
||||
points.append(position[0])
|
||||
points.append(front_line.control_point_a.position)
|
||||
points.append(front_line.control_point_b.position)
|
||||
|
||||
# If do_not_cull_carrier is enabled, add carriers as culling point
|
||||
if self.settings.perf_do_not_cull_carrier:
|
||||
for cp in self.theater.controlpoints:
|
||||
if cp.is_carrier or cp.is_lha:
|
||||
points.append(cp.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)
|
||||
|
||||
for package in self.blue_ato.packages:
|
||||
points.append(package.target.position)
|
||||
for package in self.red_ato.packages:
|
||||
points.append(package.target.position)
|
||||
|
||||
# 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))
|
||||
|
||||
self.__culling_points = 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"
|
||||
|
||||
def process_win_loss(self, turn_state: TurnState):
|
||||
if turn_state is TurnState.WIN:
|
||||
return self.message("Congratulations, you are victorious! Start a new campaign to continue.")
|
||||
elif turn_state is TurnState.LOSS:
|
||||
return self.message("Game Over, you lose. Start a new campaign to continue.")
|
||||
|
||||
17
game/infos/information.py
Normal file
17
game/infos/information.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import datetime
|
||||
|
||||
class Information():
|
||||
|
||||
def __init__(self, title="", text="", turn=0):
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.turn = turn
|
||||
self.timestamp = datetime.datetime.now()
|
||||
|
||||
def __str__(self):
|
||||
return '[{}][{}] {} {}'.format(
|
||||
self.timestamp.strftime("%Y-%m-%d %H:%M:%S") if self.timestamp is not None else '',
|
||||
self.turn,
|
||||
self.title,
|
||||
self.text
|
||||
)
|
||||
129
game/inventory.py
Normal file
129
game/inventory.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Inventory management APIs."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.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[Type[FlyingType], int] = defaultdict(int)
|
||||
|
||||
def add_aircraft(self, aircraft: Type[FlyingType], 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: Type[FlyingType], 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: Type[FlyingType]) -> 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[Type[FlyingType]]:
|
||||
"""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[Type[FlyingType], 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[Type[FlyingType]]:
|
||||
"""Iterates over all aircraft types available to the player."""
|
||||
seen: Set[Type[FlyingType]] = set()
|
||||
for control_point, inventory in self.inventories.items():
|
||||
if control_point.captured:
|
||||
for aircraft in inventory.types_available:
|
||||
if not control_point.can_operate(aircraft):
|
||||
continue
|
||||
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 game.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()
|
||||
|
||||
@@ -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,567 @@
|
||||
from dcs.terrain import Terrain
|
||||
from __future__ import annotations
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
from userdata.debriefing import *
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Iterable, List, Optional, Set
|
||||
|
||||
from theater import *
|
||||
from gen 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.translation import String
|
||||
from dcs.triggers import TriggerStart
|
||||
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 .. import db
|
||||
from ..debriefing import Debriefing
|
||||
from ..theater import Airfield
|
||||
from ..unitmap import UnitMap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
class Operation:
|
||||
attackers_starting_position = None # type: db.StartingPosition
|
||||
defenders_starting_position = None # type: db.StartingPosition
|
||||
mission = None # type: dcs.Mission
|
||||
conflict = None # type: Conflict
|
||||
armorgen = None # type: ArmorConflictGenerator
|
||||
"""Static class for managing the final Mission generation"""
|
||||
current_mission = None # type: Mission
|
||||
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
|
||||
game = None # type: Game
|
||||
environment_settings = None
|
||||
trigger_radius = TRIGGER_RADIUS_MEDIUM
|
||||
is_quick = None
|
||||
is_awacs_enabled = False
|
||||
player_awacs_enabled = True
|
||||
# TODO: #436 Generate Air Support for red
|
||||
enemy_awacs_enabled = True
|
||||
ca_slots = 1
|
||||
unit_map: UnitMap
|
||||
jtacs: List[JtacInfo] = []
|
||||
plugin_scripts: List[str] = []
|
||||
|
||||
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):
|
||||
self.game = game
|
||||
self.attacker_name = attacker_name
|
||||
self.defender_name = defender_name
|
||||
self.attacker_clients = attacker_clients
|
||||
self.defender_clients = defender_clients
|
||||
self.from_cp = from_cp
|
||||
self.to_cp = to_cp
|
||||
self.is_quick = False
|
||||
@classmethod
|
||||
def prepare(cls, game: Game):
|
||||
with open("resources/default_options.lua", "r") as f:
|
||||
options_dict = loads(f.read())["options"]
|
||||
cls._set_mission(Mission(game.theater.terrain))
|
||||
cls.game = game
|
||||
cls._setup_mission_coalitions()
|
||||
cls.current_mission.options.load_from_dict(options_dict)
|
||||
|
||||
def initialize(self, mission: Mission, conflict: Conflict):
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
@classmethod
|
||||
def conflicts(cls) -> Iterable[Conflict]:
|
||||
assert cls.game
|
||||
for frontline in cls.game.theater.conflicts():
|
||||
yield Conflict(
|
||||
cls.game.theater,
|
||||
frontline.control_point_a,
|
||||
frontline.control_point_b,
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
cls.game.player_country,
|
||||
cls.game.enemy_country,
|
||||
frontline.position
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def air_conflict(cls) -> Conflict:
|
||||
assert cls.game
|
||||
player_cp, enemy_cp = cls.game.theater.closest_opposing_control_points()
|
||||
mid_point = player_cp.position.point_from_heading(
|
||||
player_cp.position.heading_between_point(enemy_cp.position),
|
||||
player_cp.position.distance_to_point(enemy_cp.position) / 2
|
||||
)
|
||||
return Conflict(
|
||||
cls.game.theater,
|
||||
player_cp,
|
||||
enemy_cp,
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
cls.game.player_country,
|
||||
cls.game.enemy_country,
|
||||
mid_point
|
||||
)
|
||||
|
||||
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)
|
||||
@classmethod
|
||||
def _set_mission(cls, mission: Mission) -> None:
|
||||
cls.current_mission = mission
|
||||
|
||||
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)
|
||||
@classmethod
|
||||
def _setup_mission_coalitions(cls):
|
||||
cls.current_mission.coalition["blue"] = Coalition("blue")
|
||||
cls.current_mission.coalition["red"] = Coalition("red")
|
||||
|
||||
def prepare(self, terrain: Terrain, is_quick: bool):
|
||||
self.mission = dcs.Mission(terrain)
|
||||
self.is_quick = is_quick
|
||||
p_country = cls.game.player_country
|
||||
e_country = cls.game.enemy_country
|
||||
cls.current_mission.coalition["blue"].add_country(
|
||||
country_dict[db.country_id_from_name(p_country)]())
|
||||
cls.current_mission.coalition["red"].add_country(
|
||||
country_dict[db.country_id_from_name(e_country)]())
|
||||
|
||||
if is_quick:
|
||||
self.attackers_starting_position = None
|
||||
self.defenders_starting_position = None
|
||||
@classmethod
|
||||
def inject_lua_trigger(cls, contents: str, comment: str) -> None:
|
||||
trigger = TriggerStart(comment=comment)
|
||||
trigger.add_action(DoScript(String(contents)))
|
||||
cls.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
@classmethod
|
||||
def bypass_plugin_script(cls, mnemonic: str) -> None:
|
||||
cls.plugin_scripts.append(mnemonic)
|
||||
|
||||
@classmethod
|
||||
def inject_plugin_script(cls, plugin_mnemonic: str, script: str,
|
||||
script_mnemonic: str) -> None:
|
||||
if script_mnemonic in cls.plugin_scripts:
|
||||
logging.debug(
|
||||
f"Skipping already loaded {script} for {plugin_mnemonic}"
|
||||
)
|
||||
else:
|
||||
self.attackers_starting_position = self.from_cp.at
|
||||
self.defenders_starting_position = self.to_cp.at
|
||||
cls.plugin_scripts.append(script_mnemonic)
|
||||
|
||||
def generate(self):
|
||||
self.visualgen.generate()
|
||||
plugin_path = Path("./resources/plugins", plugin_mnemonic)
|
||||
|
||||
if self.is_awacs_enabled:
|
||||
self.awacsgen.generate()
|
||||
script_path = Path(plugin_path, script)
|
||||
if not script_path.exists():
|
||||
logging.error(
|
||||
f"Cannot find {script_path} for plugin {plugin_mnemonic}"
|
||||
)
|
||||
return
|
||||
|
||||
self.extra_aagen.generate()
|
||||
self.triggersgen.generate(self.is_quick, self.trigger_radius)
|
||||
trigger = TriggerStart(comment=f"Load {script_mnemonic}")
|
||||
filename = script_path.resolve()
|
||||
fileref = cls.current_mission.map_resource.add_resource_file(
|
||||
filename)
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
cls.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
if self.environment_settings is None:
|
||||
self.environment_settings = self.envgen.generate()
|
||||
else:
|
||||
self.envgen.load(self.environment_settings)
|
||||
@classmethod
|
||||
def notify_info_generators(
|
||||
cls,
|
||||
groundobjectgen: GroundObjectsGenerator,
|
||||
airsupportgen: AirSupportConflictGenerator,
|
||||
jtacs: List[JtacInfo],
|
||||
airgen: AircraftConflictGenerator,
|
||||
):
|
||||
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)
|
||||
"""
|
||||
gens: List[MissionInfoGenerator] = [
|
||||
KneeboardGenerator(cls.current_mission, cls.game),
|
||||
BriefingGenerator(cls.current_mission, cls.game)
|
||||
]
|
||||
for gen in gens:
|
||||
for dynamic_runway in groundobjectgen.runways.values():
|
||||
gen.add_dynamic_runway(dynamic_runway)
|
||||
|
||||
for global_cp in self.game.theater.controlpoints:
|
||||
if not global_cp.is_global:
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
gen.add_tanker(tanker)
|
||||
|
||||
if cls.player_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()
|
||||
|
||||
@classmethod
|
||||
def create_unit_map(cls) -> None:
|
||||
cls.unit_map = UnitMap()
|
||||
for control_point in cls.game.theater.controlpoints:
|
||||
if isinstance(control_point, Airfield):
|
||||
cls.unit_map.add_airfield(control_point)
|
||||
|
||||
@classmethod
|
||||
def create_radio_registries(cls) -> None:
|
||||
unique_map_frequencies = set() # type: Set[RadioFrequency]
|
||||
cls._create_tacan_registry(unique_map_frequencies)
|
||||
cls._create_radio_registry(unique_map_frequencies)
|
||||
|
||||
@classmethod
|
||||
def assign_channels_to_flights(cls, flights: List[FlightData],
|
||||
air_support: AirSupport) -> None:
|
||||
"""Assigns preset radio channels for client flights."""
|
||||
for flight in flights:
|
||||
if not flight.client_units:
|
||||
continue
|
||||
cls.assign_channels_to_flight(flight, air_support)
|
||||
|
||||
@staticmethod
|
||||
def assign_channels_to_flight(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
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _create_tacan_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None:
|
||||
"""
|
||||
Dedup beacon/radio frequencies, since some maps have some frequencies
|
||||
used multiple times.
|
||||
"""
|
||||
cls.tacan_registry = TacanRegistry()
|
||||
beacons = load_beacons_for_terrain(cls.game.theater.terrain.name)
|
||||
|
||||
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:
|
||||
cls.tacan_registry.reserve(beacon.tacan_channel)
|
||||
|
||||
@classmethod
|
||||
def _create_radio_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None:
|
||||
cls.radio_registry = RadioRegistry()
|
||||
for data in AIRFIELD_DATA.values():
|
||||
if data.theater == cls.game.theater.terrain.name and data.atc:
|
||||
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.
|
||||
|
||||
@classmethod
|
||||
def _generate_ground_units(cls):
|
||||
cls.groundobjectgen = GroundObjectsGenerator(
|
||||
cls.current_mission,
|
||||
cls.game,
|
||||
cls.radio_registry,
|
||||
cls.tacan_registry,
|
||||
cls.unit_map
|
||||
)
|
||||
cls.groundobjectgen.generate()
|
||||
|
||||
@classmethod
|
||||
def _generate_destroyed_units(cls) -> None:
|
||||
"""Add destroyed units to the Mission"""
|
||||
for d in cls.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 cls.game.position_culled(pos) and cls.game.settings.perf_destroyed_units:
|
||||
cls.current_mission.static_group(
|
||||
country=cls.current_mission.country(
|
||||
cls.game.player_country),
|
||||
name="",
|
||||
_type=utype,
|
||||
hidden=True,
|
||||
position=pos,
|
||||
heading=d["orientation"],
|
||||
dead=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate(cls) -> UnitMap:
|
||||
"""Build the final Mission to be exported"""
|
||||
cls.create_unit_map()
|
||||
cls.create_radio_registries()
|
||||
# Set mission time and weather conditions.
|
||||
EnvironmentGenerator(cls.current_mission,
|
||||
cls.game.conditions).generate()
|
||||
cls._generate_ground_units()
|
||||
cls._generate_destroyed_units()
|
||||
cls._generate_air_units()
|
||||
cls.assign_channels_to_flights(cls.airgen.flights,
|
||||
cls.airsupportgen.air_support)
|
||||
cls._generate_ground_conflicts()
|
||||
|
||||
if global_cp == self.from_cp and not self.is_quick:
|
||||
self.attackers_starting_position = ship
|
||||
# Triggers
|
||||
triggersgen = TriggersGenerator(cls.current_mission, cls.game)
|
||||
triggersgen.generate()
|
||||
|
||||
def units_of(self, country_name: str) -> typing.Collection[UnitType]:
|
||||
return []
|
||||
# Setup combined arms parameters
|
||||
cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0
|
||||
if cls.game.player_country in [country.name for country in cls.current_mission.coalition["blue"].countries.values()]:
|
||||
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
|
||||
else:
|
||||
cls.current_mission.groundControl.red_tactical_commander = cls.ca_slots
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing) -> bool:
|
||||
return True
|
||||
# Options
|
||||
forcedoptionsgen = ForcedOptionsGenerator(
|
||||
cls.current_mission, cls.game)
|
||||
forcedoptionsgen.generate()
|
||||
|
||||
# Generate Visuals Smoke Effects
|
||||
visualgen = VisualGenerator(cls.current_mission, cls.game)
|
||||
if cls.game.settings.perf_smoke_gen:
|
||||
visualgen.generate()
|
||||
|
||||
cls.generate_lua(cls.airgen, cls.airsupportgen, cls.jtacs)
|
||||
|
||||
# Inject Plugins Lua Scripts and data
|
||||
cls.plugin_scripts.clear()
|
||||
for plugin in LuaPluginManager.plugins():
|
||||
if plugin.enabled:
|
||||
plugin.inject_scripts(cls)
|
||||
plugin.inject_configuration(cls)
|
||||
|
||||
cls.assign_channels_to_flights(cls.airgen.flights,
|
||||
cls.airsupportgen.air_support)
|
||||
cls.notify_info_generators(
|
||||
cls.groundobjectgen,
|
||||
cls.airsupportgen,
|
||||
cls.jtacs,
|
||||
cls.airgen
|
||||
)
|
||||
|
||||
return cls.unit_map
|
||||
|
||||
@classmethod
|
||||
def _generate_air_units(cls) -> None:
|
||||
"""Generate the air units for the Operation"""
|
||||
|
||||
# Air Support (Tanker & Awacs)
|
||||
assert cls.radio_registry and cls.tacan_registry
|
||||
cls.airsupportgen = AirSupportConflictGenerator(
|
||||
cls.current_mission, cls.air_conflict(), cls.game, cls.radio_registry,
|
||||
cls.tacan_registry)
|
||||
cls.airsupportgen.generate()
|
||||
|
||||
# Generate Aircraft Activity on the map
|
||||
cls.airgen = AircraftConflictGenerator(
|
||||
cls.current_mission, cls.game.settings, cls.game,
|
||||
cls.radio_registry, cls.unit_map)
|
||||
cls.airgen.clear_parking_slots()
|
||||
|
||||
cls.airgen.generate_flights(
|
||||
cls.current_mission.country(cls.game.player_country),
|
||||
cls.game.blue_ato,
|
||||
cls.groundobjectgen.runways
|
||||
)
|
||||
cls.airgen.generate_flights(
|
||||
cls.current_mission.country(cls.game.enemy_country),
|
||||
cls.game.red_ato,
|
||||
cls.groundobjectgen.runways
|
||||
)
|
||||
cls.airgen.spawn_unused_aircraft(
|
||||
cls.current_mission.country(cls.game.player_country),
|
||||
cls.current_mission.country(cls.game.enemy_country))
|
||||
|
||||
@classmethod
|
||||
def _generate_ground_conflicts(cls) -> None:
|
||||
"""For each frontline in the Operation, generate the ground conflicts and JTACs"""
|
||||
for front_line in cls.game.theater.conflicts(True):
|
||||
player_cp = front_line.control_point_a
|
||||
enemy_cp = front_line.control_point_b
|
||||
conflict = Conflict.frontline_cas_conflict(
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
cls.current_mission.country(cls.game.player_country),
|
||||
cls.current_mission.country(cls.game.enemy_country),
|
||||
player_cp,
|
||||
enemy_cp,
|
||||
cls.game.theater
|
||||
)
|
||||
# Generate frontline ops
|
||||
player_gp = cls.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id]
|
||||
enemy_gp = cls.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
|
||||
ground_conflict_gen = GroundConflictGenerator(
|
||||
cls.current_mission,
|
||||
conflict, cls.game,
|
||||
player_gp, enemy_gp,
|
||||
player_cp.stances[enemy_cp.id],
|
||||
cls.unit_map
|
||||
)
|
||||
ground_conflict_gen.generate()
|
||||
cls.jtacs.extend(ground_conflict_gen.jtacs)
|
||||
|
||||
@classmethod
|
||||
def generate_lua(cls, airgen: AircraftConflictGenerator,
|
||||
airsupportgen: AirSupportConflictGenerator,
|
||||
jtacs: List[JtacInfo]) -> None:
|
||||
# TODO: Refactor this
|
||||
luaData = {
|
||||
"AircraftCarriers": {},
|
||||
"Tankers": {},
|
||||
"AWACs": {},
|
||||
"JTACs": {},
|
||||
"TargetPoints": {},
|
||||
} # type: ignore
|
||||
|
||||
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 airsupportgen.air_support.awacs:
|
||||
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 = str(flight.flight_type)
|
||||
flightTarget = flight.package.target
|
||||
if flightTarget:
|
||||
flightTargetName = None
|
||||
flightTargetType = None
|
||||
if isinstance(flightTarget, TheaterGroundObject):
|
||||
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)))
|
||||
Operation.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
70
game/persistency.py
Normal file
70
game/persistency.py
Normal file
@@ -0,0 +1,70 @@
|
||||
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 _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 mission_path_for(name: str) -> str:
|
||||
return os.path.join(base_path(), "Missions", "{}".format(name))
|
||||
|
||||
|
||||
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, Type
|
||||
|
||||
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: Type[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: Type[Operation]) -> None:
|
||||
for work_order in self.definition.work_orders:
|
||||
work_order.work(operation)
|
||||
|
||||
def inject_configuration(self, operation: Type[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)
|
||||
194
game/procurement.py
Normal file
194
game/procurement.py
Normal file
@@ -0,0 +1,194 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
import random
|
||||
from typing import Iterator, List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.task import CAP, CAS
|
||||
from dcs.unittype import FlyingType, UnitType, VehicleType
|
||||
|
||||
from game import db
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from gen.flights.ai_flight_planner_db import (
|
||||
capable_aircraft_for_task,
|
||||
preferred_aircraft_for_task,
|
||||
)
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AircraftProcurementRequest:
|
||||
near: MissionTarget
|
||||
range: int
|
||||
task_capability: FlightType
|
||||
number: int
|
||||
|
||||
|
||||
class ProcurementAi:
|
||||
def __init__(self, game: Game, for_player: bool, faction: Faction,
|
||||
manage_runways: bool, manage_front_line: bool,
|
||||
manage_aircraft: bool) -> None:
|
||||
self.game = game
|
||||
self.is_player = for_player
|
||||
self.faction = faction
|
||||
self.manage_runways = manage_runways
|
||||
self.manage_front_line = manage_front_line
|
||||
self.manage_aircraft = manage_aircraft
|
||||
|
||||
def spend_budget(
|
||||
self, budget: int,
|
||||
aircraft_requests: List[AircraftProcurementRequest]) -> int:
|
||||
if self.manage_runways:
|
||||
budget = self.repair_runways(budget)
|
||||
if self.manage_front_line:
|
||||
armor_budget = math.ceil(budget / 2)
|
||||
budget -= armor_budget
|
||||
budget += self.reinforce_front_line(armor_budget)
|
||||
if self.manage_aircraft:
|
||||
budget = self.purchase_aircraft(budget, aircraft_requests)
|
||||
return budget
|
||||
|
||||
def repair_runways(self, budget: int) -> int:
|
||||
for control_point in self.owned_points:
|
||||
if budget < db.RUNWAY_REPAIR_COST:
|
||||
break
|
||||
if control_point.runway_can_be_repaired:
|
||||
control_point.begin_runway_repair()
|
||||
budget -= db.RUNWAY_REPAIR_COST
|
||||
if self.is_player:
|
||||
self.game.message(
|
||||
"OPFOR has begun repairing the runway at "
|
||||
f"{control_point}"
|
||||
)
|
||||
else:
|
||||
self.game.message(
|
||||
"We have begun repairing the runway at "
|
||||
f"{control_point}"
|
||||
)
|
||||
return budget
|
||||
|
||||
def random_affordable_ground_unit(
|
||||
self, budget: int) -> Optional[Type[VehicleType]]:
|
||||
affordable_units = [u for u in self.faction.frontline_units if
|
||||
db.PRICES[u] <= budget]
|
||||
if not affordable_units:
|
||||
return None
|
||||
return random.choice(affordable_units)
|
||||
|
||||
def reinforce_front_line(self, budget: int) -> int:
|
||||
if not self.faction.frontline_units:
|
||||
return budget
|
||||
|
||||
while budget > 0:
|
||||
candidates = self.front_line_candidates()
|
||||
if not candidates:
|
||||
break
|
||||
|
||||
cp = random.choice(candidates)
|
||||
unit = self.random_affordable_ground_unit(budget)
|
||||
if unit is None:
|
||||
# Can't afford any more units.
|
||||
break
|
||||
|
||||
budget -= db.PRICES[unit]
|
||||
assert cp.pending_unit_deliveries is not None
|
||||
cp.pending_unit_deliveries.deliver({unit: 1})
|
||||
|
||||
return budget
|
||||
|
||||
def _affordable_aircraft_of_types(
|
||||
self, types: List[Type[FlyingType]], airbase: ControlPoint,
|
||||
number: int, max_price: int) -> Optional[Type[FlyingType]]:
|
||||
unit_pool = [u for u in self.faction.aircrafts if u in types]
|
||||
affordable_units = [
|
||||
u for u in unit_pool
|
||||
if db.PRICES[u] * number <= max_price and airbase.can_operate(u)
|
||||
]
|
||||
if not affordable_units:
|
||||
return None
|
||||
return random.choice(affordable_units)
|
||||
|
||||
def affordable_aircraft_for(
|
||||
self, request: AircraftProcurementRequest,
|
||||
airbase: ControlPoint, budget: int) -> Optional[Type[FlyingType]]:
|
||||
aircraft = self._affordable_aircraft_of_types(
|
||||
preferred_aircraft_for_task(request.task_capability),
|
||||
airbase, request.number, budget)
|
||||
if aircraft is not None:
|
||||
return aircraft
|
||||
return self._affordable_aircraft_of_types(
|
||||
capable_aircraft_for_task(request.task_capability),
|
||||
airbase, request.number, budget)
|
||||
|
||||
def purchase_aircraft(
|
||||
self, budget: int,
|
||||
aircraft_requests: List[AircraftProcurementRequest]) -> int:
|
||||
unit_pool = [u for u in self.faction.aircrafts
|
||||
if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
|
||||
if not unit_pool:
|
||||
return budget
|
||||
|
||||
for request in aircraft_requests:
|
||||
for airbase in self.best_airbases_for(request):
|
||||
unit = self.affordable_aircraft_for(request, airbase, budget)
|
||||
if unit is None:
|
||||
# Can't afford any aircraft capable of performing the
|
||||
# required mission that can operate from this airbase. We
|
||||
# might be able to afford aircraft at other airbases though,
|
||||
# in the case where the airbase we attempted to use is only
|
||||
# able to operate expensive aircraft.
|
||||
continue
|
||||
|
||||
budget -= db.PRICES[unit] * request.number
|
||||
assert airbase.pending_unit_deliveries is not None
|
||||
airbase.pending_unit_deliveries.deliver({unit: request.number})
|
||||
|
||||
return budget
|
||||
|
||||
@property
|
||||
def owned_points(self) -> List[ControlPoint]:
|
||||
if self.is_player:
|
||||
return self.game.theater.player_points()
|
||||
else:
|
||||
return self.game.theater.enemy_points()
|
||||
|
||||
def best_airbases_for(
|
||||
self,
|
||||
request: AircraftProcurementRequest) -> Iterator[ControlPoint]:
|
||||
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
|
||||
request.near
|
||||
)
|
||||
for cp in distance_cache.airfields_within(request.range):
|
||||
if not cp.is_friendly(self.is_player):
|
||||
continue
|
||||
if not cp.runway_is_operational():
|
||||
continue
|
||||
if cp.unclaimed_parking(self.game) < request.number:
|
||||
continue
|
||||
yield cp
|
||||
|
||||
def front_line_candidates(self) -> List[ControlPoint]:
|
||||
candidates = []
|
||||
|
||||
# Prefer to buy front line units at active front lines that are not
|
||||
# already overloaded.
|
||||
for cp in self.owned_points:
|
||||
if cp.base.total_armor >= 30:
|
||||
# Control point is already sufficiently defended.
|
||||
continue
|
||||
for connected in cp.connected_points:
|
||||
if not connected.is_friendly(to_player=self.is_player):
|
||||
candidates.append(cp)
|
||||
|
||||
if not candidates:
|
||||
# Otherwise buy them anywhere valid.
|
||||
candidates = [p for p in self.owned_points
|
||||
if p.can_deploy_ground_units]
|
||||
|
||||
return candidates
|
||||
@@ -1,8 +1,79 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Optional
|
||||
|
||||
from dcs.forcedoptions import ForcedOptions
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
player_skill = "Good"
|
||||
enemy_skill = "Average"
|
||||
only_player_takeoff = False
|
||||
night_disabled = False
|
||||
multiplier = 1
|
||||
sams = True
|
||||
|
||||
# Difficulty settings
|
||||
player_skill: str = "Good"
|
||||
enemy_skill: str = "Average"
|
||||
enemy_vehicle_skill: str = "Average"
|
||||
map_coalition_visibility: ForcedOptions.Views = ForcedOptions.Views.All
|
||||
labels: str = "Full"
|
||||
only_player_takeoff: bool = True # Legacy parameter do not use
|
||||
night_disabled: bool = False
|
||||
external_views_allowed: bool = True
|
||||
supercarrier: bool = False
|
||||
generate_marks: bool = True
|
||||
manpads: bool = True
|
||||
cold_start: bool = False # Legacy parameter do not use
|
||||
version: Optional[str] = None
|
||||
player_income_multiplier: float = 1.0
|
||||
enemy_income_multiplier: float = 1.0
|
||||
|
||||
# Campaign management
|
||||
automate_runway_repair: bool = False
|
||||
automate_front_line_reinforcements: bool = False
|
||||
automate_aircraft_reinforcements: bool = False
|
||||
|
||||
# Performance oriented
|
||||
perf_red_alert_state: bool = True
|
||||
perf_smoke_gen: bool = True
|
||||
perf_artillery: bool = True
|
||||
perf_moving_units: bool = True
|
||||
perf_infantry: bool = True
|
||||
perf_ai_parking_start: bool = True
|
||||
perf_destroyed_units: bool = True
|
||||
|
||||
# Performance culling
|
||||
perf_culling: bool = False
|
||||
perf_culling_distance: int = 100
|
||||
perf_do_not_cull_carrier = True
|
||||
|
||||
# LUA Plugins system
|
||||
plugins: Dict[str, bool] = field(default_factory=dict)
|
||||
|
||||
# Cheating
|
||||
show_red_ato: bool = False
|
||||
|
||||
never_delay_player_flights: bool = 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)
|
||||
|
||||
5
game/theater/__init__.py
Normal file
5
game/theater/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .base import *
|
||||
from .conflicttheater import *
|
||||
from .controlpoint import *
|
||||
from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import SamGroundObject
|
||||
@@ -1,36 +1,35 @@
|
||||
import typing
|
||||
import math
|
||||
import itertools
|
||||
import logging
|
||||
import math
|
||||
import typing
|
||||
from typing import Dict, Type
|
||||
|
||||
from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task
|
||||
from dcs.unittype import FlyingType, UnitType, VehicleType
|
||||
from dcs.vehicles import AirDefence, Armor
|
||||
|
||||
from game import db
|
||||
from theater.controlpoint import ControlPoint
|
||||
|
||||
from dcs.planes import *
|
||||
from dcs.vehicles import *
|
||||
from dcs.task import *
|
||||
|
||||
STRENGTH_AA_ASSEMBLE_MIN = 0.2
|
||||
PLANES_SCRAMBLE_MIN_BASE = 4
|
||||
PLANES_SCRAMBLE_MIN_BASE = 2
|
||||
PLANES_SCRAMBLE_MAX_BASE = 8
|
||||
PLANES_SCRAMBLE_FACTOR = 0.6
|
||||
PLANES_SCRAMBLE_FACTOR = 0.3
|
||||
|
||||
BASE_MAX_STRENGTH = 1
|
||||
BASE_MIN_STRENGTH = 0
|
||||
|
||||
|
||||
class Base:
|
||||
aircraft = {} # type: typing.Dict[PlaneType, int]
|
||||
armor = {} # type: typing.Dict[Armor, int]
|
||||
aa = {} # type: typing.Dict[AirDefence, int]
|
||||
strength = 1 # type: float
|
||||
commision_points = {}
|
||||
|
||||
def __init__(self):
|
||||
self.aircraft = {}
|
||||
self.armor = {}
|
||||
self.aa = {}
|
||||
self.commision_points = {}
|
||||
self.aircraft: Dict[Type[FlyingType], int] = {}
|
||||
self.armor: Dict[Type[VehicleType], int] = {}
|
||||
self.aa: Dict[AirDefence, int] = {}
|
||||
self.commision_points: Dict[Type, float] = {}
|
||||
self.strength = 1
|
||||
|
||||
@property
|
||||
def total_planes(self) -> int:
|
||||
def total_aircraft(self) -> int:
|
||||
return sum(self.aircraft.values())
|
||||
|
||||
@property
|
||||
@@ -51,17 +50,19 @@ class Base:
|
||||
def all_units(self):
|
||||
return itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items())
|
||||
|
||||
def _find_best_unit(self, dict, for_type: Task, count: int) -> typing.Dict:
|
||||
def _find_best_unit(self, available_units: Dict[UnitType, int],
|
||||
for_type: Task, count: int) -> Dict[UnitType, int]:
|
||||
if count <= 0:
|
||||
print("{}: no units for {}".format(self, for_type))
|
||||
logging.warning("{}: no units for {}".format(self, for_type))
|
||||
return {}
|
||||
|
||||
sorted_units = [key for key in dict.keys() if key in db.UNIT_BY_TASK[for_type]]
|
||||
sorted_units = [key for key in available_units if
|
||||
key in db.UNIT_BY_TASK[for_type]]
|
||||
sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True)
|
||||
|
||||
result = {}
|
||||
result: Dict[UnitType, int] = {}
|
||||
for unit_type in sorted_units:
|
||||
existing_count = dict[unit_type] # type: int
|
||||
existing_count = available_units[unit_type] # type: int
|
||||
if not existing_count:
|
||||
continue
|
||||
|
||||
@@ -74,10 +75,10 @@ class Base:
|
||||
assert result_unit_count > 0
|
||||
result[unit_type] = result.get(unit_type, 0) + result_unit_count
|
||||
|
||||
print("{} for {} ({}): {}".format(self, for_type, count, result))
|
||||
logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
|
||||
return result
|
||||
|
||||
def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[PlaneType, int]:
|
||||
def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[FlyingType, int]:
|
||||
return self._find_best_unit(self.aircraft, for_type, count)
|
||||
|
||||
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
|
||||
@@ -112,21 +113,25 @@ class Base:
|
||||
elif for_task == AirDefence:
|
||||
target_dict = self.aa
|
||||
|
||||
assert target_dict is not None
|
||||
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
||||
if target_dict is not None:
|
||||
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
||||
else:
|
||||
logging.error("Unable to determine target dict for " + str(unit_type))
|
||||
|
||||
def commit_losses(self, units_lost: typing.Dict[typing.Any, int]):
|
||||
|
||||
for unit_type, count in units_lost.items():
|
||||
|
||||
if unit_type in self.aircraft:
|
||||
target_array = self.aircraft
|
||||
elif unit_type in self.armor:
|
||||
target_array = self.armor
|
||||
elif unit_type in self.aa:
|
||||
target_array = self.aa
|
||||
else:
|
||||
print("Base didn't find event type {}".format(unit_type))
|
||||
continue
|
||||
|
||||
if unit_type not in target_array:
|
||||
print("Base didn't find event type {}".format(unit_type))
|
||||
continue
|
||||
|
||||
target_array[unit_type] = max(target_array[unit_type] - count, 0)
|
||||
@@ -135,43 +140,50 @@ class Base:
|
||||
|
||||
def affect_strength(self, amount):
|
||||
self.strength += amount
|
||||
if self.strength > 1:
|
||||
self.strength = 1
|
||||
elif self.strength < 0:
|
||||
self.strength = 0.001
|
||||
if self.strength > BASE_MAX_STRENGTH:
|
||||
self.strength = BASE_MAX_STRENGTH
|
||||
elif self.strength <= 0:
|
||||
self.strength = BASE_MIN_STRENGTH
|
||||
|
||||
def set_strength_to_minimum(self) -> None:
|
||||
self.strength = BASE_MIN_STRENGTH
|
||||
|
||||
def scramble_count(self, multiplier: float, task: Task = None) -> int:
|
||||
if task:
|
||||
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task])
|
||||
else:
|
||||
count = self.total_planes
|
||||
count = self.total_aircraft
|
||||
|
||||
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
|
||||
return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count)
|
||||
|
||||
def assemble_count(self):
|
||||
return int(self.total_armor * min(self.strength + 0.5, 1))
|
||||
return int(self.total_armor * 0.5)
|
||||
|
||||
def assemble_aa_count(self) -> int:
|
||||
if self.strength > STRENGTH_AA_ASSEMBLE_MIN:
|
||||
return self.total_aa
|
||||
else:
|
||||
return 0
|
||||
# previous logic removed because we always want the full air defense capabilities.
|
||||
return self.total_aa
|
||||
|
||||
def scramble_sweep(self, multiplier: float) -> typing.Dict[PlaneType, int]:
|
||||
def scramble_sweep(self, multiplier: float) -> typing.Dict[FlyingType, int]:
|
||||
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
|
||||
|
||||
def scramble_cas(self, multiplier: float) -> typing.Dict[PlaneType, int]:
|
||||
def scramble_last_defense(self):
|
||||
# return as many CAP-capable aircraft as we can since this is the last defense of the base
|
||||
# (but not more than 20 - that's just nuts)
|
||||
return self._find_best_planes(CAP, min(self.total_aircraft, 20))
|
||||
|
||||
def scramble_cas(self, multiplier: float) -> typing.Dict[FlyingType, int]:
|
||||
return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS))
|
||||
|
||||
def scramble_interceptors(self, multiplier: float) -> typing.Dict[PlaneType, int]:
|
||||
def scramble_interceptors(self, multiplier: float) -> typing.Dict[FlyingType, int]:
|
||||
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
|
||||
|
||||
def assemble_cap(self) -> typing.Dict[Armor, int]:
|
||||
def assemble_attack(self) -> typing.Dict[Armor, int]:
|
||||
return self._find_best_armor(PinpointStrike, self.assemble_count())
|
||||
|
||||
def assemble_defense(self) -> typing.Dict[Armor, int]:
|
||||
return self._find_best_armor(PinpointStrike, self.assemble_count())
|
||||
count = int(self.total_armor * min(self.strength + 0.5, 1))
|
||||
return self._find_best_armor(PinpointStrike, count)
|
||||
|
||||
def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]:
|
||||
return self._find_best_unit(self.aa, AirDefence, count and min(count, self.total_aa) or self.assemble_aa_count())
|
||||
936
game/theater/conflicttheater.py
Normal file
936
game/theater/conflicttheater.py
Normal file
@@ -0,0 +1,936 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from itertools import tee
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union, cast
|
||||
|
||||
from shapely import geometry
|
||||
from shapely import ops
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.countries import (
|
||||
CombinedJointTaskForcesBlue,
|
||||
CombinedJointTaskForcesRed,
|
||||
)
|
||||
from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
from dcs.planes import F_15C
|
||||
from dcs.ships import (
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
)
|
||||
from dcs.statics import Fortification
|
||||
from dcs.terrain import (
|
||||
caucasus,
|
||||
nevada,
|
||||
normandy,
|
||||
persiangulf,
|
||||
syria,
|
||||
thechannel,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, Terrain
|
||||
from dcs.unitgroup import (
|
||||
FlyingGroup,
|
||||
Group,
|
||||
ShipGroup,
|
||||
StaticGroup,
|
||||
VehicleGroup,
|
||||
)
|
||||
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
||||
|
||||
from gen.flights.flight import FlightType
|
||||
from .controlpoint import (
|
||||
Airfield,
|
||||
Carrier,
|
||||
ControlPoint,
|
||||
Lha,
|
||||
MissionTarget,
|
||||
OffMapSpawn,
|
||||
Fob,
|
||||
)
|
||||
from .landmap import Landmap, load_landmap, poly_contains
|
||||
from ..utils import nm_to_meter
|
||||
|
||||
Numeric = Union[int, float]
|
||||
|
||||
SIZE_TINY = 150
|
||||
SIZE_SMALL = 600
|
||||
SIZE_REGULAR = 1000
|
||||
SIZE_BIG = 2000
|
||||
SIZE_LARGE = 3000
|
||||
|
||||
IMPORTANCE_LOW = 1
|
||||
IMPORTANCE_MEDIUM = 1.2
|
||||
IMPORTANCE_HIGH = 1.4
|
||||
|
||||
FRONTLINE_MIN_CP_DISTANCE = 5000
|
||||
|
||||
def pairwise(iterable):
|
||||
"""
|
||||
itertools recipe
|
||||
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
||||
"""
|
||||
a, b = tee(iterable)
|
||||
next(b, None)
|
||||
return zip(a, b)
|
||||
|
||||
|
||||
class MizCampaignLoader:
|
||||
BLUE_COUNTRY = CombinedJointTaskForcesBlue()
|
||||
RED_COUNTRY = CombinedJointTaskForcesRed()
|
||||
|
||||
OFF_MAP_UNIT_TYPE = F_15C.id
|
||||
|
||||
CV_UNIT_TYPE = CVN_74_John_C__Stennis.id
|
||||
LHA_UNIT_TYPE = LHA_1_Tarawa.id
|
||||
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
|
||||
|
||||
FOB_UNIT_TYPE = Unarmed.CP_SKP_11_ATC_Mobile_Command_Post.id
|
||||
|
||||
EWR_UNIT_TYPE = AirDefence.EWR_55G6.id
|
||||
SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300PS_SR_64H6E.id
|
||||
GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_2S6.id
|
||||
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
|
||||
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
|
||||
MISSILE_SITE_UNIT_TYPE = MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M.id
|
||||
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.SS_N_2_Silkworm.id
|
||||
|
||||
# Multiple options for the required SAMs so campaign designers can more
|
||||
# accurately see the coverage of their IADS for the expected type.
|
||||
REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.SAM_Patriot_LN_M901.id,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85C.id,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85D.id,
|
||||
}
|
||||
|
||||
REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.SAM_Hawk_LN_M192.id,
|
||||
AirDefence.SAM_SA_2_LN_SM_90.id,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73.id,
|
||||
}
|
||||
|
||||
BASE_DEFENSE_RADIUS = nm_to_meter(2)
|
||||
|
||||
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
|
||||
self.theater = theater
|
||||
self.mission = Mission()
|
||||
self.mission.load_file(str(miz))
|
||||
self.control_point_id = itertools.count(1000)
|
||||
|
||||
# If there are no red carriers there usually aren't red units. Make sure
|
||||
# both countries are initialized so we don't have to deal with None.
|
||||
if self.mission.country(self.BLUE_COUNTRY.name) is None:
|
||||
self.mission.coalition["blue"].add_country(self.BLUE_COUNTRY)
|
||||
if self.mission.country(self.RED_COUNTRY.name) is None:
|
||||
self.mission.coalition["red"].add_country(self.RED_COUNTRY)
|
||||
|
||||
@staticmethod
|
||||
def control_point_from_airport(airport: Airport) -> ControlPoint:
|
||||
|
||||
# The wiki says this is a legacy property and to just use regular.
|
||||
size = SIZE_REGULAR
|
||||
|
||||
# The importance is taken from the periodicity of the airport's
|
||||
# warehouse divided by 10. 30 is the default, and out of range (valid
|
||||
# values are between 1.0 and 1.4). If it is used, pick the default
|
||||
# importance.
|
||||
if airport.periodicity == 30:
|
||||
importance = IMPORTANCE_MEDIUM
|
||||
else:
|
||||
importance = airport.periodicity / 10
|
||||
|
||||
cp = Airfield(airport, size, importance)
|
||||
cp.captured = airport.is_blue()
|
||||
|
||||
# Use the unlimited aircraft option to determine if an airfield should
|
||||
# be owned by the player when the campaign is "inverted".
|
||||
cp.captured_invert = airport.unlimited_aircrafts
|
||||
|
||||
return cp
|
||||
|
||||
def country(self, blue: bool) -> Country:
|
||||
country = self.mission.country(
|
||||
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name)
|
||||
# Should be guaranteed because we initialized them.
|
||||
assert country
|
||||
return country
|
||||
|
||||
@property
|
||||
def blue(self) -> Country:
|
||||
return self.country(blue=True)
|
||||
|
||||
@property
|
||||
def red(self) -> Country:
|
||||
return self.country(blue=False)
|
||||
|
||||
def off_map_spawns(self, blue: bool) -> Iterator[FlyingGroup]:
|
||||
for group in self.country(blue).plane_group:
|
||||
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def carriers(self, blue: bool) -> Iterator[ShipGroup]:
|
||||
for group in self.country(blue).ship_group:
|
||||
if group.units[0].type == self.CV_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def lhas(self, blue: bool) -> Iterator[ShipGroup]:
|
||||
for group in self.country(blue).ship_group:
|
||||
if group.units[0].type == self.LHA_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
|
||||
for group in self.country(blue).vehicle_group:
|
||||
if group.units[0].type == self.FOB_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ships(self) -> Iterator[ShipGroup]:
|
||||
for group in self.blue.ship_group:
|
||||
if group.units[0].type == self.SHIP_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ewrs(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.EWR_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.SAM_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def garrisons(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.GARRISON_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def offshore_strike_targets(self) -> Iterator[StaticGroup]:
|
||||
for group in self.blue.static_group:
|
||||
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def missile_sites(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def coastal_defenses(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_long_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.REQUIRED_LONG_RANGE_SAM_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_medium_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@cached_property
|
||||
def control_points(self) -> Dict[int, ControlPoint]:
|
||||
control_points = {}
|
||||
for airport in self.mission.terrain.airport_list():
|
||||
if airport.is_blue() or airport.is_red():
|
||||
control_point = self.control_point_from_airport(airport)
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
for blue in (False, True):
|
||||
for group in self.off_map_spawns(blue):
|
||||
control_point = OffMapSpawn(next(self.control_point_id),
|
||||
str(group.name), group.position)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for group in self.carriers(blue):
|
||||
# TODO: Name the carrier.
|
||||
control_point = Carrier(
|
||||
"carrier", group.position, next(self.control_point_id))
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for group in self.lhas(blue):
|
||||
# TODO: Name the LHA.
|
||||
control_point = Lha(
|
||||
"lha", group.position, next(self.control_point_id))
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for group in self.fobs(blue):
|
||||
control_point = Fob(
|
||||
str(group.name), group.position, next(self.control_point_id)
|
||||
)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
return control_points
|
||||
|
||||
@property
|
||||
def front_line_path_groups(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.country(blue=True).vehicle_group:
|
||||
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@cached_property
|
||||
def front_lines(self) -> Dict[str, ComplexFrontLine]:
|
||||
# Dict of front line ID to a front line.
|
||||
front_lines = {}
|
||||
for group in self.front_line_path_groups:
|
||||
# The unit will have its first waypoint at the source CP and the
|
||||
# final waypoint at the destination CP. Intermediate waypoints
|
||||
# define the curve of the front line.
|
||||
waypoints = [p.position for p in group.points]
|
||||
origin = self.theater.closest_control_point(waypoints[0])
|
||||
if origin is None:
|
||||
raise RuntimeError(
|
||||
f"No control point near the first waypoint of {group.name}")
|
||||
destination = self.theater.closest_control_point(waypoints[-1])
|
||||
if destination is None:
|
||||
raise RuntimeError(
|
||||
f"No control point near the final waypoint of {group.name}")
|
||||
|
||||
# Snap the begin and end points to the control points.
|
||||
waypoints[0] = origin.position
|
||||
waypoints[-1] = destination.position
|
||||
front_line_id = f"{origin.id}|{destination.id}"
|
||||
front_lines[front_line_id] = ComplexFrontLine(origin, waypoints)
|
||||
self.control_points[origin.id].connect(
|
||||
self.control_points[destination.id])
|
||||
self.control_points[destination.id].connect(
|
||||
self.control_points[origin.id])
|
||||
return front_lines
|
||||
|
||||
def objective_info(self, group: Group) -> Tuple[ControlPoint, int]:
|
||||
closest = self.theater.closest_control_point(group.position)
|
||||
distance = closest.position.distance_to_point(group.position)
|
||||
return closest, distance
|
||||
|
||||
def add_preset_locations(self) -> None:
|
||||
for group in self.garrisons:
|
||||
closest, distance = self.objective_info(group)
|
||||
if distance < self.BASE_DEFENSE_RADIUS:
|
||||
closest.preset_locations.base_garrisons.append(group.position)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Found garrison unit too far from base: {group.name}")
|
||||
|
||||
for group in self.sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
if distance < self.BASE_DEFENSE_RADIUS:
|
||||
closest.preset_locations.base_air_defense.append(group.position)
|
||||
else:
|
||||
closest.preset_locations.strike_locations.append(group.position)
|
||||
|
||||
for group in self.ewrs:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.ewrs.append(group.position)
|
||||
|
||||
for group in self.offshore_strike_targets:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.offshore_strike_locations.append(
|
||||
group.position)
|
||||
|
||||
for group in self.ships:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.ships.append(group.position)
|
||||
|
||||
for group in self.missile_sites:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.missile_sites.append(group.position)
|
||||
|
||||
for group in self.coastal_defenses:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.coastal_defenses.append(group.position)
|
||||
|
||||
for group in self.required_long_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_long_range_sams.append(
|
||||
group.position
|
||||
)
|
||||
|
||||
for group in self.required_medium_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_medium_range_sams.append(
|
||||
group.position
|
||||
)
|
||||
|
||||
def populate_theater(self) -> None:
|
||||
for control_point in self.control_points.values():
|
||||
self.theater.add_controlpoint(control_point)
|
||||
self.add_preset_locations()
|
||||
self.theater.set_frontline_data(self.front_lines)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReferencePoint:
|
||||
world_coordinates: Point
|
||||
image_coordinates: Point
|
||||
|
||||
|
||||
class ConflictTheater:
|
||||
terrain: Terrain
|
||||
|
||||
reference_points: Tuple[ReferencePoint, ReferencePoint]
|
||||
overview_image: str
|
||||
landmap: Optional[Landmap]
|
||||
"""
|
||||
land_poly = None # type: Polygon
|
||||
"""
|
||||
daytime_map: Dict[str, Tuple[int, int]]
|
||||
_frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
|
||||
|
||||
def __init__(self):
|
||||
self.controlpoints: List[ControlPoint] = []
|
||||
self._frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
|
||||
"""
|
||||
self.land_poly = geometry.Polygon(self.landmap[0][0])
|
||||
for x in self.landmap[1]:
|
||||
self.land_poly = self.land_poly.difference(geometry.Polygon(x))
|
||||
"""
|
||||
|
||||
@property
|
||||
def frontline_data(self) -> Optional[Dict[str, ComplexFrontLine]]:
|
||||
if self._frontline_data is None:
|
||||
self.load_frontline_data_from_file()
|
||||
return self._frontline_data
|
||||
|
||||
def load_frontline_data_from_file(self) -> None:
|
||||
if self._frontline_data is not None:
|
||||
logging.warning("Replacing existing frontline data from file")
|
||||
self._frontline_data = FrontLine.load_json_frontlines(self)
|
||||
if self._frontline_data is None:
|
||||
self._frontline_data = {}
|
||||
|
||||
def set_frontline_data(self, data: Dict[str, ComplexFrontLine]) -> None:
|
||||
if self._frontline_data is not None:
|
||||
logging.warning("Replacing existing frontline data")
|
||||
self._frontline_data = data
|
||||
|
||||
def add_controlpoint(self, point: ControlPoint,
|
||||
connected_to: Optional[List[ControlPoint]] = None):
|
||||
if connected_to is None:
|
||||
connected_to = []
|
||||
for connected_point in connected_to:
|
||||
point.connect(to=connected_point)
|
||||
|
||||
self.controlpoints.append(point)
|
||||
|
||||
def find_ground_objects_by_obj_name(self, obj_name):
|
||||
found = []
|
||||
for cp in self.controlpoints:
|
||||
for g in cp.ground_objects:
|
||||
if g.obj_name == obj_name:
|
||||
found.append(g)
|
||||
return found
|
||||
|
||||
def is_in_sea(self, point: Point) -> bool:
|
||||
if not self.landmap:
|
||||
return False
|
||||
|
||||
if self.is_on_land(point):
|
||||
return False
|
||||
|
||||
for exclusion_zone in self.landmap[1]:
|
||||
if poly_contains(point.x, point.y, exclusion_zone):
|
||||
return False
|
||||
|
||||
for sea in self.landmap[2]:
|
||||
if poly_contains(point.x, point.y, sea):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_on_land(self, point: Point) -> bool:
|
||||
if not self.landmap:
|
||||
return True
|
||||
|
||||
is_point_included = False
|
||||
for inclusion_zone in self.landmap[0]:
|
||||
if poly_contains(point.x, point.y, inclusion_zone):
|
||||
is_point_included = True
|
||||
|
||||
if not is_point_included:
|
||||
return False
|
||||
|
||||
for exclusion_zone in self.landmap[1]:
|
||||
if poly_contains(point.x, point.y, exclusion_zone):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def nearest_land_pos(self, point: Point, extend_dist: int = 50) -> Point:
|
||||
"""Returns the nearest point inside a land exclusion zone from point
|
||||
`extend_dist` determines how far inside the zone the point should be placed"""
|
||||
if self.is_on_land(point):
|
||||
return point
|
||||
point = geometry.Point(point.x, point.y)
|
||||
nearest_points = []
|
||||
if not self.landmap:
|
||||
raise RuntimeError("Landmap not initialized")
|
||||
for inclusion_zone in self.landmap[0]:
|
||||
nearest_pair = ops.nearest_points(point, inclusion_zone)
|
||||
nearest_points.append(nearest_pair[1])
|
||||
min_distance = None # type: Optional[geometry.Point]
|
||||
nearest_point = None # type: Optional[geometry.Point]
|
||||
for pt in nearest_points:
|
||||
distance = point.distance(pt)
|
||||
if not min_distance or distance < min_distance:
|
||||
min_distance = distance
|
||||
nearest_point = pt
|
||||
assert isinstance(nearest_point, geometry.Point)
|
||||
point = Point(point.x, point.y)
|
||||
nearest_point = Point(nearest_point.x, nearest_point.y)
|
||||
new_point = point.point_from_heading(
|
||||
point.heading_between_point(nearest_point),
|
||||
point.distance_to_point(nearest_point) + extend_dist
|
||||
)
|
||||
return new_point
|
||||
|
||||
def player_points(self) -> List[ControlPoint]:
|
||||
return [point for point in self.controlpoints if point.captured]
|
||||
|
||||
def conflicts(self, from_player=True) -> Iterator[FrontLine]:
|
||||
for cp in [x for x in self.controlpoints if x.captured == from_player]:
|
||||
for connected_point in [x for x in cp.connected_points if x.captured != from_player]:
|
||||
yield FrontLine(cp, connected_point, self)
|
||||
|
||||
def enemy_points(self) -> List[ControlPoint]:
|
||||
return [point for point in self.controlpoints if not point.captured]
|
||||
|
||||
def closest_control_point(self, point: Point) -> ControlPoint:
|
||||
closest = self.controlpoints[0]
|
||||
closest_distance = point.distance_to_point(closest.position)
|
||||
for control_point in self.controlpoints[1:]:
|
||||
distance = point.distance_to_point(control_point.position)
|
||||
if distance < closest_distance:
|
||||
closest = control_point
|
||||
closest_distance = distance
|
||||
return closest
|
||||
|
||||
def closest_opposing_control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
||||
"""
|
||||
Returns a tuple of the two nearest opposing ControlPoints in theater.
|
||||
(player_cp, enemy_cp)
|
||||
"""
|
||||
all_cp_min_distances = {}
|
||||
for idx, control_point in enumerate(self.controlpoints):
|
||||
distances = {}
|
||||
closest_distance = None
|
||||
for i, cp in enumerate(self.controlpoints):
|
||||
if i != idx and cp.captured is not control_point.captured:
|
||||
dist = cp.position.distance_to_point(control_point.position)
|
||||
if not closest_distance:
|
||||
closest_distance = dist
|
||||
distances[cp.id] = dist
|
||||
if dist < closest_distance:
|
||||
distances[cp.id] = dist
|
||||
closest_cp_id = min(distances, key=distances.get) # type: ignore
|
||||
|
||||
all_cp_min_distances[(control_point.id, closest_cp_id)] = distances[closest_cp_id]
|
||||
closest_opposing_cps = [
|
||||
self.find_control_point_by_id(i)
|
||||
for i
|
||||
in min(all_cp_min_distances, key=all_cp_min_distances.get) # type: ignore
|
||||
] # type: List[ControlPoint]
|
||||
assert len(closest_opposing_cps) == 2
|
||||
if closest_opposing_cps[0].captured:
|
||||
return cast(Tuple[ControlPoint, ControlPoint], tuple(closest_opposing_cps))
|
||||
else:
|
||||
return cast(Tuple[ControlPoint, ControlPoint], tuple(reversed(closest_opposing_cps)))
|
||||
|
||||
def find_control_point_by_id(self, id: int) -> ControlPoint:
|
||||
for i in self.controlpoints:
|
||||
if i.id == id:
|
||||
return i
|
||||
raise RuntimeError(f"Cannot find ControlPoint with ID {id}")
|
||||
|
||||
def add_json_cp(self, theater, p: dict) -> ControlPoint:
|
||||
cp: ControlPoint
|
||||
if p["type"] == "airbase":
|
||||
|
||||
airbase = theater.terrain.airports[p["id"]]
|
||||
|
||||
if "size" in p.keys():
|
||||
size = p["size"]
|
||||
else:
|
||||
size = SIZE_REGULAR
|
||||
|
||||
if "importance" in p.keys():
|
||||
importance = p["importance"]
|
||||
else:
|
||||
importance = IMPORTANCE_MEDIUM
|
||||
|
||||
cp = Airfield(airbase, size, importance)
|
||||
elif p["type"] == "carrier":
|
||||
cp = Carrier("carrier", Point(p["x"], p["y"]), p["id"])
|
||||
else:
|
||||
cp = Lha("lha", Point(p["x"], p["y"]), p["id"])
|
||||
|
||||
if "captured_invert" in p.keys():
|
||||
cp.captured_invert = p["captured_invert"]
|
||||
else:
|
||||
cp.captured_invert = False
|
||||
|
||||
return cp
|
||||
|
||||
@staticmethod
|
||||
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
|
||||
theaters = {
|
||||
"Caucasus": CaucasusTheater,
|
||||
"Nevada": NevadaTheater,
|
||||
"Persian Gulf": PersianGulfTheater,
|
||||
"Normandy": NormandyTheater,
|
||||
"The Channel": TheChannelTheater,
|
||||
"Syria": SyriaTheater,
|
||||
}
|
||||
theater = theaters[data["theater"]]
|
||||
t = theater()
|
||||
|
||||
miz = data.get("miz", None)
|
||||
if miz is not None:
|
||||
MizCampaignLoader(directory / miz, t).populate_theater()
|
||||
return t
|
||||
|
||||
cps = {}
|
||||
for p in data["player_points"]:
|
||||
cp = t.add_json_cp(theater, p)
|
||||
cp.captured = True
|
||||
cps[p["id"]] = cp
|
||||
t.add_controlpoint(cp)
|
||||
|
||||
for p in data["enemy_points"]:
|
||||
cp = t.add_json_cp(theater, p)
|
||||
cps[p["id"]] = cp
|
||||
t.add_controlpoint(cp)
|
||||
|
||||
for l in data["links"]:
|
||||
cps[l[0]].connect(cps[l[1]])
|
||||
cps[l[1]].connect(cps[l[0]])
|
||||
|
||||
return t
|
||||
|
||||
|
||||
class CaucasusTheater(ConflictTheater):
|
||||
terrain = caucasus.Caucasus()
|
||||
overview_image = "caumap.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(caucasus.Gelendzhik.position, Point(176, 298)),
|
||||
ReferencePoint(caucasus.Batumi.position, Point(1307, 1205)),
|
||||
)
|
||||
|
||||
landmap = load_landmap("resources\\caulandmap.p")
|
||||
daytime_map = {
|
||||
"dawn": (6, 9),
|
||||
"day": (9, 18),
|
||||
"dusk": (18, 20),
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
class PersianGulfTheater(ConflictTheater):
|
||||
terrain = persiangulf.PersianGulf()
|
||||
overview_image = "persiangulf.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(persiangulf.Jiroft_Airport.position,
|
||||
Point(1692, 1343)),
|
||||
ReferencePoint(persiangulf.Liwa_Airbase.position, Point(358, 3238)),
|
||||
)
|
||||
landmap = load_landmap("resources\\gulflandmap.p")
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (8, 16),
|
||||
"dusk": (16, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
class NevadaTheater(ConflictTheater):
|
||||
terrain = nevada.Nevada()
|
||||
overview_image = "nevada.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(nevada.Mina_Airport_3Q0.position, Point(252, 295)),
|
||||
ReferencePoint(nevada.Laughlin_Airport.position, Point(844, 909)),
|
||||
)
|
||||
landmap = load_landmap("resources\\nevlandmap.p")
|
||||
daytime_map = {
|
||||
"dawn": (4, 6),
|
||||
"day": (6, 17),
|
||||
"dusk": (17, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
class NormandyTheater(ConflictTheater):
|
||||
terrain = normandy.Normandy()
|
||||
overview_image = "normandy.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(normandy.Needs_Oar_Point.position, Point(515, 329)),
|
||||
ReferencePoint(normandy.Evreux.position, Point(2029, 1709)),
|
||||
)
|
||||
landmap = load_landmap("resources\\normandylandmap.p")
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (10, 17),
|
||||
"dusk": (17, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
class TheChannelTheater(ConflictTheater):
|
||||
terrain = thechannel.TheChannel()
|
||||
overview_image = "thechannel.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(thechannel.Abbeville_Drucat.position, Point(2005, 2390)),
|
||||
ReferencePoint(thechannel.Detling.position, Point(706, 382))
|
||||
)
|
||||
landmap = load_landmap("resources\\channellandmap.p")
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (10, 17),
|
||||
"dusk": (17, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
class SyriaTheater(ConflictTheater):
|
||||
terrain = syria.Syria()
|
||||
overview_image = "syria.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(syria.Eyn_Shemer.position, Point(564, 1289)),
|
||||
ReferencePoint(syria.Tabqa.position, Point(1329, 491)),
|
||||
)
|
||||
landmap = load_landmap("resources\\syrialandmap.p")
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (8, 16),
|
||||
"dusk": (16, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComplexFrontLine:
|
||||
"""
|
||||
Stores data necessary for building a multi-segment frontline.
|
||||
"points" should be ordered from closest to farthest distance originating from start_cp.position
|
||||
"""
|
||||
|
||||
start_cp: ControlPoint
|
||||
points: List[Point]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FrontLineSegment:
|
||||
"""
|
||||
Describes a line segment of a FrontLine
|
||||
"""
|
||||
|
||||
point_a: Point
|
||||
point_b: Point
|
||||
|
||||
@property
|
||||
def attack_heading(self) -> Numeric:
|
||||
"""The heading of the frontline segment from player to enemy control point"""
|
||||
return self.point_a.heading_between_point(self.point_b)
|
||||
|
||||
@property
|
||||
def attack_distance(self) -> Numeric:
|
||||
"""Length of the segment"""
|
||||
return self.point_a.distance_to_point(self.point_b)
|
||||
|
||||
|
||||
class FrontLine(MissionTarget):
|
||||
"""Defines a front line location between two control points.
|
||||
Front lines are the area where ground combat happens.
|
||||
Overwrites the entirety of MissionTarget __init__ method to allow for
|
||||
dynamic position calculation.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
control_point_a: ControlPoint,
|
||||
control_point_b: ControlPoint,
|
||||
theater: ConflictTheater
|
||||
) -> None:
|
||||
self.control_point_a = control_point_a
|
||||
self.control_point_b = control_point_b
|
||||
self.segments: List[FrontLineSegment] = []
|
||||
self.theater = theater
|
||||
self._build_segments()
|
||||
self.name = f"Front line {control_point_a}/{control_point_b}"
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
"""Returns True if the objective is in friendly territory."""
|
||||
return False
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
yield from [
|
||||
FlightType.CAS,
|
||||
# TODO: FlightType.TROOP_TRANSPORT
|
||||
# TODO: FlightType.EVAC
|
||||
]
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
"""
|
||||
The position where the conflict should occur
|
||||
according to the current strength of each control point.
|
||||
"""
|
||||
return self.point_from_a(self._position_distance)
|
||||
|
||||
@property
|
||||
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
||||
"""Returns a tuple of the two control points."""
|
||||
return self.control_point_a, self.control_point_b
|
||||
|
||||
@property
|
||||
def attack_distance(self):
|
||||
"""The total distance of all segments"""
|
||||
return sum(i.attack_distance for i in self.segments)
|
||||
|
||||
@property
|
||||
def attack_heading(self):
|
||||
"""The heading of the active attack segment from player to enemy control point"""
|
||||
return self.active_segment.attack_heading
|
||||
|
||||
@property
|
||||
def active_segment(self) -> FrontLineSegment:
|
||||
"""The FrontLine segment where there can be an active conflict"""
|
||||
if self._position_distance <= self.segments[0].attack_distance:
|
||||
return self.segments[0]
|
||||
|
||||
remaining_dist = self._position_distance
|
||||
for segment in self.segments:
|
||||
if remaining_dist <= segment.attack_distance:
|
||||
return segment
|
||||
else:
|
||||
remaining_dist -= segment.attack_distance
|
||||
logging.error(
|
||||
"Frontline attack distance is greater than the sum of its segments"
|
||||
)
|
||||
return self.segments[0]
|
||||
|
||||
def point_from_a(self, distance: Numeric) -> Point:
|
||||
"""
|
||||
Returns a point {distance} away from control_point_a along the frontline segments.
|
||||
"""
|
||||
if distance < self.segments[0].attack_distance:
|
||||
return self.control_point_a.position.point_from_heading(
|
||||
self.segments[0].attack_heading, distance
|
||||
)
|
||||
remaining_dist = distance
|
||||
for segment in self.segments:
|
||||
if remaining_dist < segment.attack_distance:
|
||||
return segment.point_a.point_from_heading(
|
||||
segment.attack_heading, remaining_dist
|
||||
)
|
||||
else:
|
||||
remaining_dist -= segment.attack_distance
|
||||
|
||||
@property
|
||||
def _position_distance(self) -> float:
|
||||
"""
|
||||
The distance from point "a" where the conflict should occur
|
||||
according to the current strength of each control point
|
||||
"""
|
||||
total_strength = (
|
||||
self.control_point_a.base.strength + self.control_point_b.base.strength
|
||||
)
|
||||
if self.control_point_a.base.strength == 0:
|
||||
return self._adjust_for_min_dist(0)
|
||||
if self.control_point_b.base.strength == 0:
|
||||
return self._adjust_for_min_dist(self.attack_distance)
|
||||
strength_pct = self.control_point_a.base.strength / total_strength
|
||||
return self._adjust_for_min_dist(strength_pct * self.attack_distance)
|
||||
|
||||
def _adjust_for_min_dist(self, distance: Numeric) -> Numeric:
|
||||
"""
|
||||
Ensures the frontline conflict is never located within the minimum distance
|
||||
constant of either end control point.
|
||||
"""
|
||||
if (distance > self.attack_distance / 2) and (
|
||||
distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance
|
||||
):
|
||||
distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE
|
||||
elif (distance < self.attack_distance / 2) and (
|
||||
distance < FRONTLINE_MIN_CP_DISTANCE
|
||||
):
|
||||
distance = FRONTLINE_MIN_CP_DISTANCE
|
||||
return distance
|
||||
|
||||
def _build_segments(self) -> None:
|
||||
"""Create line segments for the frontline"""
|
||||
control_point_ids = "|".join(
|
||||
[str(self.control_point_a.id), str(self.control_point_b.id)]
|
||||
) # from_cp.id|to_cp.id
|
||||
reversed_cp_ids = "|".join(
|
||||
[str(self.control_point_b.id), str(self.control_point_a.id)]
|
||||
)
|
||||
complex_frontlines = self.theater.frontline_data
|
||||
if (complex_frontlines) and (
|
||||
(control_point_ids in complex_frontlines)
|
||||
or (reversed_cp_ids in complex_frontlines)
|
||||
):
|
||||
# The frontline segments must be stored in the correct order for the distance algorithms to work.
|
||||
# The points in the frontline are ordered from the id before the | to the id after.
|
||||
# First, check if control point id pair matches in order, and create segments if a match is found.
|
||||
if control_point_ids in complex_frontlines:
|
||||
point_pairs = pairwise(complex_frontlines[control_point_ids].points)
|
||||
for i in point_pairs:
|
||||
self.segments.append(FrontLineSegment(i[0], i[1]))
|
||||
# Check the reverse order and build in reverse if found.
|
||||
elif reversed_cp_ids in complex_frontlines:
|
||||
point_pairs = pairwise(
|
||||
reversed(complex_frontlines[reversed_cp_ids].points)
|
||||
)
|
||||
for i in point_pairs:
|
||||
self.segments.append(FrontLineSegment(i[0], i[1]))
|
||||
# If no complex frontline has been configured, fall back to the old straight line method.
|
||||
else:
|
||||
self.segments.append(
|
||||
FrontLineSegment(
|
||||
self.control_point_a.position, self.control_point_b.position
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def load_json_frontlines(
|
||||
theater: ConflictTheater
|
||||
) -> Optional[Dict[str, ComplexFrontLine]]:
|
||||
"""Load complex frontlines from json"""
|
||||
try:
|
||||
path = Path(f"resources/frontlines/{theater.terrain.name.lower()}.json")
|
||||
with open(path, "r") as file:
|
||||
logging.debug(f"Loading frontline from {path}...")
|
||||
data = json.load(file)
|
||||
return {
|
||||
frontline: ComplexFrontLine(
|
||||
data[frontline]["start_cp"],
|
||||
[Point(i[0], i[1]) for i in data[frontline]["points"]],
|
||||
)
|
||||
for frontline in data
|
||||
}
|
||||
except OSError:
|
||||
logging.warning(
|
||||
f"Unable to load preset frontlines for {theater.terrain.name}"
|
||||
)
|
||||
return None
|
||||
722
game/theater/controlpoint.py
Normal file
722
game/theater/controlpoint.py
Normal file
@@ -0,0 +1,722 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.ships import (
|
||||
CVN_74_John_C__Stennis,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
LHA_1_Tarawa,
|
||||
Type_071_Amphibious_Transport_Dock,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, ParkingSlot
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from gen.runways import RunwayAssigner, RunwayData
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from .base import Base
|
||||
from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import (
|
||||
BaseDefenseGroundObject,
|
||||
EwrGroundObject,
|
||||
SamGroundObject,
|
||||
TheaterGroundObject,
|
||||
VehicleGroupGroundObject, GenericCarrierGroundObject,
|
||||
)
|
||||
from ..weather import Conditions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from gen.flights.flight import FlightType
|
||||
from ..event import UnitsDeliveryEvent
|
||||
|
||||
|
||||
class ControlPointType(Enum):
|
||||
#: An airbase with slots for everything.
|
||||
AIRBASE = 0
|
||||
#: A group with a Stennis type carrier (F/A-18, F-14 compatible).
|
||||
AIRCRAFT_CARRIER_GROUP = 1
|
||||
#: A group with a Tarawa carrier (Helicopters & Harrier).
|
||||
LHA_GROUP = 2
|
||||
#: A FARP, with slots for helicopters
|
||||
FARP = 4
|
||||
#: A FOB (ground units only)
|
||||
FOB = 5
|
||||
OFF_MAP = 6
|
||||
|
||||
|
||||
class LocationType(Enum):
|
||||
BaseAirDefense = "base air defense"
|
||||
Coastal = "coastal defense"
|
||||
Ewr = "EWR"
|
||||
Garrison = "garrison"
|
||||
MissileSite = "missile site"
|
||||
OffshoreStrikeTarget = "offshore strike target"
|
||||
Sam = "SAM"
|
||||
Ship = "ship"
|
||||
Shorad = "SHORAD"
|
||||
StrikeTarget = "strike target"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PresetLocations:
|
||||
"""Defines the preset locations loaded from the campaign mission file."""
|
||||
|
||||
#: Locations used for spawning ground defenses for bases.
|
||||
base_garrisons: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations used for spawning air defenses for bases. Used by SAMs, AAA,
|
||||
#: and SHORADs.
|
||||
base_air_defense: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations used by EWRs.
|
||||
ewrs: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations used by non-carrier ships. Carriers and LHAs are not random.
|
||||
ships: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations used by coastal defenses.
|
||||
coastal_defenses: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations used by ground based strike objectives.
|
||||
strike_locations: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations used by offshore strike objectives.
|
||||
offshore_strike_locations: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations used by missile sites like scuds and V-2s.
|
||||
missile_sites: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations of long range SAMs which should always be spawned.
|
||||
required_long_range_sams: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations of medium range SAMs which should always be spawned.
|
||||
required_medium_range_sams: List[Point] = field(default_factory=list)
|
||||
|
||||
@staticmethod
|
||||
def _random_from(points: List[Point]) -> Optional[Point]:
|
||||
"""Finds, removes, and returns a random position from the given list."""
|
||||
if not points:
|
||||
return None
|
||||
point = random.choice(points)
|
||||
points.remove(point)
|
||||
return point
|
||||
|
||||
def random_for(self, location_type: LocationType) -> Optional[Point]:
|
||||
"""Returns a position suitable for the given location type.
|
||||
|
||||
The location, if found, will be claimed by the caller and not available
|
||||
to subsequent calls.
|
||||
"""
|
||||
if location_type == LocationType.BaseAirDefense:
|
||||
return self._random_from(self.base_air_defense)
|
||||
if location_type == LocationType.Coastal:
|
||||
return self._random_from(self.coastal_defenses)
|
||||
if location_type == LocationType.Ewr:
|
||||
return self._random_from(self.ewrs)
|
||||
if location_type == LocationType.Garrison:
|
||||
return self._random_from(self.base_garrisons)
|
||||
if location_type == LocationType.MissileSite:
|
||||
return self._random_from(self.missile_sites)
|
||||
if location_type == LocationType.OffshoreStrikeTarget:
|
||||
return self._random_from(self.offshore_strike_locations)
|
||||
if location_type == LocationType.Sam:
|
||||
return self._random_from(self.strike_locations)
|
||||
if location_type == LocationType.Ship:
|
||||
return self._random_from(self.ships)
|
||||
if location_type == LocationType.Shorad:
|
||||
return self._random_from(self.base_garrisons)
|
||||
if location_type == LocationType.StrikeTarget:
|
||||
return self._random_from(self.strike_locations)
|
||||
logging.error(f"Unknown location type: {location_type}")
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PendingOccupancy:
|
||||
present: int
|
||||
ordered: int
|
||||
transferring: int
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
return self.present + self.ordered + self.transferring
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunwayStatus:
|
||||
damaged: bool = False
|
||||
repair_turns_remaining: Optional[int] = None
|
||||
|
||||
def damage(self) -> None:
|
||||
self.damaged = True
|
||||
# If the runway is already under repair and is damaged again, progress
|
||||
# is reset.
|
||||
self.repair_turns_remaining = None
|
||||
|
||||
def begin_repair(self) -> None:
|
||||
if self.repair_turns_remaining is not None:
|
||||
logging.error("Runway already under repair. Restarting.")
|
||||
self.repair_turns_remaining = 4
|
||||
|
||||
def process_turn(self) -> None:
|
||||
if self.repair_turns_remaining is not None:
|
||||
if self.repair_turns_remaining == 1:
|
||||
self.repair_turns_remaining = None
|
||||
self.damaged = False
|
||||
else:
|
||||
self.repair_turns_remaining -= 1
|
||||
|
||||
@property
|
||||
def needs_repair(self) -> bool:
|
||||
return self.damaged and self.repair_turns_remaining is None
|
||||
|
||||
def __str__(self) -> str:
|
||||
if not self.damaged:
|
||||
return "Runway operational"
|
||||
|
||||
turns_remaining = self.repair_turns_remaining
|
||||
if turns_remaining is None:
|
||||
return "Runway damaged"
|
||||
|
||||
return f"Runway repairing, {turns_remaining} turns remaining"
|
||||
|
||||
|
||||
class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
position = None # type: Point
|
||||
name = None # type: str
|
||||
|
||||
captured = False
|
||||
has_frontline = True
|
||||
|
||||
alt = 0
|
||||
|
||||
# TODO: Only airbases have IDs.
|
||||
# TODO: has_frontline is only reasonable for airbases.
|
||||
# TODO: cptype is obsolete.
|
||||
def __init__(self, cp_id: int, name: str, position: Point,
|
||||
at: db.StartingPosition, size: int,
|
||||
importance: float, has_frontline=True,
|
||||
cptype=ControlPointType.AIRBASE):
|
||||
super().__init__(" ".join(re.split(r"[ \-]", name)[:2]), position)
|
||||
# TODO: Should be Airbase specific.
|
||||
self.id = cp_id
|
||||
self.full_name = name
|
||||
self.at = at
|
||||
self.connected_objectives: List[TheaterGroundObject] = []
|
||||
self.base_defenses: List[BaseDefenseGroundObject] = []
|
||||
self.preset_locations = PresetLocations()
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
self.size = size
|
||||
self.importance = importance
|
||||
self.captured = False
|
||||
self.captured_invert = False
|
||||
# TODO: Should be Airbase specific.
|
||||
self.has_frontline = has_frontline
|
||||
self.connected_points: List[ControlPoint] = []
|
||||
self.base: Base = Base()
|
||||
self.cptype = cptype
|
||||
# TODO: Should be Airbase specific.
|
||||
self.stances: Dict[int, CombatStance] = {}
|
||||
self.pending_unit_deliveries: Optional[UnitsDeliveryEvent] = None
|
||||
|
||||
self.target_position: Optional[Point] = None
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{__class__}: {self.name}>"
|
||||
|
||||
@property
|
||||
def ground_objects(self) -> List[TheaterGroundObject]:
|
||||
return list(
|
||||
itertools.chain(self.connected_objectives, self.base_defenses))
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def heading(self) -> int:
|
||||
...
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def is_global(self):
|
||||
return not self.connected_points
|
||||
|
||||
@property
|
||||
def is_carrier(self):
|
||||
"""
|
||||
:return: Whether this control point is an aircraft carrier
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_fleet(self):
|
||||
"""
|
||||
:return: Whether this control point is a boat (mobile)
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_lha(self):
|
||||
"""
|
||||
:return: Whether this control point is an LHA
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def moveable(self) -> bool:
|
||||
"""
|
||||
:return: Whether this control point can be moved around
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def total_aircraft_parking(self):
|
||||
"""
|
||||
:return: The maximum number of aircraft that can be stored in this
|
||||
control point
|
||||
"""
|
||||
...
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def connect(self, to: ControlPoint) -> None:
|
||||
self.connected_points.append(to)
|
||||
self.stances[to.id] = CombatStance.DEFENSIVE
|
||||
|
||||
@abstractmethod
|
||||
def runway_is_operational(self) -> bool:
|
||||
"""
|
||||
Check whether this control point supports taking offs and landings.
|
||||
:return:
|
||||
"""
|
||||
...
|
||||
|
||||
# TODO: Should be naval specific.
|
||||
def get_carrier_group_name(self):
|
||||
"""
|
||||
Get the carrier group name if the airbase is a carrier
|
||||
:return: Carrier group name
|
||||
"""
|
||||
if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||
ControlPointType.LHA_GROUP]:
|
||||
for g in self.ground_objects:
|
||||
if g.dcs_identifier == "CARRIER":
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
if db.unit_type_from_name(u.type) in [
|
||||
CVN_74_John_C__Stennis,
|
||||
CV_1143_5_Admiral_Kuznetsov]:
|
||||
return group.name
|
||||
elif g.dcs_identifier == "LHA":
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
if db.unit_type_from_name(u.type) in [LHA_1_Tarawa]:
|
||||
return group.name
|
||||
return None
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def is_connected(self, to) -> bool:
|
||||
return to in self.connected_points
|
||||
|
||||
def find_ground_objects_by_obj_name(self, obj_name):
|
||||
found = []
|
||||
for g in self.ground_objects:
|
||||
if g.obj_name == obj_name:
|
||||
found.append(g)
|
||||
return found
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
return self.captured == to_player
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def clear_base_defenses(self) -> None:
|
||||
for base_defense in self.base_defenses:
|
||||
if isinstance(base_defense, EwrGroundObject):
|
||||
self.preset_locations.ewrs.append(base_defense.position)
|
||||
elif isinstance(base_defense, SamGroundObject):
|
||||
self.preset_locations.base_air_defense.append(
|
||||
base_defense.position)
|
||||
elif isinstance(base_defense, VehicleGroupGroundObject):
|
||||
self.preset_locations.base_garrisons.append(
|
||||
base_defense.position)
|
||||
else:
|
||||
logging.error(
|
||||
"Could not determine preset location type for "
|
||||
f"{base_defense}. Assuming garrison type.")
|
||||
self.preset_locations.base_garrisons.append(
|
||||
base_defense.position)
|
||||
self.base_defenses = []
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
if for_player:
|
||||
self.captured = True
|
||||
else:
|
||||
self.captured = False
|
||||
|
||||
self.base.set_strength_to_minimum()
|
||||
|
||||
self.base.aircraft = {}
|
||||
self.base.armor = {}
|
||||
|
||||
self.clear_base_defenses()
|
||||
from .start_generator import BaseDefenseGenerator
|
||||
BaseDefenseGenerator(game, self).generate()
|
||||
|
||||
@abstractmethod
|
||||
def can_operate(self, aircraft: Type[FlyingType]) -> bool:
|
||||
...
|
||||
|
||||
def aircraft_transferring(self, game: Game) -> int:
|
||||
if self.captured:
|
||||
ato = game.blue_ato
|
||||
else:
|
||||
ato = game.red_ato
|
||||
|
||||
total = 0
|
||||
for package in ato.packages:
|
||||
for flight in package.flights:
|
||||
if flight.departure == flight.arrival:
|
||||
continue
|
||||
if flight.departure == self:
|
||||
total -= flight.count
|
||||
elif flight.arrival == self:
|
||||
total += flight.count
|
||||
return total
|
||||
|
||||
def expected_aircraft_next_turn(self, game: Game) -> PendingOccupancy:
|
||||
assert self.pending_unit_deliveries
|
||||
on_order = 0
|
||||
for unit_bought in self.pending_unit_deliveries.units:
|
||||
if issubclass(unit_bought, FlyingType):
|
||||
on_order += self.pending_unit_deliveries.units[unit_bought]
|
||||
|
||||
return PendingOccupancy(self.base.total_aircraft, on_order,
|
||||
self.aircraft_transferring(game))
|
||||
|
||||
def unclaimed_parking(self, game: Game) -> int:
|
||||
return (self.total_aircraft_parking -
|
||||
self.expected_aircraft_next_turn(game).total)
|
||||
|
||||
@abstractmethod
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
...
|
||||
|
||||
@property
|
||||
def parking_slots(self) -> Iterator[ParkingSlot]:
|
||||
yield from []
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
...
|
||||
|
||||
@property
|
||||
def runway_can_be_repaired(self) -> bool:
|
||||
return self.runway_status.needs_repair
|
||||
|
||||
def begin_runway_repair(self) -> None:
|
||||
if not self.runway_can_be_repaired:
|
||||
logging.error(f"Cannot repair runway at {self}")
|
||||
return
|
||||
self.runway_status.begin_repair()
|
||||
|
||||
def process_turn(self) -> None:
|
||||
runway_status = self.runway_status
|
||||
if runway_status is not None:
|
||||
runway_status.process_turn()
|
||||
|
||||
# Process movements for ships control points group
|
||||
if self.target_position is not None:
|
||||
delta = self.target_position - self.position
|
||||
self.position = self.target_position
|
||||
self.target_position = None
|
||||
|
||||
# Move the linked unit groups
|
||||
for ground_object in self.ground_objects:
|
||||
if isinstance(ground_object, GenericCarrierGroundObject):
|
||||
for group in ground_object.groups:
|
||||
for u in group.units:
|
||||
u.position.x = u.position.x + delta.x
|
||||
u.position.y = u.position.y + delta.y
|
||||
|
||||
|
||||
class Airfield(ControlPoint):
|
||||
|
||||
def __init__(self, airport: Airport, size: int,
|
||||
importance: float, has_frontline=True):
|
||||
super().__init__(airport.id, airport.name, airport.position, airport,
|
||||
size, importance, has_frontline,
|
||||
cptype=ControlPointType.AIRBASE)
|
||||
self.airport = airport
|
||||
self._runway_status = RunwayStatus()
|
||||
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
# TODO: Allow helicopters.
|
||||
# Need to implement ground spawns so the helos don't use the runway.
|
||||
# TODO: Allow harrier.
|
||||
# Needs ground spawns just like helos do, but also need to be able to
|
||||
# limit takeoff weight to ~20500 lbs or it won't be able to take off.
|
||||
return self.runway_is_operational()
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: FlightType.LOGISTICS
|
||||
]
|
||||
else:
|
||||
yield from [
|
||||
FlightType.OCA_AIRCRAFT,
|
||||
FlightType.OCA_RUNWAY,
|
||||
]
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return len(self.airport.parking_slots)
|
||||
|
||||
@property
|
||||
def heading(self) -> int:
|
||||
return self.airport.runways[0].heading
|
||||
|
||||
def runway_is_operational(self) -> bool:
|
||||
return not self.runway_status.damaged
|
||||
|
||||
@property
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
return self._runway_status
|
||||
|
||||
def damage_runway(self) -> None:
|
||||
self.runway_status.damage()
|
||||
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
assigner = RunwayAssigner(conditions)
|
||||
return assigner.get_preferred_runway(self.airport)
|
||||
|
||||
@property
|
||||
def parking_slots(self) -> Iterator[ParkingSlot]:
|
||||
yield from self.airport.parking_slots
|
||||
|
||||
@property
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class NavalControlPoint(ControlPoint, ABC):
|
||||
|
||||
@property
|
||||
def is_fleet(self) -> bool:
|
||||
return True
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
yield from super().mission_types(for_player)
|
||||
from gen.flights.flight import FlightType
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: Buddy tanking for the A-4?
|
||||
# TODO: Rescue chopper?
|
||||
# TODO: Inter-ship logistics?
|
||||
]
|
||||
else:
|
||||
yield FlightType.ANTISHIP
|
||||
|
||||
@property
|
||||
def heading(self) -> int:
|
||||
return 0 # TODO compute heading
|
||||
|
||||
def runway_is_operational(self) -> bool:
|
||||
# Necessary because it's possible for the carrier itself to have sunk
|
||||
# while its escorts are still alive.
|
||||
for g in self.ground_objects:
|
||||
if g.dcs_identifier in ["CARRIER", "LHA"]:
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
if db.unit_type_from_name(u.type) in [
|
||||
CVN_74_John_C__Stennis, LHA_1_Tarawa,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Type_071_Amphibious_Transport_Dock]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
# TODO: Assign TACAN and ICLS earlier so we don't need this.
|
||||
fallback = RunwayData(self.full_name, runway_heading=0, runway_name="")
|
||||
return dynamic_runways.get(self.name, fallback)
|
||||
|
||||
@property
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
return RunwayStatus(damaged=not self.runway_is_operational())
|
||||
|
||||
@property
|
||||
def runway_can_be_repaired(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def moveable(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class Carrier(NavalControlPoint):
|
||||
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
import game.theater.conflicttheater
|
||||
super().__init__(cp_id, name, at, at,
|
||||
game.theater.conflicttheater.SIZE_SMALL, 1,
|
||||
has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP)
|
||||
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
raise RuntimeError("Carriers cannot be captured")
|
||||
|
||||
@property
|
||||
def is_carrier(self):
|
||||
return True
|
||||
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
return aircraft in db.CARRIER_CAPABLE
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return 90
|
||||
|
||||
|
||||
class Lha(NavalControlPoint):
|
||||
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
import game.theater.conflicttheater
|
||||
super().__init__(cp_id, name, at, at,
|
||||
game.theater.conflicttheater.SIZE_SMALL, 1,
|
||||
has_frontline=False, cptype=ControlPointType.LHA_GROUP)
|
||||
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
raise RuntimeError("LHAs cannot be captured")
|
||||
|
||||
@property
|
||||
def is_lha(self) -> bool:
|
||||
return True
|
||||
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
return aircraft in db.LHA_CAPABLE
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return 20
|
||||
|
||||
|
||||
class OffMapSpawn(ControlPoint):
|
||||
|
||||
def runway_is_operational(self) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(self, cp_id: int, name: str, position: Point):
|
||||
from . import IMPORTANCE_MEDIUM, SIZE_REGULAR
|
||||
super().__init__(cp_id, name, position, at=position,
|
||||
size=SIZE_REGULAR, importance=IMPORTANCE_MEDIUM,
|
||||
has_frontline=False, cptype=ControlPointType.OFF_MAP)
|
||||
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
raise RuntimeError("Off map control points cannot be captured")
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
yield from []
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return 1000
|
||||
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def heading(self) -> int:
|
||||
return 0
|
||||
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
logging.warning("TODO: Off map spawns have no runways.")
|
||||
return RunwayData(self.full_name, runway_heading=0, runway_name="")
|
||||
|
||||
@property
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
return RunwayStatus()
|
||||
|
||||
@property
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class Fob(ControlPoint):
|
||||
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
import game.theater.conflicttheater
|
||||
super().__init__(cp_id, name, at, at,
|
||||
game.theater.conflicttheater.SIZE_SMALL, 1,
|
||||
has_frontline=True, cptype=ControlPointType.FOB)
|
||||
self.name = name
|
||||
|
||||
def runway_is_operational(self) -> bool:
|
||||
return False
|
||||
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
logging.warning("TODO: FOBs have no runways.")
|
||||
return RunwayData(self.full_name, runway_heading=0, runway_name="")
|
||||
|
||||
@property
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
return RunwayStatus()
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.BARCAP,
|
||||
# TODO: FlightType.LOGISTICS
|
||||
]
|
||||
else:
|
||||
yield from [
|
||||
FlightType.STRIKE,
|
||||
FlightType.SWEEP,
|
||||
FlightType.ESCORT,
|
||||
FlightType.SEAD,
|
||||
]
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return 0
|
||||
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def heading(self) -> int:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return True
|
||||
30
game/theater/landmap.py
Normal file
30
game/theater/landmap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import pickle
|
||||
from typing import Collection, Optional, Tuple
|
||||
import logging
|
||||
|
||||
from shapely import geometry
|
||||
|
||||
Zone = Collection[Tuple[float, float]]
|
||||
Landmap = Tuple[Collection[geometry.Polygon], Collection[geometry.Polygon], Collection[geometry.Polygon]]
|
||||
|
||||
|
||||
def load_landmap(filename: str) -> Optional[Landmap]:
|
||||
try:
|
||||
with open(filename, "rb") as f:
|
||||
return pickle.load(f)
|
||||
except:
|
||||
logging.exception(f"Failed to load landmap {filename}")
|
||||
return None
|
||||
|
||||
|
||||
def poly_contains(x, y, poly:geometry.Polygon):
|
||||
return poly.contains(geometry.Point(x, y))
|
||||
|
||||
|
||||
def poly_centroid(poly) -> Tuple[float, float]:
|
||||
x_list = [vertex[0] for vertex in poly]
|
||||
y_list = [vertex[1] for vertex in poly]
|
||||
x = sum(x_list) / len(poly)
|
||||
y = sum(y_list) / len(poly)
|
||||
return (x, y)
|
||||
|
||||
43
game/theater/missiontarget.py
Normal file
43
game/theater/missiontarget.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterator, TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
class MissionTarget:
|
||||
def __init__(self, name: str, position: Point) -> None:
|
||||
"""Initializes a mission target.
|
||||
|
||||
Args:
|
||||
name: The name of the mission target.
|
||||
position: The location of the mission target.
|
||||
"""
|
||||
self.name = name
|
||||
self.position = position
|
||||
|
||||
def distance_to(self, other: MissionTarget) -> int:
|
||||
"""Computes the distance to the given mission target."""
|
||||
return self.position.distance_to_point(other.position)
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
"""Returns True if the objective is in friendly territory."""
|
||||
raise NotImplementedError
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if self.is_friendly(for_player):
|
||||
yield FlightType.BARCAP
|
||||
else:
|
||||
yield from [
|
||||
FlightType.ESCORT,
|
||||
FlightType.TARCAP,
|
||||
FlightType.SEAD,
|
||||
FlightType.SWEEP,
|
||||
# TODO: FlightType.ELINT,
|
||||
# TODO: FlightType.EWAR,
|
||||
# TODO: FlightType.RECON,
|
||||
]
|
||||
741
game/theater/start_generator.py
Normal file
741
game/theater/start_generator.py
Normal file
@@ -0,0 +1,741 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import pickle
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from game import Game, db
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import Carrier, Lha, LocationType
|
||||
from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
EwrGroundObject,
|
||||
LhaGroundObject,
|
||||
MissileSiteGroundObject,
|
||||
SamGroundObject,
|
||||
ShipGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
)
|
||||
from game.version import VERSION
|
||||
from gen import namegen
|
||||
from gen.defenses.armor_group_generator import generate_armor_group
|
||||
from gen.fleet.ship_group_generator import (
|
||||
generate_carrier_group,
|
||||
generate_lha_group,
|
||||
generate_ship_group,
|
||||
)
|
||||
from gen.locations.preset_location_finder import MizDataLocationFinder
|
||||
from gen.missiles.missiles_group_generator import generate_missile_group
|
||||
from gen.sam.airdefensegroupgenerator import AirDefenseRange
|
||||
from gen.sam.sam_group_generator import (
|
||||
generate_anti_air_group,
|
||||
generate_ewr_group,
|
||||
)
|
||||
from . import (
|
||||
ConflictTheater,
|
||||
ControlPoint,
|
||||
ControlPointType,
|
||||
Fob,
|
||||
OffMapSpawn,
|
||||
)
|
||||
from ..settings import Settings
|
||||
|
||||
GroundObjectTemplates = Dict[str, Dict[str, Any]]
|
||||
|
||||
UNIT_VARIETY = 6
|
||||
UNIT_AMOUNT_FACTOR = 16
|
||||
UNIT_COUNT_IMPORTANCE_LOG = 1.3
|
||||
|
||||
COUNT_BY_TASK = {
|
||||
PinpointStrike: 12,
|
||||
CAP: 8,
|
||||
CAS: 4,
|
||||
AirDefence: 1,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GeneratorSettings:
|
||||
start_date: datetime
|
||||
player_budget: int
|
||||
enemy_budget: int
|
||||
midgame: bool
|
||||
inverted: bool
|
||||
no_carrier: bool
|
||||
no_lha: bool
|
||||
no_player_navy: bool
|
||||
no_enemy_navy: bool
|
||||
|
||||
|
||||
class GameGenerator:
|
||||
def __init__(self, player: str, enemy: str, theater: ConflictTheater,
|
||||
settings: Settings,
|
||||
generator_settings: GeneratorSettings) -> None:
|
||||
self.player = player
|
||||
self.enemy = enemy
|
||||
self.theater = theater
|
||||
self.settings = settings
|
||||
self.generator_settings = generator_settings
|
||||
|
||||
def generate(self) -> Game:
|
||||
# Reset name generator
|
||||
namegen.reset()
|
||||
self.prepare_theater()
|
||||
game = Game(
|
||||
player_name=self.player,
|
||||
enemy_name=self.enemy,
|
||||
theater=self.theater,
|
||||
start_date=self.generator_settings.start_date,
|
||||
settings=self.settings,
|
||||
player_budget=self.generator_settings.player_budget,
|
||||
enemy_budget=self.generator_settings.enemy_budget
|
||||
)
|
||||
|
||||
GroundObjectGenerator(game, self.generator_settings).generate()
|
||||
game.settings.version = VERSION
|
||||
return game
|
||||
|
||||
def prepare_theater(self) -> None:
|
||||
to_remove: List[ControlPoint] = []
|
||||
# Auto-capture half the bases if midgame.
|
||||
if self.generator_settings.midgame:
|
||||
control_points = self.theater.controlpoints
|
||||
for control_point in control_points[:len(control_points) // 2]:
|
||||
control_point.captured = True
|
||||
|
||||
# Remove carrier and lha, invert situation if needed
|
||||
for cp in self.theater.controlpoints:
|
||||
if isinstance(cp, Carrier) and self.generator_settings.no_carrier:
|
||||
to_remove.append(cp)
|
||||
elif isinstance(cp, Lha) and self.generator_settings.no_lha:
|
||||
to_remove.append(cp)
|
||||
|
||||
if self.generator_settings.inverted:
|
||||
cp.captured = cp.captured_invert
|
||||
|
||||
# do remove
|
||||
for cp in to_remove:
|
||||
self.theater.controlpoints.remove(cp)
|
||||
|
||||
# TODO: Fix this. This captures all bases for blue.
|
||||
# reapply midgame inverted if needed
|
||||
if self.generator_settings.midgame and self.generator_settings.inverted:
|
||||
for i, cp in enumerate(reversed(self.theater.controlpoints)):
|
||||
if i > len(self.theater.controlpoints):
|
||||
break
|
||||
else:
|
||||
cp.captured = True
|
||||
|
||||
|
||||
class LocationFinder:
|
||||
def __init__(self, game: Game, control_point: ControlPoint) -> None:
|
||||
self.game = game
|
||||
self.control_point = control_point
|
||||
self.miz_data = MizDataLocationFinder.compute_possible_locations(
|
||||
game.theater.terrain.name, control_point.full_name)
|
||||
|
||||
def location_for(self, location_type: LocationType) -> Optional[Point]:
|
||||
position = self.control_point.preset_locations.random_for(location_type)
|
||||
if position is not None:
|
||||
return position
|
||||
|
||||
logging.warning(f"No campaign location for %s at %s",
|
||||
location_type.value, self.control_point)
|
||||
position = self.random_from_miz_data(
|
||||
location_type == LocationType.OffshoreStrikeTarget)
|
||||
if position is not None:
|
||||
return position
|
||||
|
||||
logging.debug(f"No mizdata location for %s at %s", location_type.value,
|
||||
self.control_point)
|
||||
position = self.random_position(location_type)
|
||||
if position is not None:
|
||||
return position
|
||||
|
||||
logging.error(f"Could not find position for %s at %s",
|
||||
location_type.value, self.control_point)
|
||||
return None
|
||||
|
||||
def random_from_miz_data(self, offshore: bool) -> Optional[Point]:
|
||||
if offshore:
|
||||
locations = self.miz_data.offshore_locations
|
||||
else:
|
||||
locations = self.miz_data.ashore_locations
|
||||
if self.miz_data.offshore_locations:
|
||||
preset = random.choice(locations)
|
||||
locations.remove(preset)
|
||||
return preset.position
|
||||
return None
|
||||
|
||||
def random_position(self, location_type: LocationType) -> Optional[Point]:
|
||||
# TODO: Flesh out preset locations so we never hit this case.
|
||||
logging.warning("Falling back to random location for %s at %s",
|
||||
location_type.value, self.control_point)
|
||||
|
||||
is_base_defense = location_type in {
|
||||
LocationType.BaseAirDefense,
|
||||
LocationType.Garrison,
|
||||
LocationType.Shorad,
|
||||
}
|
||||
|
||||
on_land = location_type not in {
|
||||
LocationType.OffshoreStrikeTarget,
|
||||
LocationType.Ship,
|
||||
}
|
||||
|
||||
avoid_others = location_type not in {
|
||||
LocationType.Garrison,
|
||||
LocationType.MissileSite,
|
||||
LocationType.Sam,
|
||||
LocationType.Ship,
|
||||
LocationType.Shorad,
|
||||
}
|
||||
|
||||
if is_base_defense:
|
||||
min_range = 400
|
||||
max_range = 3200
|
||||
elif location_type == LocationType.Ship:
|
||||
min_range = 5000
|
||||
max_range = 40000
|
||||
elif location_type == LocationType.MissileSite:
|
||||
min_range = 2500
|
||||
max_range = 40000
|
||||
else:
|
||||
min_range = 10000
|
||||
max_range = 40000
|
||||
|
||||
position = self._find_random_position(min_range, max_range,
|
||||
on_land, is_base_defense,
|
||||
avoid_others)
|
||||
|
||||
# Retry once, searching a bit further (On some big airbases, 3200 is too
|
||||
# short (Ex : Incirlik)), but searching farther on every base would be
|
||||
# problematic, as some base defense units would end up very far away
|
||||
# from small airfields.
|
||||
if position is None and is_base_defense:
|
||||
position = self._find_random_position(3200, 4800,
|
||||
on_land, is_base_defense,
|
||||
avoid_others)
|
||||
return position
|
||||
|
||||
def _find_random_position(self, min_range: int, max_range: int,
|
||||
on_ground: bool, is_base_defense: bool,
|
||||
avoid_others: bool) -> Optional[Point]:
|
||||
"""
|
||||
Find a valid ground object location
|
||||
:param on_ground: Whether it should be on ground or on sea (True = on
|
||||
ground)
|
||||
:param min_range: Minimal range from point
|
||||
:param max_range: Max range from point
|
||||
:param is_base_defense: True if the location is for base defense.
|
||||
:return:
|
||||
"""
|
||||
near = self.control_point.position
|
||||
others = self.control_point.ground_objects
|
||||
|
||||
def is_valid(point: Optional[Point]) -> bool:
|
||||
if point is None:
|
||||
return False
|
||||
|
||||
if on_ground and not self.game.theater.is_on_land(point):
|
||||
return False
|
||||
elif not on_ground and not self.game.theater.is_in_sea(point):
|
||||
return False
|
||||
|
||||
if avoid_others:
|
||||
for other in others:
|
||||
if other.position.distance_to_point(point) < 10000:
|
||||
return False
|
||||
|
||||
if is_base_defense:
|
||||
# If it's a base defense we don't care how close it is to other
|
||||
# points.
|
||||
return True
|
||||
|
||||
# Else verify that it's not too close to another control point.
|
||||
for control_point in self.game.theater.controlpoints:
|
||||
if control_point != self.control_point:
|
||||
if control_point.position.distance_to_point(point) < 30000:
|
||||
return False
|
||||
for ground_obj in control_point.ground_objects:
|
||||
if ground_obj.position.distance_to_point(point) < 10000:
|
||||
return False
|
||||
return True
|
||||
|
||||
for _ in range(300):
|
||||
# Check if on land or sea
|
||||
p = near.random_point_within(max_range, min_range)
|
||||
if is_valid(p):
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
class ControlPointGroundObjectGenerator:
|
||||
def __init__(self, game: Game, generator_settings: GeneratorSettings,
|
||||
control_point: ControlPoint) -> None:
|
||||
self.game = game
|
||||
self.generator_settings = generator_settings
|
||||
self.control_point = control_point
|
||||
self.location_finder = LocationFinder(game, control_point)
|
||||
|
||||
@property
|
||||
def faction_name(self) -> str:
|
||||
if self.control_point.captured:
|
||||
return self.game.player_name
|
||||
else:
|
||||
return self.game.enemy_name
|
||||
|
||||
@property
|
||||
def faction(self) -> Faction:
|
||||
return db.FACTIONS[self.faction_name]
|
||||
|
||||
def generate(self) -> bool:
|
||||
self.control_point.connected_objectives = []
|
||||
if self.faction.navy_generators:
|
||||
# Even airbases can generate navies if they are close enough to the
|
||||
# water. This is not controlled by the control point definition, but
|
||||
# rather by whether or not the generator can find a valid position
|
||||
# for the ship.
|
||||
self.generate_navy()
|
||||
|
||||
return True
|
||||
|
||||
def generate_navy(self) -> None:
|
||||
skip_player_navy = self.generator_settings.no_player_navy
|
||||
if self.control_point.captured and skip_player_navy:
|
||||
return
|
||||
|
||||
skip_enemy_navy = self.generator_settings.no_enemy_navy
|
||||
if not self.control_point.captured and skip_enemy_navy:
|
||||
return
|
||||
|
||||
for _ in range(self.faction.navy_group_count):
|
||||
self.generate_ship()
|
||||
|
||||
def generate_ship(self) -> None:
|
||||
point = self.location_finder.location_for(
|
||||
LocationType.OffshoreStrikeTarget)
|
||||
if point is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = ShipGroundObject(namegen.random_objective_name(), group_id, point,
|
||||
self.control_point)
|
||||
|
||||
group = generate_ship_group(self.game, g, self.faction_name)
|
||||
g.groups = []
|
||||
if group is not None:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
|
||||
class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
def generate(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
def generate(self) -> bool:
|
||||
if not super().generate():
|
||||
return False
|
||||
|
||||
carrier_names = self.faction.carrier_names
|
||||
if not carrier_names:
|
||||
logging.info(
|
||||
f"Skipping generation of {self.control_point.name} because "
|
||||
f"{self.faction_name} has no carriers")
|
||||
return False
|
||||
|
||||
# Create ground object group
|
||||
group_id = self.game.next_group_id()
|
||||
g = CarrierGroundObject(namegen.random_objective_name(), group_id,
|
||||
self.control_point)
|
||||
group = generate_carrier_group(self.faction_name, self.game, g)
|
||||
g.groups = []
|
||||
if group is not None:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
self.control_point.name = random.choice(carrier_names)
|
||||
return True
|
||||
|
||||
|
||||
class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
def generate(self) -> bool:
|
||||
if not super().generate():
|
||||
return False
|
||||
|
||||
lha_names = self.faction.helicopter_carrier_names
|
||||
if not lha_names:
|
||||
logging.info(
|
||||
f"Skipping generation of {self.control_point.name} because "
|
||||
f"{self.faction_name} has no LHAs")
|
||||
return False
|
||||
|
||||
# Create ground object group
|
||||
group_id = self.game.next_group_id()
|
||||
g = LhaGroundObject(namegen.random_objective_name(), group_id,
|
||||
self.control_point)
|
||||
group = generate_lha_group(self.faction_name, self.game, g)
|
||||
g.groups = []
|
||||
if group is not None:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
self.control_point.name = random.choice(lha_names)
|
||||
return True
|
||||
|
||||
|
||||
class BaseDefenseGenerator:
|
||||
def __init__(self, game: Game, control_point: ControlPoint) -> None:
|
||||
self.game = game
|
||||
self.control_point = control_point
|
||||
self.location_finder = LocationFinder(game, control_point)
|
||||
|
||||
@property
|
||||
def faction_name(self) -> str:
|
||||
if self.control_point.captured:
|
||||
return self.game.player_name
|
||||
else:
|
||||
return self.game.enemy_name
|
||||
|
||||
@property
|
||||
def faction(self) -> Faction:
|
||||
return db.FACTIONS[self.faction_name]
|
||||
|
||||
def generate(self) -> None:
|
||||
self.generate_ewr()
|
||||
self.generate_garrison()
|
||||
self.generate_base_defenses()
|
||||
|
||||
def generate_ewr(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Ewr)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = EwrGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point)
|
||||
|
||||
group = generate_ewr_group(self.game, g, self.faction)
|
||||
if group is None:
|
||||
logging.error(f"Could not generate EWR at {self.control_point}")
|
||||
return
|
||||
|
||||
g.groups = [group]
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_base_defenses(self) -> None:
|
||||
# First group has a 1/2 chance of being a SAM, 1/6 chance of SHORAD,
|
||||
# and a 1/6 chance of a garrison.
|
||||
#
|
||||
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
|
||||
# being a garrison.
|
||||
for i in range(random.randint(2, 5)):
|
||||
if i == 0 and random.randint(0, 1) == 0:
|
||||
self.generate_sam()
|
||||
elif random.randint(0, 2) == 1:
|
||||
self.generate_shorad()
|
||||
else:
|
||||
self.generate_garrison()
|
||||
|
||||
def generate_garrison(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Garrison)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = VehicleGroupGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point,
|
||||
for_airbase=True)
|
||||
|
||||
group = generate_armor_group(self.faction_name, self.game, g)
|
||||
if group is None:
|
||||
logging.error(
|
||||
f"Could not generate garrison at {self.control_point}")
|
||||
return
|
||||
g.groups.append(group)
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_sam(self) -> None:
|
||||
position = self.location_finder.location_for(
|
||||
LocationType.BaseAirDefense)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point, for_airbase=True)
|
||||
|
||||
group = generate_anti_air_group(self.game, g, self.faction)
|
||||
if group is None:
|
||||
logging.error(f"Could not generate SAM at {self.control_point}")
|
||||
return
|
||||
g.groups.append(group)
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_shorad(self) -> None:
|
||||
position = self.location_finder.location_for(
|
||||
LocationType.BaseAirDefense)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point, for_airbase=True)
|
||||
|
||||
group = generate_anti_air_group(self.game, g, self.faction,
|
||||
ranges=[{AirDefenseRange.Short}])
|
||||
if group is None:
|
||||
logging.error(
|
||||
f"Could not generate SHORAD group at {self.control_point}")
|
||||
return
|
||||
g.groups.append(group)
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
|
||||
class FobDefenseGenerator(BaseDefenseGenerator):
|
||||
def generate(self) -> None:
|
||||
self.generate_garrison()
|
||||
self.generate_fob_defenses()
|
||||
|
||||
def generate_fob_defenses(self):
|
||||
# First group has a 1/2 chance of being a SHORAD,
|
||||
# and a 1/2 chance of a garrison.
|
||||
#
|
||||
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
|
||||
# being a garrison.
|
||||
for i in range(random.randint(2, 5)):
|
||||
if i == 0 and random.randint(0, 1) == 0:
|
||||
self.generate_shorad()
|
||||
elif i == 0 and random.randint(0, 1) == 0:
|
||||
self.generate_garrison()
|
||||
elif random.randint(0, 2) == 1:
|
||||
self.generate_shorad()
|
||||
else:
|
||||
self.generate_garrison()
|
||||
|
||||
|
||||
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
def __init__(self, game: Game, generator_settings: GeneratorSettings,
|
||||
control_point: ControlPoint,
|
||||
templates: GroundObjectTemplates) -> None:
|
||||
super().__init__(game, generator_settings, control_point)
|
||||
self.templates = templates
|
||||
|
||||
def generate(self) -> bool:
|
||||
if not super().generate():
|
||||
return False
|
||||
|
||||
BaseDefenseGenerator(self.game, self.control_point).generate()
|
||||
self.generate_ground_points()
|
||||
|
||||
if self.faction.missiles:
|
||||
self.generate_missile_sites()
|
||||
|
||||
return True
|
||||
|
||||
def generate_ground_points(self) -> None:
|
||||
"""Generate ground objects and AA sites for the control point."""
|
||||
skip_sams = self.generate_required_aa()
|
||||
|
||||
if self.control_point.is_global:
|
||||
return
|
||||
|
||||
# Always generate at least one AA point.
|
||||
self.generate_aa_site()
|
||||
|
||||
# And between 2 and 7 other objectives.
|
||||
amount = random.randrange(2, 7)
|
||||
for i in range(amount):
|
||||
# 1 in 4 additional objectives are AA.
|
||||
if random.randint(0, 3) == 0:
|
||||
if skip_sams > 0:
|
||||
skip_sams -= 1
|
||||
else:
|
||||
self.generate_aa_site()
|
||||
else:
|
||||
self.generate_ground_point()
|
||||
|
||||
def generate_required_aa(self) -> int:
|
||||
"""Generates the AA sites that are required by the campaign.
|
||||
|
||||
Returns:
|
||||
The number of AA sites that were generated.
|
||||
"""
|
||||
presets = self.control_point.preset_locations
|
||||
for position in presets.required_long_range_sams:
|
||||
self.generate_aa_at(position, ranges=[
|
||||
{AirDefenseRange.Long},
|
||||
{AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
])
|
||||
for position in presets.required_medium_range_sams:
|
||||
self.generate_aa_at(position, ranges=[
|
||||
{AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
])
|
||||
return (len(presets.required_long_range_sams) +
|
||||
len(presets.required_medium_range_sams))
|
||||
|
||||
def generate_ground_point(self) -> None:
|
||||
try:
|
||||
category = random.choice(self.faction.building_set)
|
||||
except IndexError:
|
||||
logging.exception("Faction has no buildings defined")
|
||||
return
|
||||
|
||||
obj_name = namegen.random_objective_name()
|
||||
template = random.choice(list(self.templates[category].values()))
|
||||
|
||||
if category == "oil":
|
||||
location_type = LocationType.OffshoreStrikeTarget
|
||||
else:
|
||||
location_type = LocationType.StrikeTarget
|
||||
|
||||
# Pick from preset locations
|
||||
point = self.location_finder.location_for(location_type)
|
||||
if point is None:
|
||||
return
|
||||
|
||||
object_id = 0
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
# TODO: Create only one TGO per objective, each with multiple units.
|
||||
for unit in template:
|
||||
object_id += 1
|
||||
|
||||
template_point = Point(unit["offset"].x, unit["offset"].y)
|
||||
g = BuildingGroundObject(
|
||||
obj_name, category, group_id, object_id, point + template_point,
|
||||
unit["heading"], self.control_point, unit["type"])
|
||||
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_aa_site(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Sam)
|
||||
if position is None:
|
||||
return
|
||||
self.generate_aa_at(position, ranges=[
|
||||
# Prefer to use proper SAMs, but fall back to SHORADs if needed.
|
||||
{AirDefenseRange.Long, AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
])
|
||||
|
||||
def generate_aa_at(
|
||||
self, position: Point,
|
||||
ranges: Iterable[Set[AirDefenseRange]]) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point, for_airbase=False)
|
||||
group = generate_anti_air_group(self.game, g, self.faction, ranges)
|
||||
if group is None:
|
||||
logging.error("Could not generate air defense group for %s at %s",
|
||||
g.name, self.control_point)
|
||||
return
|
||||
g.groups = [group]
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_missile_sites(self) -> None:
|
||||
for i in range(self.faction.missiles_group_count):
|
||||
self.generate_missile_site()
|
||||
|
||||
def generate_missile_site(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.MissileSite)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = MissileSiteGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point)
|
||||
group = generate_missile_group(self.game, g, self.faction_name)
|
||||
g.groups = []
|
||||
if group is not None:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
return
|
||||
|
||||
|
||||
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||
def generate(self) -> bool:
|
||||
self.generate_fob()
|
||||
FobDefenseGenerator(self.game, self.control_point).generate()
|
||||
self.generate_required_aa()
|
||||
return True
|
||||
|
||||
def generate_fob(self) -> None:
|
||||
try:
|
||||
category = self.faction.building_set[self.faction.building_set.index('fob')]
|
||||
except IndexError:
|
||||
logging.exception("Faction has no fob buildings defined")
|
||||
return
|
||||
|
||||
obj_name = self.control_point.name
|
||||
template = random.choice(list(self.templates[category].values()))
|
||||
point = self.control_point.position
|
||||
# Pick from preset locations
|
||||
object_id = 0
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
# TODO: Create only one TGO per objective, each with multiple units.
|
||||
for unit in template:
|
||||
object_id += 1
|
||||
|
||||
template_point = Point(unit["offset"].x, unit["offset"].y)
|
||||
g = BuildingGroundObject(
|
||||
obj_name, category, group_id, object_id, point + template_point,
|
||||
unit["heading"], self.control_point, unit["type"], airbase_group=True)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
|
||||
class GroundObjectGenerator:
|
||||
def __init__(self, game: Game,
|
||||
generator_settings: GeneratorSettings) -> None:
|
||||
self.game = game
|
||||
self.generator_settings = generator_settings
|
||||
with open("resources/groundobject_templates.p", "rb") as f:
|
||||
self.templates: GroundObjectTemplates = pickle.load(f)
|
||||
|
||||
def generate(self) -> None:
|
||||
# Copied so we can remove items from the original list without breaking
|
||||
# the iterator.
|
||||
control_points = list(self.game.theater.controlpoints)
|
||||
for control_point in control_points:
|
||||
if not self.generate_for_control_point(control_point):
|
||||
self.game.theater.controlpoints.remove(control_point)
|
||||
|
||||
def generate_for_control_point(self, control_point: ControlPoint) -> bool:
|
||||
generator: ControlPointGroundObjectGenerator
|
||||
if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
|
||||
generator = CarrierGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point)
|
||||
elif control_point.cptype == ControlPointType.LHA_GROUP:
|
||||
generator = LhaGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point)
|
||||
elif isinstance(control_point, OffMapSpawn):
|
||||
generator = NoOpGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point)
|
||||
elif isinstance(control_point, Fob):
|
||||
generator = FobGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point,
|
||||
self.templates)
|
||||
else:
|
||||
generator = AirbaseGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point,
|
||||
self.templates)
|
||||
return generator.generate()
|
||||
364
game/theater/theatergroundobject.py
Normal file
364
game/theater/theatergroundobject.py
Normal file
@@ -0,0 +1,364 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from typing import Iterator, List, TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import Group
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .controlpoint import ControlPoint
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
from .missiontarget import MissionTarget
|
||||
|
||||
NAME_BY_CATEGORY = {
|
||||
"power": "Power plant",
|
||||
"ammo": "Ammo depot",
|
||||
"fuel": "Fuel depot",
|
||||
"aa": "AA Defense Site",
|
||||
"ware": "Warehouse",
|
||||
"farp": "FARP",
|
||||
"fob": "FOB",
|
||||
"factory": "Factory",
|
||||
"comms": "Comms. tower",
|
||||
"oil": "Oil platform",
|
||||
"derrick": "Derrick",
|
||||
"ww2bunker": "Bunker",
|
||||
"village": "Village",
|
||||
"allycamp": "Camp",
|
||||
"EWR":"EWR",
|
||||
}
|
||||
|
||||
ABBREV_NAME = {
|
||||
"power": "PLANT",
|
||||
"ammo": "AMMO",
|
||||
"fuel": "FUEL",
|
||||
"aa": "AA",
|
||||
"ware": "WARE",
|
||||
"farp": "FARP",
|
||||
"fob": "FOB",
|
||||
"factory": "FACTORY",
|
||||
"comms": "COMMST",
|
||||
"oil": "OILP",
|
||||
"derrick": "DERK",
|
||||
"ww2bunker": "BUNK",
|
||||
"village": "VLG",
|
||||
"allycamp": "CMP",
|
||||
}
|
||||
|
||||
CATEGORY_MAP = {
|
||||
|
||||
# Special cases
|
||||
"CARRIER": ["CARRIER"],
|
||||
"LHA": ["LHA"],
|
||||
"aa": ["AA"],
|
||||
|
||||
# Buildings
|
||||
"power": ["Workshop A", "Electric power box", "Garage small A", "Farm B", "Repair workshop", "Garage B"],
|
||||
"ware": ["Warehouse", "Hangar A"],
|
||||
"fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"],
|
||||
"ammo": [".Ammunition depot", "Hangar B"],
|
||||
"farp": ["FARP Tent", "FARP Ammo Dump Coating", "FARP Fuel Depot", "FARP Command Post", "FARP CP Blindage"],
|
||||
"fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"],
|
||||
"factory": ["Tech combine", "Tech hangar A"],
|
||||
"comms": ["TV tower", "Comms tower M"],
|
||||
"oil": ["Oil platform"],
|
||||
"derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"],
|
||||
"ww2bunker": ["Siegfried Line", "Fire Control Bunker", "SK_C_28_naval_gun", "Concertina Wire", "Czech hedgehogs 1"],
|
||||
"village": ["Small house 1B", "Small House 1A", "Small warehouse 1"],
|
||||
"allycamp": [],
|
||||
}
|
||||
|
||||
|
||||
class TheaterGroundObject(MissionTarget):
|
||||
|
||||
def __init__(self, name: str, category: str, group_id: int, position: Point,
|
||||
heading: int, control_point: ControlPoint, dcs_identifier: str,
|
||||
airbase_group: bool, sea_object: bool) -> None:
|
||||
super().__init__(name, position)
|
||||
self.category = category
|
||||
self.group_id = group_id
|
||||
self.heading = heading
|
||||
self.control_point = control_point
|
||||
self.dcs_identifier = dcs_identifier
|
||||
self.airbase_group = airbase_group
|
||||
self.sea_object = sea_object
|
||||
self.is_dead = False
|
||||
# TODO: There is never more than one group.
|
||||
self.groups: List[Group] = []
|
||||
|
||||
@property
|
||||
def units(self) -> List[Unit]:
|
||||
"""
|
||||
:return: all the units at this location
|
||||
"""
|
||||
return list(itertools.chain.from_iterable([g.units for g in self.groups]))
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
"""The name of the unit group."""
|
||||
return f"{self.category}|{self.group_id}"
|
||||
|
||||
@property
|
||||
def waypoint_name(self) -> str:
|
||||
return f"[{self.name}] {self.category}"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return NAME_BY_CATEGORY[self.category]
|
||||
|
||||
def is_same_group(self, identifier: str) -> bool:
|
||||
return self.group_id == identifier
|
||||
|
||||
@property
|
||||
def obj_name(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def faction_color(self) -> str:
|
||||
return "BLUE" if self.control_point.captured else "RED"
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
return self.control_point.is_friendly(to_player)
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
# TODO: FlightType.LOGISTICS
|
||||
# TODO: FlightType.TROOP_TRANSPORT
|
||||
]
|
||||
else:
|
||||
yield from [
|
||||
FlightType.STRIKE,
|
||||
FlightType.BAI,
|
||||
]
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def alive_unit_count(self) -> int:
|
||||
return sum(len(g.units) for g in self.groups)
|
||||
|
||||
@property
|
||||
def might_have_aa(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class BuildingGroundObject(TheaterGroundObject):
|
||||
def __init__(self, name: str, category: str, group_id: int, object_id: int,
|
||||
position: Point, heading: int, control_point: ControlPoint,
|
||||
dcs_identifier: str, airbase_group=False) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category=category,
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=heading,
|
||||
control_point=control_point,
|
||||
dcs_identifier=dcs_identifier,
|
||||
airbase_group=airbase_group,
|
||||
sea_object=False
|
||||
)
|
||||
self.object_id = object_id
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
"""The name of the unit group."""
|
||||
return f"{self.category}|{self.group_id}|{self.object_id}"
|
||||
|
||||
@property
|
||||
def waypoint_name(self) -> str:
|
||||
return f"{super().waypoint_name} #{self.object_id}"
|
||||
|
||||
|
||||
class NavalGroundObject(TheaterGroundObject):
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if not self.is_friendly(for_player):
|
||||
yield FlightType.ANTISHIP
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def might_have_aa(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class GenericCarrierGroundObject(NavalGroundObject):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: Why is this both a CP and a TGO?
|
||||
class CarrierGroundObject(GenericCarrierGroundObject):
|
||||
def __init__(self, name: str, group_id: int,
|
||||
control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="CARRIER",
|
||||
group_id=group_id,
|
||||
position=control_point.position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="CARRIER",
|
||||
airbase_group=True,
|
||||
sea_object=True
|
||||
)
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them,
|
||||
# add to EWR.
|
||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||
|
||||
|
||||
# TODO: Why is this both a CP and a TGO?
|
||||
class LhaGroundObject(GenericCarrierGroundObject):
|
||||
def __init__(self, name: str, group_id: int,
|
||||
control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="LHA",
|
||||
group_id=group_id,
|
||||
position=control_point.position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="LHA",
|
||||
airbase_group=True,
|
||||
sea_object=True
|
||||
)
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them,
|
||||
# add to EWR.
|
||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||
|
||||
|
||||
class MissileSiteGroundObject(TheaterGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=False,
|
||||
sea_object=False
|
||||
)
|
||||
|
||||
|
||||
class BaseDefenseGroundObject(TheaterGroundObject):
|
||||
"""Base type for all base defenses."""
|
||||
|
||||
|
||||
# TODO: Differentiate types.
|
||||
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
|
||||
# be split into their own types.
|
||||
class SamGroundObject(BaseDefenseGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint, for_airbase: bool) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=for_airbase,
|
||||
sea_object=False
|
||||
)
|
||||
# Set by the SAM unit generator if the generated group is compatible
|
||||
# with Skynet.
|
||||
self.skynet_capable = False
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
if self.skynet_capable:
|
||||
# Prefix the group names of SAM sites with the side color so Skynet
|
||||
# can find them.
|
||||
return f"{self.faction_color}|SAM|{self.group_id}"
|
||||
else:
|
||||
return super().group_name
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if not self.is_friendly(for_player):
|
||||
yield FlightType.DEAD
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def might_have_aa(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint, for_airbase: bool) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=for_airbase,
|
||||
sea_object=False
|
||||
)
|
||||
|
||||
|
||||
class EwrGroundObject(BaseDefenseGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="EWR",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="EWR",
|
||||
airbase_group=True,
|
||||
sea_object=False
|
||||
)
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them.
|
||||
return f"{self.faction_color}|{super().group_name}"
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if not self.is_friendly(for_player):
|
||||
yield FlightType.DEAD
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def might_have_aa(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class ShipGroundObject(NavalGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=False,
|
||||
sea_object=True
|
||||
)
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them,
|
||||
# add to EWR.
|
||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||
133
game/unitmap.py
Normal file
133
game/unitmap.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Maps generated units back to their Liberation types."""
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional, Type
|
||||
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game import db
|
||||
from game.theater import Airfield, ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import BuildingGroundObject
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrontLineUnit:
|
||||
unit_type: Type[VehicleType]
|
||||
origin: ControlPoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundObjectUnit:
|
||||
ground_object: TheaterGroundObject
|
||||
group: Group
|
||||
unit: Unit
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Building:
|
||||
ground_object: BuildingGroundObject
|
||||
|
||||
|
||||
class UnitMap:
|
||||
def __init__(self) -> None:
|
||||
self.aircraft: Dict[str, Flight] = {}
|
||||
self.airfields: Dict[str, Airfield] = {}
|
||||
self.front_line_units: Dict[str, FrontLineUnit] = {}
|
||||
self.ground_object_units: Dict[str, GroundObjectUnit] = {}
|
||||
self.buildings: Dict[str, Building] = {}
|
||||
|
||||
def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None:
|
||||
for unit in group.units:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(unit.name)
|
||||
if name in self.aircraft:
|
||||
raise RuntimeError(f"Duplicate unit name: {name}")
|
||||
self.aircraft[name] = flight
|
||||
|
||||
def flight(self, unit_name: str) -> Optional[Flight]:
|
||||
return self.aircraft.get(unit_name, None)
|
||||
|
||||
def add_airfield(self, airfield: Airfield) -> None:
|
||||
if airfield.name in self.airfields:
|
||||
raise RuntimeError(f"Duplicate airfield: {airfield.name}")
|
||||
self.airfields[airfield.name] = airfield
|
||||
|
||||
def airfield(self, name: str) -> Optional[Airfield]:
|
||||
return self.airfields.get(name, None)
|
||||
|
||||
def add_front_line_units(self, group: Group, origin: ControlPoint) -> None:
|
||||
for unit in group.units:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(unit.name)
|
||||
if name in self.front_line_units:
|
||||
raise RuntimeError(f"Duplicate front line unit: {name}")
|
||||
unit_type = db.unit_type_from_name(unit.type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(f"Unknown unit type: {unit.type}")
|
||||
if not issubclass(unit_type, VehicleType):
|
||||
raise RuntimeError(
|
||||
f"{name} is a {unit_type.__name__}, expected a VehicleType")
|
||||
self.front_line_units[name] = FrontLineUnit(unit_type, origin)
|
||||
|
||||
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
|
||||
return self.front_line_units.get(name, None)
|
||||
|
||||
def add_ground_object_units(self, ground_object: TheaterGroundObject,
|
||||
persistence_group: Group,
|
||||
miz_group: Group) -> None:
|
||||
"""Adds a group associated with a TGO to the unit map.
|
||||
|
||||
Args:
|
||||
ground_object: The TGO the group is associated with.
|
||||
persistence_group: The Group tracked by the TGO itself.
|
||||
miz_group: The Group spawned for the miz to match persistence_group.
|
||||
"""
|
||||
# Deaths for units at TGOs are recorded in the Group that is contained
|
||||
# by the TGO, but when groundobjectsgen populates the miz it creates new
|
||||
# groups based on that template, so the units and groups in the miz are
|
||||
# not a direct match for the units and groups that persist in the TGO.
|
||||
#
|
||||
# This means that we need to map the spawned unit names back to the
|
||||
# original TGO units, not the ones in the miz.
|
||||
if len(persistence_group.units) != len(miz_group.units):
|
||||
raise ValueError("Persistent group does not match generated group")
|
||||
unit_pairs = zip(persistence_group.units, miz_group.units)
|
||||
for persistent_unit, miz_unit in unit_pairs:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(miz_unit.name)
|
||||
if name in self.ground_object_units:
|
||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||
self.ground_object_units[name] = GroundObjectUnit(
|
||||
ground_object, persistence_group, persistent_unit)
|
||||
|
||||
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
|
||||
return self.ground_object_units.get(name, None)
|
||||
|
||||
def add_building(self, ground_object: BuildingGroundObject,
|
||||
group: Group) -> None:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(group.name)
|
||||
if name in self.buildings:
|
||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||
self.buildings[name] = Building(ground_object)
|
||||
|
||||
def add_fortification(self, ground_object: BuildingGroundObject,
|
||||
group: VehicleGroup) -> None:
|
||||
if len(group.units) != 1:
|
||||
raise ValueError("Fortification groups must have exactly one unit.")
|
||||
unit = group.units[0]
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(unit.name)
|
||||
if name in self.buildings:
|
||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||
self.buildings[name] = Building(ground_object)
|
||||
|
||||
def building_or_fortification(self, name: str) -> Optional[Building]:
|
||||
return self.buildings.get(name, None)
|
||||
75
game/utils.py
Normal file
75
game/utils.py
Normal file
@@ -0,0 +1,75 @@
|
||||
def meter_to_feet(value_in_meter: float) -> int:
|
||||
"""Converts meters to feets
|
||||
|
||||
:arg value_in_meter Value in meters
|
||||
"""
|
||||
return int(3.28084 * value_in_meter)
|
||||
|
||||
|
||||
def feet_to_meter(value_in_feet: float) -> int:
|
||||
"""Converts feets to meters
|
||||
|
||||
:arg value_in_feet Value in feets
|
||||
"""
|
||||
return int(value_in_feet / 3.28084)
|
||||
|
||||
|
||||
def meter_to_nm(value_in_meter: float) -> int:
|
||||
"""Converts meters to nautic miles
|
||||
|
||||
:arg value_in_meter Value in meters
|
||||
"""
|
||||
return int(value_in_meter / 1852)
|
||||
|
||||
|
||||
def nm_to_meter(value_in_nm: float) -> int:
|
||||
"""Converts nautic miles to meters
|
||||
|
||||
:arg value_in_nm Value in nautic miles
|
||||
"""
|
||||
return int(value_in_nm * 1852)
|
||||
|
||||
|
||||
def knots_to_kph(value_in_knots: float) -> int:
|
||||
"""Converts Knots to Kilometer Per Hour
|
||||
|
||||
:arg value_in_knots Knots
|
||||
"""
|
||||
return int(value_in_knots * 1.852)
|
||||
|
||||
|
||||
def mps_to_knots(value_in_mps: float) -> int:
|
||||
"""Converts Meters Per Second To Knots
|
||||
|
||||
:arg value_in_mps Meters Per Second
|
||||
"""
|
||||
return int(value_in_mps * 1.943)
|
||||
|
||||
|
||||
def mps_to_kph(speed: float) -> int:
|
||||
"""Converts meters per second to kilometers per hour.
|
||||
|
||||
:arg speed Speed in m/s.
|
||||
"""
|
||||
return int(speed * 3.6)
|
||||
|
||||
|
||||
def kph_to_mps(speed: float) -> int:
|
||||
"""Converts kilometers per hour to meters per second.
|
||||
|
||||
:arg speed Speed in KPH.
|
||||
"""
|
||||
return int(speed / 3.6)
|
||||
|
||||
|
||||
def heading_sum(h, a) -> int:
|
||||
h += a
|
||||
if h > 360:
|
||||
return h - 360
|
||||
elif h < 0:
|
||||
return 360 + h
|
||||
else:
|
||||
return h
|
||||
|
||||
def opposite_heading(h):
|
||||
return heading_sum(h, 180)
|
||||
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.3.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()
|
||||
185
game/weather.py
Normal file
185
game/weather.py
Normal file
@@ -0,0 +1,185 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.weather import Weather as PydcsWeather, Wind
|
||||
|
||||
from game.settings import Settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.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
|
||||
)
|
||||
|
||||
1923
gen/aircraft.py
1923
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
160
gen/airsupportgen.py
Normal file
160
gen/airsupportgen.py
Normal file
@@ -0,0 +1,160 @@
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Type, Tuple
|
||||
|
||||
from dcs.mission import Mission, StartType
|
||||
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
|
||||
from dcs.unittype import UnitType
|
||||
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]
|
||||
|
||||
@staticmethod
|
||||
def _get_tanker_params(unit_type: Type[UnitType]) -> Tuple[int, int]:
|
||||
if unit_type is KC130:
|
||||
return (TANKER_ALT - 500, 596)
|
||||
elif unit_type is KC_135:
|
||||
return (TANKER_ALT, 770)
|
||||
elif unit_type is KC135MPRS:
|
||||
return (TANKER_ALT + 500, 596)
|
||||
return (TANKER_ALT, 574)
|
||||
|
||||
def generate(self):
|
||||
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)):
|
||||
alt, airspeed = self._get_tanker_params(tanker_unit_type)
|
||||
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=alt,
|
||||
race_distance=58000,
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
speed=airspeed,
|
||||
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))
|
||||
|
||||
possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
|
||||
|
||||
if len(possible_awacs) > 0:
|
||||
awacs_unit = possible_awacs[0]
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
|
||||
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))
|
||||
else:
|
||||
logging.warning("No AWACS for faction")
|
||||
758
gen/armor.py
758
gen/armor.py
@@ -1,64 +1,720 @@
|
||||
from game import db
|
||||
from .conflictgen import *
|
||||
from .naming import *
|
||||
from __future__ import annotations
|
||||
|
||||
from dcs.mission import *
|
||||
from dcs.unittype import *
|
||||
from dcs.point import *
|
||||
from dcs.task import *
|
||||
from dcs.country import *
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
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 (EPLRS, AttackGroup, ControlledTask, FireAtPoint,
|
||||
GoToWaypoint, Hold, OrbitAction, SetImmortalCommand,
|
||||
SetInvisibleCommand)
|
||||
from dcs.triggers import Event, TriggerOnce
|
||||
from dcs.unit import Vehicle
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
from dcs.unittype import VehicleType
|
||||
from game import db
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import heading_sum, opposite_heading
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
|
||||
from gen.ground_forces.ai_ground_planner import (DISTANCE_FROM_FRONTLINE,
|
||||
CombatGroup, CombatGroupRole)
|
||||
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .conflictgen import Conflict
|
||||
from .ground_forces.combat_stance import CombatStance
|
||||
from .naming import namegen
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
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: Game,
|
||||
player_planned_combat_groups: List[CombatGroup],
|
||||
enemy_planned_combat_groups: List[CombatGroup],
|
||||
player_stance: CombatStance,
|
||||
unit_map: UnitMap) -> None:
|
||||
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 = self._enemy_stance()
|
||||
self.game = game
|
||||
self.unit_map = unit_map
|
||||
self.jtacs: List[JtacInfo] = []
|
||||
|
||||
def _group_point(self, point) -> Point:
|
||||
distance = randint(
|
||||
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]),
|
||||
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[1]),
|
||||
def _enemy_stance(self):
|
||||
"""Picks the enemy stance according to the number of planned groups on the frontline for each side"""
|
||||
if len(self.enemy_planned_combat_groups) > len(self.player_planned_combat_groups):
|
||||
return random.choice(
|
||||
[
|
||||
CombatStance.AGGRESSIVE,
|
||||
CombatStance.AGGRESSIVE,
|
||||
CombatStance.AGGRESSIVE,
|
||||
CombatStance.ELIMINATION,
|
||||
CombatStance.BREAKTHROUGH
|
||||
]
|
||||
)
|
||||
else:
|
||||
return random.choice(
|
||||
[
|
||||
CombatStance.DEFENSIVE,
|
||||
CombatStance.DEFENSIVE,
|
||||
CombatStance.DEFENSIVE,
|
||||
CombatStance.AMBUSH,
|
||||
CombatStance.AGGRESSIVE
|
||||
]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _group_point(point: Point, base_distance) -> Point:
|
||||
distance = random.randint(
|
||||
int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
|
||||
int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
|
||||
)
|
||||
return point.random_point_within(distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR)
|
||||
|
||||
def generate(self):
|
||||
position = Conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp, self.game.theater)
|
||||
frontline_vector = Conflict.frontline_vector(
|
||||
self.conflict.from_cp,
|
||||
self.conflict.to_cp,
|
||||
self.game.theater
|
||||
)
|
||||
|
||||
# Create player groups at random position
|
||||
player_groups = self._generate_groups(self.player_planned_combat_groups, frontline_vector, True)
|
||||
|
||||
# Create enemy groups at random position
|
||||
enemy_groups = self._generate_groups(self.enemy_planned_combat_groups, frontline_vector, False)
|
||||
|
||||
# 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: VehicleGroup,
|
||||
is_player: bool,
|
||||
side: Country,
|
||||
forward_heading: int
|
||||
) -> None:
|
||||
|
||||
infantry_position = self.conflict.find_ground_position(
|
||||
group.points[0].position.random_point_within(250, 50),
|
||||
500,
|
||||
forward_heading,
|
||||
self.conflict.theater
|
||||
)
|
||||
if not infantry_position:
|
||||
logging.warning("Could not find infantry position")
|
||||
return
|
||||
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
|
||||
|
||||
# Disable infantry unit gen if disabled
|
||||
if not self.game.settings.perf_infantry:
|
||||
if self.game.settings.manpads:
|
||||
# 50% of armored units protected by manpad
|
||||
if random.choice([True, False]):
|
||||
manpads = db.find_manpad(faction)
|
||||
if len(manpads) > 0:
|
||||
u = random.choice(manpads)
|
||||
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)
|
||||
return
|
||||
|
||||
possible_infantry_units = db.find_infantry(faction, allow_manpad=self.game.settings.manpads)
|
||||
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 _set_reform_waypoint(
|
||||
self,
|
||||
dcs_group: VehicleGroup,
|
||||
forward_heading: int
|
||||
) -> None:
|
||||
"""Setting a waypoint close to the spawn position allows the group to reform gracefully
|
||||
rather than spin
|
||||
"""
|
||||
reform_point = dcs_group.position.point_from_heading(forward_heading, 50)
|
||||
dcs_group.add_waypoint(reform_point)
|
||||
|
||||
def _plan_artillery_action(
|
||||
self,
|
||||
stance: CombatStance,
|
||||
gen_group: CombatGroup,
|
||||
dcs_group: VehicleGroup,
|
||||
forward_heading: int,
|
||||
target: Point
|
||||
) -> bool:
|
||||
"""
|
||||
Handles adding the DCS tasks for artillery groups for all combat stances.
|
||||
Returns True if tasking was added, returns False if the stance was not a combat stance.
|
||||
"""
|
||||
self._set_reform_waypoint(dcs_group, forward_heading)
|
||||
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))
|
||||
# TODO: Update to fire at group instead of point
|
||||
fire_task = FireAtPoint(target, len(gen_group.units) * 10, 100)
|
||||
fire_task.number = 2 if stance != CombatStance.RETREAT else 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[1].tasks.append(Hold())
|
||||
retreat = self.find_retreat_point(dcs_group, heading_sum(forward_heading, 180), (int)(RETREAT_DISTANCE/3))
|
||||
dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad)
|
||||
dcs_group.points[2].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(to_index=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)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _plan_tank_ifv_action(
|
||||
self,
|
||||
stance: CombatStance,
|
||||
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
||||
dcs_group: VehicleGroup,
|
||||
forward_heading: int,
|
||||
to_cp: ControlPoint,
|
||||
) -> bool:
|
||||
"""
|
||||
Handles adding the DCS tasks for tank and IFV groups for all combat stances.
|
||||
Returns True if tasking was added, returns False if the stance was not a combat stance.
|
||||
"""
|
||||
self._set_reform_waypoint(dcs_group, forward_heading)
|
||||
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
|
||||
)
|
||||
)
|
||||
target_point = self.conflict.theater.nearest_land_pos(
|
||||
target.points[0].position + rand_offset
|
||||
)
|
||||
dcs_group.add_waypoint(target_point)
|
||||
dcs_group.points[2].tasks.append(AttackGroup(target.id))
|
||||
|
||||
if (
|
||||
to_cp.position.distance_to_point(dcs_group.points[0].position)
|
||||
<=
|
||||
AGGRESIVE_MOVE_DISTANCE
|
||||
):
|
||||
attack_point = self.conflict.theater.nearest_land_pos(
|
||||
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.OffRoad)
|
||||
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 = self.conflict.theater.nearest_land_pos(
|
||||
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.OffRoad)
|
||||
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)
|
||||
for i, target in enumerate(targets, start=1):
|
||||
rand_offset = Point(
|
||||
random.randint(
|
||||
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
|
||||
),
|
||||
random.randint(
|
||||
-RANDOM_OFFSET_ATTACK,
|
||||
RANDOM_OFFSET_ATTACK
|
||||
)
|
||||
)
|
||||
target_point = self.conflict.theater.nearest_land_pos(
|
||||
target.points[0].position+rand_offset
|
||||
)
|
||||
dcs_group.add_waypoint(target_point, PointAction.OffRoad)
|
||||
dcs_group.points[i + 1].tasks.append(AttackGroup(target.id))
|
||||
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
|
||||
attack_point = self.conflict.theater.nearest_land_pos(
|
||||
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)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _plan_apc_atgm_action(
|
||||
self,
|
||||
stance: CombatStance,
|
||||
dcs_group: VehicleGroup,
|
||||
forward_heading: int,
|
||||
to_cp: ControlPoint,
|
||||
) -> bool:
|
||||
"""
|
||||
Handles adding the DCS tasks for APC and ATGM groups for all combat stances.
|
||||
Returns True if tasking was added, returns False if the stance was not a combat stance.
|
||||
"""
|
||||
self._set_reform_waypoint(dcs_group, forward_heading)
|
||||
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 = self.conflict.theater.nearest_land_pos(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.OffRoad)
|
||||
|
||||
if stance != CombatStance.RETREAT:
|
||||
self.add_morale_trigger(dcs_group, forward_heading)
|
||||
return True
|
||||
return False
|
||||
|
||||
def plan_action_for_groups(
|
||||
self, stance: CombatStance,
|
||||
ally_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
||||
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
||||
forward_heading: int,
|
||||
from_cp: ControlPoint,
|
||||
to_cp: ControlPoint
|
||||
) -> None:
|
||||
|
||||
if not self.game.settings.perf_moving_units:
|
||||
return
|
||||
|
||||
for dcs_group, group in ally_groups:
|
||||
if hasattr(group.units[0], 'eplrs') and group.units[0].eplrs:
|
||||
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
|
||||
|
||||
if group.role == CombatGroupRole.ARTILLERY:
|
||||
if self.game.settings.perf_artillery:
|
||||
target = self.get_artillery_target_in_range(dcs_group, group, enemy_groups)
|
||||
if target is not None:
|
||||
self._plan_artillery_action(stance, group, dcs_group, forward_heading, target)
|
||||
|
||||
elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]:
|
||||
self._plan_tank_ifv_action(stance, enemy_groups, dcs_group, forward_heading, to_cp)
|
||||
|
||||
elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]:
|
||||
self._plan_apc_atgm_action(stance, dcs_group, forward_heading, to_cp)
|
||||
|
||||
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.OffRoad)
|
||||
dcs_group.add_waypoint(reposition_point, PointAction.OffRoad)
|
||||
|
||||
def add_morale_trigger(self, dcs_group: VehicleGroup, forward_heading: int) -> None:
|
||||
"""
|
||||
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(to_index=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: VehicleGroup,
|
||||
frontline_heading: int,
|
||||
distance: int = RETREAT_DISTANCE
|
||||
) -> Point:
|
||||
"""
|
||||
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
|
||||
"""
|
||||
desired_point = dcs_group.points[0].position.point_from_heading(heading_sum(frontline_heading, +180), distance)
|
||||
if self.conflict.theater.is_on_land(desired_point):
|
||||
return desired_point
|
||||
return self.conflict.theater.nearest_land_pos(desired_point)
|
||||
|
||||
|
||||
def find_offensive_point(
|
||||
self,
|
||||
dcs_group: VehicleGroup,
|
||||
frontline_heading: int,
|
||||
distance: int
|
||||
) -> Point:
|
||||
"""
|
||||
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
|
||||
"""
|
||||
desired_point = dcs_group.points[0].position.point_from_heading(frontline_heading, distance)
|
||||
if self.conflict.theater.is_on_land(desired_point):
|
||||
return desired_point
|
||||
return self.conflict.theater.nearest_land_pos(desired_point)
|
||||
|
||||
@staticmethod
|
||||
def find_n_nearest_enemy_groups(
|
||||
player_group: VehicleGroup,
|
||||
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
||||
n: int
|
||||
) -> List[VehicleGroup]:
|
||||
"""
|
||||
Return the nearest 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 = [] # type: List[Optional[VehicleGroup]]
|
||||
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):
|
||||
# TODO: Is this supposed to return no groups if enemy_groups is less than n?
|
||||
if len(sorted_list) <= i:
|
||||
break
|
||||
else:
|
||||
targets.append(sorted_list[i][0])
|
||||
return targets
|
||||
|
||||
@staticmethod
|
||||
def find_nearest_enemy_group(
|
||||
player_group: VehicleGroup,
|
||||
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
|
||||
) -> Optional[VehicleGroup]:
|
||||
"""
|
||||
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, _ 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
|
||||
|
||||
@staticmethod
|
||||
def get_artillery_target_in_range(
|
||||
dcs_group: VehicleGroup,
|
||||
group: CombatGroup,
|
||||
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
|
||||
) -> Optional[Point]:
|
||||
"""
|
||||
Search the enemy groups for a potential target suitable to an artillery unit
|
||||
"""
|
||||
# TODO: Update to return a list of groups instead of a single point
|
||||
rng = group.units[0].threat_range
|
||||
if not enemy_groups:
|
||||
return None
|
||||
for _ 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
|
||||
|
||||
@staticmethod
|
||||
def get_artilery_group_distance_from_frontline(group: CombatGroup) -> int:
|
||||
"""
|
||||
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][1]:
|
||||
rg = random.randint(
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]
|
||||
)
|
||||
elif rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
||||
rg = random.randint(
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][0],
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][1]
|
||||
)
|
||||
return rg
|
||||
|
||||
def get_valid_position_for_group(
|
||||
self,
|
||||
conflict_position: Point,
|
||||
combat_width: int,
|
||||
distance_from_frontline: int,
|
||||
heading: int,
|
||||
spawn_heading: int
|
||||
):
|
||||
shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width))
|
||||
desired_point = shifted.point_from_heading(
|
||||
spawn_heading,
|
||||
distance_from_frontline
|
||||
)
|
||||
return Conflict.find_ground_position(desired_point, combat_width, heading, self.conflict.theater)
|
||||
|
||||
|
||||
def _generate_groups(
|
||||
self,
|
||||
groups: List[CombatGroup],
|
||||
frontline_vector: Tuple[Point, int, int],
|
||||
is_player: bool
|
||||
) -> List[Tuple[VehicleGroup, CombatGroup]]:
|
||||
"""Finds valid positions for planned groups and generates a pydcs group for them"""
|
||||
positioned_groups = []
|
||||
position, heading, combat_width = frontline_vector
|
||||
spawn_heading = int(heading_sum(heading, -90)) if is_player else int(heading_sum(heading, 90))
|
||||
country = self.game.player_country if is_player else self.game.enemy_country
|
||||
for group in groups:
|
||||
if group.role == CombatGroupRole.ARTILLERY:
|
||||
distance_from_frontline = self.get_artilery_group_distance_from_frontline(group)
|
||||
else:
|
||||
distance_from_frontline = random.randint(
|
||||
DISTANCE_FROM_FRONTLINE[group.role][0],
|
||||
DISTANCE_FROM_FRONTLINE[group.role][1]
|
||||
)
|
||||
|
||||
return point.random_point_within(distance, self.conflict.size * SPREAD_DISTANCE_SIZE_FACTOR)
|
||||
final_position = self.get_valid_position_for_group(
|
||||
position,
|
||||
combat_width,
|
||||
distance_from_frontline,
|
||||
heading,
|
||||
spawn_heading
|
||||
)
|
||||
|
||||
if final_position is not None:
|
||||
g = self._generate_group(
|
||||
self.mission.country(country),
|
||||
group.units[0],
|
||||
len(group.units),
|
||||
final_position,
|
||||
distance_from_frontline,
|
||||
heading=opposite_heading(spawn_heading),
|
||||
)
|
||||
if is_player:
|
||||
g.set_skill(self.game.settings.player_skill)
|
||||
else:
|
||||
g.set_skill(self.game.settings.enemy_vehicle_skill)
|
||||
positioned_groups.append((g, group))
|
||||
self.gen_infantry_group_for_group(
|
||||
g,
|
||||
is_player,
|
||||
self.mission.country(country),
|
||||
opposite_heading(spawn_heading)
|
||||
)
|
||||
else:
|
||||
logging.warning(f"Unable to get valid position for {group}")
|
||||
|
||||
return positioned_groups
|
||||
|
||||
def _generate_group(
|
||||
self,
|
||||
side: Country,
|
||||
unit: VehicleType,
|
||||
count: int,
|
||||
at: Point,
|
||||
distance_from_frontline,
|
||||
move_formation: PointAction = PointAction.OffRoad,
|
||||
heading=0,
|
||||
) -> VehicleGroup:
|
||||
|
||||
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=at,
|
||||
group_size=count,
|
||||
heading=heading,
|
||||
move_formation=move_formation)
|
||||
|
||||
self.unit_map.add_front_line_units(group, cp)
|
||||
|
||||
def _generate_group(self, side: Country, unit: VehicleType, count: int, at: Point):
|
||||
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
|
||||
|
||||
203
gen/ato.py
Normal file
203
gen/ato.py
Normal file
@@ -0,0 +1,203 @@
|
||||
"""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 game.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.OCA_AIRCRAFT,
|
||||
FlightType.OCA_RUNWAY,
|
||||
FlightType.BAI,
|
||||
FlightType.DEAD,
|
||||
FlightType.SEAD,
|
||||
FlightType.TARCAP,
|
||||
FlightType.BARCAP,
|
||||
FlightType.SWEEP,
|
||||
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"
|
||||
oca_strike_types = {FlightType.OCA_AIRCRAFT, FlightType.OCA_RUNWAY}
|
||||
if task in oca_strike_types:
|
||||
return "OCA Strike"
|
||||
return str(task)
|
||||
|
||||
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)
|
||||
176
gen/briefinggen.py
Normal file
176
gen/briefinggen.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
Briefing generation logic
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Dict, List, TYPE_CHECKING
|
||||
|
||||
from dcs.mission import Mission
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
from game.theater import ControlPoint, FrontLine
|
||||
from .aircraft import FlightData
|
||||
from .airsupportgen import AwacsInfo, TankerInfo
|
||||
from .armor import JtacInfo
|
||||
from .flights.flight import FlightWaypoint
|
||||
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
|
||||
|
||||
|
||||
def format_waypoint_time(waypoint: FlightWaypoint, depart_prefix: str) -> str:
|
||||
if waypoint.tot is not None:
|
||||
time = timedelta(seconds=int(waypoint.tot.total_seconds()))
|
||||
return f"T+{time} "
|
||||
elif waypoint.departure_time is not None:
|
||||
time = timedelta(seconds=int(waypoint.departure_time.total_seconds()))
|
||||
return f"{depart_prefix} T+{time} "
|
||||
return ""
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
env.filters["waypoint_timing"] = format_waypoint_time
|
||||
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,264 +1,113 @@
|
||||
import typing
|
||||
import pdb
|
||||
import dcs
|
||||
import logging
|
||||
import random
|
||||
from typing import Tuple, Optional
|
||||
|
||||
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 *
|
||||
|
||||
AIR_DISTANCE = 40000
|
||||
from game.theater.conflicttheater import ConflictTheater, FrontLine
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
from game.utils import heading_sum, opposite_heading
|
||||
|
||||
|
||||
GROUND_DISTANCE_FACTOR = 1
|
||||
GROUNDINTERCEPT_DISTANCE_FACTOR = 6
|
||||
AIR_DISTANCE = 32000
|
||||
|
||||
INTERCEPT_ATTACKERS_HEADING = -45, 45
|
||||
INTERCEPT_DEFENDERS_HEADING = -10, 10
|
||||
INTERCEPT_CONFLICT_DISTANCE = 50000
|
||||
INTERCEPT_ATTACKERS_DISTANCE = 100000
|
||||
INTERCEPT_MAX_DISTANCE = 160000
|
||||
INTERCEPT_MIN_DISTANCE = 100000
|
||||
|
||||
NAVAL_INTERCEPT_DISTANCE_FACTOR = 0.4
|
||||
NAVAL_INTERCEPT_DISTANCE_MAX = 40000
|
||||
NAVAL_INTERCEPT_STEP = 5000
|
||||
|
||||
|
||||
def _opposite_heading(h):
|
||||
return h+180
|
||||
|
||||
|
||||
def _heading_sum(h, a) -> int:
|
||||
h += a
|
||||
if h > 360:
|
||||
return h - 360
|
||||
elif h < 0:
|
||||
return 360 + h
|
||||
else:
|
||||
return h
|
||||
|
||||
FRONTLINE_LENGTH = 80000
|
||||
|
||||
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: Optional[int] = None,
|
||||
size: Optional[int] = 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.size = to_cp.size
|
||||
self.radials = to_cp.radials
|
||||
self.ground_attackers_location = ground_attackers_location
|
||||
self.ground_defenders_location = ground_defenders_location
|
||||
self.air_attackers_location = air_attackers_location
|
||||
self.air_defenders_location = air_defenders_location
|
||||
self.heading = heading
|
||||
self.size = size
|
||||
|
||||
@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, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int]:
|
||||
frontline = FrontLine(from_cp, to_cp, theater)
|
||||
attack_heading = frontline.attack_heading
|
||||
position = cls.find_ground_position(frontline.position, FRONTLINE_LENGTH, heading_sum(attack_heading, 90), theater)
|
||||
return position, opposite_heading(attack_heading)
|
||||
|
||||
initial = initial.point_from_heading(heading, 800)
|
||||
@classmethod
|
||||
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:
|
||||
"""
|
||||
Returns a vector for a valid frontline location avoiding exclusion zones.
|
||||
"""
|
||||
center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
|
||||
left_heading = heading_sum(heading, -90)
|
||||
right_heading = heading_sum(heading, 90)
|
||||
left_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater)
|
||||
right_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater)
|
||||
distance = int(left_position.distance_to_point(right_position))
|
||||
return left_position, right_heading, distance
|
||||
|
||||
print("Didn't find ground position!")
|
||||
@classmethod
|
||||
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)
|
||||
conflict = cls(
|
||||
position=position,
|
||||
heading=heading,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
size=distance
|
||||
)
|
||||
return conflict
|
||||
|
||||
@classmethod
|
||||
def extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
|
||||
"""Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance"""
|
||||
pos = initial
|
||||
for distance in range(0, int(max_distance), 100):
|
||||
pos = initial.point_from_heading(heading, distance)
|
||||
if not theater.is_on_land(pos):
|
||||
return initial.point_from_heading(heading, distance - 100)
|
||||
return pos
|
||||
|
||||
@classmethod
|
||||
def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater, coerce=True) -> Optional[Point]:
|
||||
"""
|
||||
Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance.
|
||||
`coerce=True` will return the closest land position to `initial` regardless of heading or distance
|
||||
`coerce=False` will return None if a point isn't found
|
||||
"""
|
||||
pos = initial
|
||||
if theater.is_on_land(pos):
|
||||
return pos
|
||||
for distance in range(0, int(max_distance), 100):
|
||||
pos = initial.point_from_heading(heading, distance)
|
||||
if theater.is_on_land(pos):
|
||||
return pos
|
||||
pos = initial.point_from_heading(opposite_heading(heading), distance)
|
||||
if coerce:
|
||||
pos = theater.nearest_land_pos(initial)
|
||||
return pos
|
||||
logging.error("Didn't find ground position ({})!".format(initial))
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def capture_conflict(cls, 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_location(attackers_location, distance * 2, _heading_sum(attack_heading, 180), 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)
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker,
|
||||
defenders_side=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),
|
||||
air_defenders_location=position.point_from_heading(_opposite_heading(attack_raw_heading), CAPTURE_AIR_DEFENDERS_DISTANCE)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def intercept_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
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 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,
|
||||
ground_attackers_location=None,
|
||||
ground_defenders_location=None,
|
||||
air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, INTERCEPT_ATTACKERS_DISTANCE),
|
||||
air_defenders_location=position
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def ground_attack_conflict(cls, 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)
|
||||
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)
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker,
|
||||
defenders_side=defender,
|
||||
ground_attackers_location=position,
|
||||
ground_defenders_location=None,
|
||||
air_attackers_location=None,
|
||||
air_defenders_location=position.point_from_heading(heading, AIR_DISTANCE),
|
||||
)
|
||||
|
||||
@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)
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker,
|
||||
defenders_side=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)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def ground_base_attack(cls, 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)
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker,
|
||||
defenders_side=defender,
|
||||
ground_attackers_location=None,
|
||||
ground_defenders_location=defenders_location,
|
||||
air_attackers_location=position.point_from_heading(attack_heading, AIR_DISTANCE),
|
||||
air_defenders_location=position
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def naval_intercept_conflict(cls, attacker: Country, defender: Country, 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)
|
||||
for offset in range(0, initial_distance, NAVAL_INTERCEPT_STEP):
|
||||
position = to_cp.position.point_from_heading(_opposite_heading(radial), initial_distance - offset)
|
||||
|
||||
if not theater.is_on_land(position):
|
||||
break
|
||||
|
||||
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,
|
||||
ground_attackers_location=None,
|
||||
ground_defenders_location=position,
|
||||
air_attackers_location=position.point_from_heading(attacker_heading, AIR_DISTANCE),
|
||||
air_defenders_location=position.point_from_heading(_opposite_heading(attacker_heading), AIR_DISTANCE)
|
||||
)
|
||||
|
||||
@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)
|
||||
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)
|
||||
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)
|
||||
|
||||
return cls(
|
||||
position=dest,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker,
|
||||
defenders_side=defender,
|
||||
ground_attackers_location=from_cp.position,
|
||||
ground_defenders_location=frontline_position,
|
||||
air_attackers_location=from_cp.position.point_from_heading(0, 100),
|
||||
air_defenders_location=frontline_position
|
||||
)
|
||||
|
||||
|
||||
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 game.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, "DD1", self.position.x + 2400, self.position.y + 900, self.heading)
|
||||
self.add_unit(dd_type, "DD2", 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 game.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 game.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, "DD1", self.position.x + 2400, self.position.y + 900, self.heading)
|
||||
self.add_unit(dd_type, "DD2", 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
|
||||
648
gen/flights/ai_flight_planner.py
Normal file
648
gen/flights/ai_flight_planner.py
Normal file
@@ -0,0 +1,648 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import operator
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import (
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Type,
|
||||
)
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from game.data.radar_db import UNITS_WITH_RADAR
|
||||
from game.infos.information import Information
|
||||
from game.procurement import AircraftProcurementRequest
|
||||
from game.theater import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
FrontLine,
|
||||
MissionTarget,
|
||||
OffMapSpawn,
|
||||
SamGroundObject,
|
||||
TheaterGroundObject,
|
||||
)
|
||||
# Avoid importing some types that cause circular imports unless type checking.
|
||||
from game.theater.theatergroundobject import (
|
||||
EwrGroundObject,
|
||||
NavalGroundObject, VehicleGroupGroundObject,
|
||||
)
|
||||
from game.utils import nm_to_meter
|
||||
from gen import Conflict
|
||||
from gen.ato import Package
|
||||
from gen.flights.ai_flight_planner_db import (
|
||||
ANTISHIP_CAPABLE,
|
||||
ANTISHIP_PREFERRED,
|
||||
CAP_CAPABLE,
|
||||
CAP_PREFERRED,
|
||||
CAS_CAPABLE,
|
||||
CAS_PREFERRED,
|
||||
RUNWAY_ATTACK_CAPABLE,
|
||||
RUNWAY_ATTACK_PREFERRED,
|
||||
SEAD_CAPABLE,
|
||||
SEAD_PREFERRED,
|
||||
STRIKE_CAPABLE,
|
||||
STRIKE_PREFERRED, capable_aircraft_for_task, preferred_aircraft_for_task,
|
||||
)
|
||||
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
|
||||
|
||||
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} {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, FlyingType]]:
|
||||
"""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, preferred_aircraft_for_task(flight.task)
|
||||
)
|
||||
if result is not None:
|
||||
return result
|
||||
return self.find_aircraft_of_type(
|
||||
flight, capable_aircraft_for_task(flight.task)
|
||||
)
|
||||
|
||||
def find_aircraft_of_type(
|
||||
self, flight: ProposedFlight, types: List[Type[FlyingType]],
|
||||
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
|
||||
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 not airfield.can_operate(aircraft):
|
||||
continue
|
||||
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.closest_airfields = closest_airfields
|
||||
self.is_player = is_player
|
||||
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
|
||||
if isinstance(airfield, OffMapSpawn):
|
||||
start_type = "In Flight"
|
||||
else:
|
||||
start_type = self.start_type
|
||||
|
||||
flight = Flight(self.package, aircraft, plan.num_aircraft, plan.task,
|
||||
start_type, departure=airfield, arrival=airfield,
|
||||
divert=self.find_divert_field(aircraft, airfield))
|
||||
self.package.add_flight(flight)
|
||||
return True
|
||||
|
||||
def find_divert_field(self, aircraft: FlyingType,
|
||||
arrival: ControlPoint) -> Optional[ControlPoint]:
|
||||
divert_limit = nm_to_meter(150)
|
||||
for airfield in self.closest_airfields.airfields_within(divert_limit):
|
||||
if airfield.captured != self.is_player:
|
||||
continue
|
||||
if airfield == arrival:
|
||||
continue
|
||||
if not airfield.can_operate(aircraft):
|
||||
continue
|
||||
if isinstance(airfield, OffMapSpawn):
|
||||
continue
|
||||
return airfield
|
||||
return None
|
||||
|
||||
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:
|
||||
is_ewr = isinstance(ground_object, EwrGroundObject)
|
||||
is_sam = isinstance(ground_object, SamGroundObject)
|
||||
if not is_ewr and not is_sam:
|
||||
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[MissionTarget]:
|
||||
"""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).
|
||||
"""
|
||||
return self._targets_by_range(self.enemy_sams())
|
||||
|
||||
def enemy_vehicle_groups(self) -> Iterator[VehicleGroupGroundObject]:
|
||||
"""Iterates over all enemy vehicle groups."""
|
||||
for cp in self.enemy_control_points():
|
||||
for ground_object in cp.ground_objects:
|
||||
if not isinstance(ground_object, VehicleGroupGroundObject):
|
||||
continue
|
||||
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
|
||||
yield ground_object
|
||||
|
||||
def threatening_vehicle_groups(self) -> Iterator[MissionTarget]:
|
||||
"""Iterates over enemy vehicle groups near friendly control points.
|
||||
|
||||
Groups are sorted by their closest proximity to any friendly control
|
||||
point (airfield or fleet).
|
||||
"""
|
||||
return self._targets_by_range(self.enemy_vehicle_groups())
|
||||
|
||||
def enemy_ships(self) -> Iterator[NavalGroundObject]:
|
||||
for cp in self.enemy_control_points():
|
||||
for ground_object in cp.ground_objects:
|
||||
if not isinstance(ground_object, NavalGroundObject):
|
||||
continue
|
||||
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
|
||||
yield ground_object
|
||||
|
||||
def threatening_ships(self) -> Iterator[MissionTarget]:
|
||||
"""Iterates over enemy ships near friendly control points.
|
||||
|
||||
Groups are sorted by their closest proximity to any friendly control
|
||||
point (airfield or fleet).
|
||||
"""
|
||||
return self._targets_by_range(self.enemy_ships())
|
||||
|
||||
def _targets_by_range(
|
||||
self,
|
||||
targets: Iterable[MissionTarget]) -> Iterator[MissionTarget]:
|
||||
target_ranges: List[Tuple[MissionTarget, int]] = []
|
||||
for target in targets:
|
||||
ranges: List[int] = []
|
||||
for cp in self.friendly_control_points():
|
||||
ranges.append(target.distance_to(cp))
|
||||
target_ranges.append((target, min(ranges)))
|
||||
|
||||
target_ranges = sorted(target_ranges, key=operator.itemgetter(1))
|
||||
for target, _range in target_ranges:
|
||||
yield target
|
||||
|
||||
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]] = []
|
||||
# Building objectives are made of several individual TGOs (one per
|
||||
# building).
|
||||
found_targets: Set[str] = set()
|
||||
for enemy_cp in self.enemy_control_points():
|
||||
for ground_object in enemy_cp.ground_objects:
|
||||
if isinstance(ground_object, VehicleGroupGroundObject):
|
||||
# BAI target, not strike target.
|
||||
continue
|
||||
if isinstance(ground_object, NavalGroundObject):
|
||||
# Anti-ship target, not strike target.
|
||||
continue
|
||||
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, self.game.theater)
|
||||
|
||||
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():
|
||||
if isinstance(cp, OffMapSpawn):
|
||||
# Off-map spawn locations don't need protection.
|
||||
continue
|
||||
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 oca_targets(self, min_aircraft: int) -> Iterator[MissionTarget]:
|
||||
airfields = []
|
||||
for control_point in self.enemy_control_points():
|
||||
if not isinstance(control_point, Airfield):
|
||||
continue
|
||||
if control_point.base.total_aircraft >= min_aircraft:
|
||||
airfields.append(control_point)
|
||||
return self._targets_by_range(airfields)
|
||||
|
||||
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_ANTISHIP_RANGE = nm_to_meter(150)
|
||||
MAX_BAI_RANGE = nm_to_meter(150)
|
||||
MAX_OCA_RANGE = nm_to_meter(150)
|
||||
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
|
||||
self.procurement_requests: List[AircraftProcurementRequest] = []
|
||||
|
||||
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 CAS.
|
||||
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),
|
||||
])
|
||||
|
||||
for group in self.objective_finder.threatening_ships():
|
||||
yield ProposedMission(group, [
|
||||
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_ANTISHIP_RANGE),
|
||||
])
|
||||
|
||||
for group in self.objective_finder.threatening_vehicle_groups():
|
||||
yield ProposedMission(group, [
|
||||
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE),
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_BAI_RANGE),
|
||||
])
|
||||
|
||||
for target in self.objective_finder.oca_targets(min_aircraft=20):
|
||||
yield ProposedMission(target, [
|
||||
ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE),
|
||||
ProposedFlight(FlightType.OCA_RUNWAY, 2, self.MAX_OCA_RANGE),
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_OCA_RANGE),
|
||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_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)
|
||||
self.procurement_requests.append(AircraftProcurementRequest(
|
||||
near=mission.location,
|
||||
range=proposed_flight.max_distance,
|
||||
task_capability=proposed_flight.task,
|
||||
number=proposed_flight.num_aircraft
|
||||
))
|
||||
|
||||
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,
|
||||
FlightType.TARCAP,
|
||||
}
|
||||
|
||||
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}")
|
||||
586
gen/flights/ai_flight_planner_db.py
Normal file
586
gen/flights/ai_flight_planner_db.py
Normal file
@@ -0,0 +1,586 @@
|
||||
import logging
|
||||
from typing import List, Type
|
||||
|
||||
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,
|
||||
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,
|
||||
I_16
|
||||
)
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
from pydcs_extensions.f22a.f22a import F_22A
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
|
||||
# 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.
|
||||
|
||||
# Interceptor are the aircraft prioritized for interception tasks
|
||||
# If none is available, the AI will use regular CAP-capable aircraft instead
|
||||
INTERCEPT_CAPABLE = [
|
||||
MiG_21Bis,
|
||||
MiG_25PD,
|
||||
MiG_31,
|
||||
MiG_29S,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29K,
|
||||
JF_17,
|
||||
J_11A,
|
||||
Su_27,
|
||||
Su_30,
|
||||
Su_33,
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
Rafale_M,
|
||||
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
|
||||
]
|
||||
|
||||
# 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,
|
||||
F_22A,
|
||||
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
|
||||
I_16,
|
||||
|
||||
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_29A,
|
||||
MiG_29G,
|
||||
MiG_29S,
|
||||
|
||||
Su_27,
|
||||
J_11A,
|
||||
JF_17,
|
||||
Su_30,
|
||||
Su_33,
|
||||
Su_57,
|
||||
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_16C_50,
|
||||
F_22A,
|
||||
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
I_16,
|
||||
|
||||
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_30,
|
||||
Su_34,
|
||||
|
||||
JF_17,
|
||||
|
||||
M_2000C,
|
||||
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_5E_3,
|
||||
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
F_15E,
|
||||
F_22A,
|
||||
|
||||
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,
|
||||
|
||||
I_16,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
|
||||
WingLoong_I,
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator
|
||||
]
|
||||
|
||||
CAS_PREFERRED = [
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_24MR,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_30,
|
||||
Su_34,
|
||||
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
|
||||
Tornado_GR4,
|
||||
|
||||
C_101CC,
|
||||
MB_339PAN,
|
||||
L_39ZA,
|
||||
AJS37,
|
||||
|
||||
SA342M,
|
||||
SA342L,
|
||||
OH_58D,
|
||||
|
||||
AH_64A,
|
||||
AH_64D,
|
||||
AH_1W,
|
||||
|
||||
Mi_28N,
|
||||
Mi_24V,
|
||||
Ka_50,
|
||||
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
A_20G,
|
||||
I_16,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
|
||||
WingLoong_I,
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator
|
||||
]
|
||||
|
||||
# Aircraft used for SEAD / DEAD tasks
|
||||
SEAD_CAPABLE = [
|
||||
F_4E,
|
||||
FA_18C_hornet,
|
||||
|
||||
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,
|
||||
Rafale_B
|
||||
]
|
||||
|
||||
SEAD_PREFERRED = [
|
||||
F_4E,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Tornado_IDS,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
Su_30,
|
||||
Su_34,
|
||||
Su_24M,
|
||||
]
|
||||
|
||||
# Aircraft used for Strike mission
|
||||
STRIKE_CAPABLE = [
|
||||
MiG_15bis,
|
||||
MiG_21Bis,
|
||||
MiG_27K,
|
||||
MB_339PAN,
|
||||
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_24MR,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_27,
|
||||
Su_33,
|
||||
Su_30,
|
||||
Su_34,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29K,
|
||||
MiG_29S,
|
||||
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
|
||||
JF_17,
|
||||
|
||||
M_2000C,
|
||||
|
||||
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,
|
||||
Rafale_B
|
||||
|
||||
]
|
||||
|
||||
STRIKE_PREFERRED = [
|
||||
AJS37,
|
||||
A_20G,
|
||||
B_17G,
|
||||
B_1B,
|
||||
B_52H,
|
||||
F_117A,
|
||||
F_15E,
|
||||
Su_24M,
|
||||
Su_30,
|
||||
Su_34,
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
]
|
||||
|
||||
ANTISHIP_CAPABLE = [
|
||||
AJS37,
|
||||
C_101CC,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
FA_18C_hornet,
|
||||
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
|
||||
Su_30,
|
||||
Su_34,
|
||||
Tu_22M3,
|
||||
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
|
||||
Ju_88A4,
|
||||
Rafale_A_S,
|
||||
Rafale_B
|
||||
]
|
||||
|
||||
ANTISHIP_PREFERRED = [
|
||||
AJS37,
|
||||
C_101CC,
|
||||
FA_18C_hornet,
|
||||
JF_17,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
Su_24M,
|
||||
Su_30,
|
||||
Su_34,
|
||||
Tu_22M3,
|
||||
Ju_88A4
|
||||
]
|
||||
|
||||
RUNWAY_ATTACK_PREFERRED = [
|
||||
JF_17,
|
||||
Su_30,
|
||||
Su_34,
|
||||
Tornado_IDS,
|
||||
]
|
||||
|
||||
RUNWAY_ATTACK_CAPABLE = STRIKE_CAPABLE
|
||||
|
||||
DRONES = [
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator,
|
||||
WingLoong_I
|
||||
]
|
||||
|
||||
|
||||
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.ANTISHIP:
|
||||
return ANTISHIP_PREFERRED
|
||||
elif task == FlightType.BAI:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.CAS:
|
||||
return CAS_PREFERRED
|
||||
elif task in (FlightType.DEAD, FlightType.SEAD):
|
||||
return SEAD_PREFERRED
|
||||
elif task == FlightType.OCA_AIRCRAFT:
|
||||
return CAS_PREFERRED
|
||||
elif task == FlightType.OCA_RUNWAY:
|
||||
return RUNWAY_ATTACK_PREFERRED
|
||||
elif task == FlightType.STRIKE:
|
||||
return STRIKE_PREFERRED
|
||||
elif task == FlightType.ESCORT:
|
||||
return CAP_PREFERRED
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
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.ANTISHIP:
|
||||
return ANTISHIP_CAPABLE
|
||||
elif task == FlightType.BAI:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.CAS:
|
||||
return CAS_CAPABLE
|
||||
elif task in (FlightType.DEAD, FlightType.SEAD):
|
||||
return SEAD_CAPABLE
|
||||
elif task == FlightType.OCA_AIRCRAFT:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.OCA_RUNWAY:
|
||||
return RUNWAY_ATTACK_CAPABLE
|
||||
elif task == FlightType.STRIKE:
|
||||
return STRIKE_CAPABLE
|
||||
elif task == FlightType.ESCORT:
|
||||
return CAP_CAPABLE
|
||||
else:
|
||||
logging.error(f"Unplannable flight type: {task}")
|
||||
return []
|
||||
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 game.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]
|
||||
171
gen/flights/flight.py
Normal file
171
gen/flights/flight.py
Normal file
@@ -0,0 +1,171 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.ato import Package
|
||||
from gen.flights.flightplan import FlightPlan
|
||||
|
||||
|
||||
class FlightType(Enum):
|
||||
TARCAP = "TARCAP"
|
||||
BARCAP = "BARCAP"
|
||||
CAS = "CAS"
|
||||
INTERCEPTION = "Intercept"
|
||||
STRIKE = "Strike"
|
||||
ANTISHIP = "Anti-ship"
|
||||
SEAD = "SEAD"
|
||||
DEAD = "DEAD"
|
||||
ESCORT = "Escort"
|
||||
BAI = "BAI"
|
||||
SWEEP = "Fighter sweep"
|
||||
OCA_RUNWAY = "OCA/Runway"
|
||||
OCA_AIRCRAFT = "OCA/Aircraft"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
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
|
||||
INGRESS_SWEEP = 21
|
||||
INGRESS_BAI = 22
|
||||
DIVERT = 23
|
||||
INGRESS_OCA_RUNWAY = 24
|
||||
INGRESS_OCA_AIRCRAFT = 25
|
||||
|
||||
|
||||
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
|
||||
self.flyover = 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: Type[FlyingType],
|
||||
count: int, flight_type: FlightType, start_type: str,
|
||||
departure: ControlPoint, arrival: ControlPoint,
|
||||
divert: Optional[ControlPoint]) -> None:
|
||||
self.package = package
|
||||
self.unit_type = unit_type
|
||||
self.count = count
|
||||
self.departure = departure
|
||||
self.arrival = arrival
|
||||
self.divert = divert
|
||||
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 from_cp(self) -> ControlPoint:
|
||||
return self.departure
|
||||
|
||||
@property
|
||||
def points(self) -> List[FlightWaypoint]:
|
||||
return self.flight_plan.waypoints[1:]
|
||||
|
||||
def __repr__(self):
|
||||
name = db.unit_type_name(self.unit_type)
|
||||
return f"[{self.flight_type}] {self.count} x {name}"
|
||||
1286
gen/flights/flightplan.py
Normal file
1286
gen/flights/flightplan.py
Normal file
File diff suppressed because it is too large
Load Diff
225
gen/flights/traveltime.py
Normal file
225
gen/flights/traveltime.py
Normal file
@@ -0,0 +1,225 @@
|
||||
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_m: 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_m: The altitude in meters.
|
||||
|
||||
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
|
||||
altitude_ft = altitude_m * 3.28084
|
||||
if altitude_ft <= 36152:
|
||||
temperature_f = 59 - 0.00356 * altitude_ft
|
||||
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)
|
||||
|
||||
|
||||
# TODO: Most if not all of this should move into FlightPlan.
|
||||
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_waypoint = flight.flight_plan.tot_waypoint
|
||||
if tot_waypoint is None:
|
||||
tot = self.package.time_over_target
|
||||
else:
|
||||
tot = flight.flight_plan.tot_for_waypoint(tot_waypoint)
|
||||
if tot is None:
|
||||
logging.error(f"TOT waypoint for {flight} has no TOT")
|
||||
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()
|
||||
# Account for TOT offsets for the flight plan. An offset of -2 minutes
|
||||
# means the flight's TOT is 2 minutes ahead of the package's so it needs
|
||||
# an extra two minutes.
|
||||
offset = -flight.flight_plan.tot_offset
|
||||
startup = self.estimate_startup(flight)
|
||||
ground_ops = self.estimate_ground_ops(flight)
|
||||
return startup + ground_ops + time_to_target + offset
|
||||
|
||||
@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
|
||||
414
gen/flights/waypointbuilder.py
Normal file
414
gen/flights/waypointbuilder.py
Normal file
@@ -0,0 +1,414 @@
|
||||
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 dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
MissionTarget,
|
||||
OffMapSpawn,
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.weather import Conditions
|
||||
from .flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StrikeTarget:
|
||||
name: str
|
||||
target: Union[VehicleGroup, 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)
|
||||
|
||||
def takeoff(self, 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
|
||||
if isinstance(departure, OffMapSpawn):
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.NAV,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
)
|
||||
waypoint.name = "NAV"
|
||||
waypoint.alt_type = "BARO"
|
||||
waypoint.description = "Enter theater"
|
||||
waypoint.pretty_name = "Enter theater"
|
||||
else:
|
||||
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
|
||||
|
||||
def land(self, arrival: ControlPoint) -> FlightWaypoint:
|
||||
"""Create descent waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Args:
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
position = arrival.position
|
||||
if isinstance(arrival, OffMapSpawn):
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.NAV,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
)
|
||||
waypoint.name = "NAV"
|
||||
waypoint.alt_type = "BARO"
|
||||
waypoint.description = "Exit theater"
|
||||
waypoint.pretty_name = "Exit theater"
|
||||
else:
|
||||
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 divert(self,
|
||||
divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
|
||||
"""Create divert waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Args:
|
||||
divert: Divert airfield or carrier.
|
||||
"""
|
||||
if divert is None:
|
||||
return None
|
||||
|
||||
position = divert.position
|
||||
if isinstance(divert, OffMapSpawn):
|
||||
if self.is_helo:
|
||||
altitude = 500
|
||||
else:
|
||||
altitude = self.doctrine.rendezvous_altitude
|
||||
altitude_type = "BARO"
|
||||
else:
|
||||
altitude = 0
|
||||
altitude_type = "RADIO"
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.DIVERT,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
)
|
||||
waypoint.alt_type = altitude_type
|
||||
waypoint.name = "DIVERT"
|
||||
waypoint.description = "Divert"
|
||||
waypoint.pretty_name = "Divert"
|
||||
waypoint.only_for_player = True
|
||||
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(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 bai_group(self, target: StrikeTarget) -> FlightWaypoint:
|
||||
return self._target_point(target, f"ATTACK {target.name}")
|
||||
|
||||
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, flyover=True)
|
||||
|
||||
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"DEAD on {target.name}", target)
|
||||
|
||||
def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
|
||||
|
||||
@staticmethod
|
||||
def _target_area(name: str, location: MissionTarget,
|
||||
flyover: bool = False) -> 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"
|
||||
|
||||
# Most 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.
|
||||
#
|
||||
# The exception is for flight plans that require passing over the
|
||||
# target. For example, OCA strikes need to get close enough to detect
|
||||
# the targets in their engagement zone or they will RTB immediately.
|
||||
if flyover:
|
||||
waypoint.flyover = True
|
||||
else:
|
||||
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))
|
||||
|
||||
@staticmethod
|
||||
def sweep_start(position: Point, altitude: int) -> FlightWaypoint:
|
||||
"""Creates a sweep start waypoint.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the sweep in meters.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.INGRESS_SWEEP,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
)
|
||||
waypoint.name = "SWEEP START"
|
||||
waypoint.description = "Proceed to the target and engage enemy aircraft"
|
||||
waypoint.pretty_name = "Sweep start"
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def sweep_end(position: Point, altitude: int) -> FlightWaypoint:
|
||||
"""Creates a sweep end waypoint.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the sweep in meters.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.EGRESS,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
)
|
||||
waypoint.name = "SWEEP END"
|
||||
waypoint.description = "End of sweep"
|
||||
waypoint.pretty_name = "Sweep end"
|
||||
return waypoint
|
||||
|
||||
def sweep(self, start: Point, end: Point,
|
||||
altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates two waypoint for a racetrack orbit.
|
||||
|
||||
Args:
|
||||
start: The beginning of the sweep.
|
||||
end: The end of the sweep.
|
||||
altitude: The sweep altitude.
|
||||
"""
|
||||
return (self.sweep_start(start, altitude),
|
||||
self.sweep_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
|
||||
44
gen/forcedoptionsgen.py
Normal file
44
gen/forcedoptionsgen.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs.forcedoptions import ForcedOptions
|
||||
from dcs.mission import Mission
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
|
||||
class ForcedOptionsGenerator:
|
||||
def __init__(self, mission: Mission, game: Game) -> None:
|
||||
self.mission = mission
|
||||
self.game = game
|
||||
|
||||
def _set_options_view(self) -> None:
|
||||
self.mission.forced_options.options_view = self.game.settings.map_coalition_visibility
|
||||
|
||||
def _set_external_views(self) -> None:
|
||||
if not self.game.settings.external_views_allowed:
|
||||
self.mission.forced_options.external_views = self.game.settings.external_views_allowed
|
||||
|
||||
def _set_labels(self) -> None:
|
||||
# TODO: Fix settings to use the real type.
|
||||
# TODO: Allow forcing "full" and have default do nothing.
|
||||
if self.game.settings.labels == "Abbreviated":
|
||||
self.mission.forced_options.labels = ForcedOptions.Labels.Abbreviate
|
||||
elif self.game.settings.labels == "Dot Only":
|
||||
self.mission.forced_options.labels = ForcedOptions.Labels.DotOnly
|
||||
elif self.game.settings.labels == "Off":
|
||||
self.mission.forced_options.labels = ForcedOptions.Labels.None_
|
||||
|
||||
def _set_unrestricted_satnav(self) -> None:
|
||||
blue = self.game.player_faction
|
||||
red = self.game.enemy_faction
|
||||
if blue.unrestricted_satnav or red.unrestricted_satnav:
|
||||
self.mission.forced_options.unrestricted_satnav = True
|
||||
|
||||
def generate(self):
|
||||
self._set_options_view()
|
||||
self._set_external_views()
|
||||
self._set_labels()
|
||||
self._set_unrestricted_satnav()
|
||||
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.unittype import VehicleType
|
||||
from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
|
||||
|
||||
import pydcs_extensions.frenchpack.frenchpack as frenchpack
|
||||
from game.theater import ControlPoint
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
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: (2200, 3200),
|
||||
CombatGroupRole.APC: (7500, 8500),
|
||||
CombatGroupRole.IFV: (2700, 3700),
|
||||
CombatGroupRole.ARTILLERY: (16000, 18000),
|
||||
CombatGroupRole.SHORAD: (12000, 13000),
|
||||
CombatGroupRole.LOGI: (18000, 20000),
|
||||
CombatGroupRole.INFANTRY: (2800, 3300),
|
||||
CombatGroupRole.ATGM: (5200, 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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user