mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
1437 Commits
2.2.1
...
develop-3.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6866ac245 | ||
|
|
aeb043e863 | ||
|
|
e33a4ec0c3 | ||
|
|
6b310a10de | ||
|
|
9433138c20 | ||
|
|
a3d61ea286 | ||
|
|
3a11068259 | ||
|
|
f5d4c04b75 | ||
|
|
04fa65107e | ||
|
|
abc0c19bd1 | ||
|
|
0d31de344d | ||
|
|
28c0ae7802 | ||
|
|
c0bef399ad | ||
|
|
21681a8240 | ||
|
|
bc0ac0cbfc | ||
|
|
7f508f0b2c | ||
|
|
ef9e957ef2 | ||
|
|
7d08e1ee2c | ||
|
|
97ac6a0612 | ||
|
|
83c311c853 | ||
|
|
df97110546 | ||
|
|
f570cfc12e | ||
|
|
70b4f75f25 | ||
|
|
1ac95438ce | ||
|
|
73ca6606e1 | ||
|
|
7a04cf5905 | ||
|
|
64c426653c | ||
|
|
a8960c9bbe | ||
|
|
72282845e8 | ||
|
|
e64aff4e91 | ||
|
|
e192e54c90 | ||
|
|
39b0599b7b | ||
|
|
45b40e4aa3 | ||
|
|
9887a8ff83 | ||
|
|
8d3556aa4b | ||
|
|
a59c01bcfe | ||
|
|
fb72962f74 | ||
|
|
ed1dacfe7c | ||
|
|
794de0fcbb | ||
|
|
9d71b2e727 | ||
|
|
5b8f626651 | ||
|
|
461f4b82a9 | ||
|
|
15653d0628 | ||
|
|
dffc631b87 | ||
|
|
17efb48b2e | ||
|
|
7e85825d2b | ||
|
|
798591b980 | ||
|
|
4a52af298c | ||
|
|
fe886a754e | ||
|
|
0220fa4ff6 | ||
|
|
e7336d8608 | ||
|
|
d77a174ac1 | ||
|
|
6348317893 | ||
|
|
a516cd2f80 | ||
|
|
e1aa3e9d0e | ||
|
|
d1d1acf6e0 | ||
|
|
3e43414d9c | ||
|
|
6d682d509f | ||
|
|
3a592aee8b | ||
|
|
b74f60fe0e | ||
|
|
34f3a50234 | ||
|
|
6094179a40 | ||
|
|
e3bc2688ba | ||
|
|
96cdea2a94 | ||
|
|
cb159e3341 | ||
|
|
136e776b03 | ||
|
|
a0833e8943 | ||
|
|
8bb1b1da7c | ||
|
|
558502d8ea | ||
|
|
8edb952800 | ||
|
|
f3d79e58db | ||
|
|
f26ff085e1 | ||
|
|
7ea550738e | ||
|
|
6b1048590f | ||
|
|
203f0d3851 | ||
|
|
d9c38a716c | ||
|
|
24709d01bd | ||
|
|
2dc2681f84 | ||
|
|
d53a39860e | ||
|
|
ad2f084112 | ||
|
|
d59c42ed3f | ||
|
|
e022ffee62 | ||
|
|
77ddd5ed78 | ||
|
|
8604faffe6 | ||
|
|
45919200c4 | ||
|
|
d498bb9cff | ||
|
|
389f60786a | ||
|
|
2d0929cd69 | ||
|
|
e94ebd6ed2 | ||
|
|
77373606fe | ||
|
|
284f2bc323 | ||
|
|
355e6e1d15 | ||
|
|
f6909d2f98 | ||
|
|
c42974f7b3 | ||
|
|
230d80a2a5 | ||
|
|
551038b295 | ||
|
|
4055b06e71 | ||
|
|
6616359baf | ||
|
|
a5336bbe56 | ||
|
|
871e7f7a50 | ||
|
|
d1c7146a47 | ||
|
|
30f6220c3e | ||
|
|
acd3e87996 | ||
|
|
8c8814d07e | ||
|
|
417fc3af5b | ||
|
|
2218733da4 | ||
|
|
9d1060975e | ||
|
|
82281e2477 | ||
|
|
d0976c45e9 | ||
|
|
a888397bef | ||
|
|
7b2bb4a128 | ||
|
|
d440dc00f1 | ||
|
|
d61382f4e2 | ||
|
|
d4fe893539 | ||
|
|
1af95955b6 | ||
|
|
a43e926dd2 | ||
|
|
ff49046bfa | ||
|
|
95b0b851a5 | ||
|
|
077ca19912 | ||
|
|
089cc23648 | ||
|
|
e6b9a73d03 | ||
|
|
cea264e871 | ||
|
|
d0bde7b016 | ||
|
|
5b271df66f | ||
|
|
bc7faee880 | ||
|
|
a2abdcf5d3 | ||
|
|
d4e843983d | ||
|
|
6e41c36a44 | ||
|
|
1fe3451120 | ||
|
|
bc4a95d0a5 | ||
|
|
14dc6d1604 | ||
|
|
1795ed7617 | ||
|
|
e8edb31be3 | ||
|
|
58fd30e6ad | ||
|
|
9a34ada258 | ||
|
|
748a752e29 | ||
|
|
37748ef3bd | ||
|
|
d41007de8e | ||
|
|
45befd440c | ||
|
|
a2c10f1c7a | ||
|
|
d7768f86d3 | ||
|
|
dae3835eb0 | ||
|
|
e9b5784d30 | ||
|
|
1521f0a9b1 | ||
|
|
9a9c351f47 | ||
|
|
4ec11ddea5 | ||
|
|
f619b6b9fc | ||
|
|
bcccb3206d | ||
|
|
11a8ff7f70 | ||
|
|
f6ab1aad77 | ||
|
|
5a732acf64 | ||
|
|
e4e06e0a6e | ||
|
|
28f20d47d3 | ||
|
|
82ce688a0d | ||
|
|
f36757b650 | ||
|
|
ac4a7441e9 | ||
|
|
9091afe682 | ||
|
|
e2034b19e7 | ||
|
|
1b7a225f9d | ||
|
|
a52043ef29 | ||
|
|
b38d271f10 | ||
|
|
e480519855 | ||
|
|
8b8d1e87e7 | ||
|
|
cd6de191d1 | ||
|
|
8b8e018521 | ||
|
|
5277beede3 | ||
|
|
57a2457050 | ||
|
|
2f8656d54f | ||
|
|
49102e510d | ||
|
|
e7b8548698 | ||
|
|
9c2bad85d5 | ||
|
|
4147d2f684 | ||
|
|
6b30f47588 | ||
|
|
e49da6afd6 | ||
|
|
6fa0a29249 | ||
|
|
c163e2c981 | ||
|
|
372bf9d97f | ||
|
|
619d5dd1b9 | ||
|
|
4939faf5fa | ||
|
|
205e4aa707 | ||
|
|
81ce7fbb62 | ||
|
|
de9651533f | ||
|
|
e6e31fd234 | ||
|
|
d242079a74 | ||
|
|
48f26cb181 | ||
|
|
c37a5b2405 | ||
|
|
d15bfaac76 | ||
|
|
e94657875f | ||
|
|
f2bd7300aa | ||
|
|
c255aee3b9 | ||
|
|
305d1f0523 | ||
|
|
970f2c25dd | ||
|
|
b7b3b35816 | ||
|
|
e8f326ebce | ||
|
|
62b743025a | ||
|
|
7934463a53 | ||
|
|
d15ef63182 | ||
|
|
c7edba5120 | ||
|
|
188f871bc8 | ||
|
|
31eba975fd | ||
|
|
2ea0bccd25 | ||
|
|
fa321c7ddc | ||
|
|
1d7b0c9b17 | ||
|
|
a4fbcd2d02 | ||
|
|
d788b286aa | ||
|
|
eedb5c26a9 | ||
|
|
ddd6e7d18f | ||
|
|
eae0d6be94 | ||
|
|
5e68dbe1ca | ||
|
|
98e0be6be9 | ||
|
|
7450a6b7eb | ||
|
|
c3802e5a37 | ||
|
|
43cd9bce67 | ||
|
|
2f6ab6d2b0 | ||
|
|
2df17c32cd | ||
|
|
16fff8d87a | ||
|
|
1087069277 | ||
|
|
1b624e7e6f | ||
|
|
7223ae327a | ||
|
|
bcdefda0db | ||
|
|
fc56642631 | ||
|
|
69299d395c | ||
|
|
a789f58068 | ||
|
|
f68935735d | ||
|
|
ba2157cc43 | ||
|
|
57fe5c04ec | ||
|
|
3a08944c99 | ||
|
|
b6154b273c | ||
|
|
e332bff362 | ||
|
|
59e03434e4 | ||
|
|
2ca0edf5fd | ||
|
|
90dca9072e | ||
|
|
c0ead4a484 | ||
|
|
f8cb9e2bd3 | ||
|
|
df4dabf68f | ||
|
|
40720f9949 | ||
|
|
7e7a1dce7b | ||
|
|
39b9a7f0ed | ||
|
|
43010779d4 | ||
|
|
a1a4fc8c7c | ||
|
|
621e4a513c | ||
|
|
6c821039b5 | ||
|
|
f80b948fb1 | ||
|
|
d4c27da892 | ||
|
|
11bf0ca868 | ||
|
|
664092c023 | ||
|
|
0cd2c4a90c | ||
|
|
2f6c04a86d | ||
|
|
a382e74a89 | ||
|
|
3c8c76f50d | ||
|
|
cbce379132 | ||
|
|
e795e96bfb | ||
|
|
e12e2c4b0b | ||
|
|
9a1b21a2fa | ||
|
|
79708f9ba6 | ||
|
|
102544877d | ||
|
|
1c32ae1227 | ||
|
|
55d7e444c7 | ||
|
|
9243fd499b | ||
|
|
844dc48d65 | ||
|
|
52d96b8518 | ||
|
|
8274e68846 | ||
|
|
f1adcd1836 | ||
|
|
2a77f57aa4 | ||
|
|
04ebe4c68a | ||
|
|
1c7e065c52 | ||
|
|
80f3857f44 | ||
|
|
3b62831401 | ||
|
|
a047e1d063 | ||
|
|
45985e1684 | ||
|
|
7dac886375 | ||
|
|
af3b8a9902 | ||
|
|
4e37666037 | ||
|
|
2769d32c81 | ||
|
|
4b004320a4 | ||
|
|
edfc879b41 | ||
|
|
0879d1da0d | ||
|
|
c5159f8a87 | ||
|
|
a0d9bf0f26 | ||
|
|
a3cce8ff72 | ||
|
|
242f00390d | ||
|
|
f4b64370bb | ||
|
|
ae57e4da83 | ||
|
|
cd391a360c | ||
|
|
dcbe12f1af | ||
|
|
5b61cfe922 | ||
|
|
739406614d | ||
|
|
8076206a90 | ||
|
|
f63d218aae | ||
|
|
f2e3ccd18c | ||
|
|
d41e69d770 | ||
|
|
0e3bc1ce43 | ||
|
|
6ca175345f | ||
|
|
c063a638cd | ||
|
|
2dfe1420bc | ||
|
|
7dd379c5c3 | ||
|
|
752eb6235d | ||
|
|
3f077727ae | ||
|
|
51d557524d | ||
|
|
5d9563304f | ||
|
|
53cb68f82c | ||
|
|
95b107ffad | ||
|
|
06dedf51aa | ||
|
|
ed7c8c11d9 | ||
|
|
643e5954f3 | ||
|
|
c7cc9d2a65 | ||
|
|
5050914d25 | ||
|
|
31fa2d866f | ||
|
|
4a096cb728 | ||
|
|
16b52f929c | ||
|
|
8ec133830f | ||
|
|
c144799a11 | ||
|
|
e56511a05a | ||
|
|
bdb959d986 | ||
|
|
dae9c368b7 | ||
|
|
2a401a302d | ||
|
|
ff3b8e5270 | ||
|
|
bb1a066ff7 | ||
|
|
9a9872812f | ||
|
|
969e4a2d65 | ||
|
|
77f0b87c54 | ||
|
|
eec56256e8 | ||
|
|
99dc91dcb4 | ||
|
|
5adcfbd7bd | ||
|
|
3c5f1f7c4b | ||
|
|
4415429661 | ||
|
|
92c404fbb6 | ||
|
|
956b9aaf95 | ||
|
|
c8348f1b44 | ||
|
|
d73ceb374c | ||
|
|
3e01953a3a | ||
|
|
1a65b1affb | ||
|
|
dd75078019 | ||
|
|
1ab205cb46 | ||
|
|
eb26d54ac1 | ||
|
|
d884645f37 | ||
|
|
45f0c3c85f | ||
|
|
4e498e6932 | ||
|
|
d9d68cd37c | ||
|
|
56abd0bb7f | ||
|
|
747683e9e8 | ||
|
|
5b191d72a6 | ||
|
|
b7619630cf | ||
|
|
de07f10e57 | ||
|
|
87e6080215 | ||
|
|
e721a234e1 | ||
|
|
67289bbba2 | ||
|
|
b0c24f6e51 | ||
|
|
12f474ecbe | ||
|
|
e2f20a7a65 | ||
|
|
58ffabe2d6 | ||
|
|
426f06045e | ||
|
|
2ca875192a | ||
|
|
36b2f24de9 | ||
|
|
2cf3b3be2b | ||
|
|
8320c6940b | ||
|
|
3c9d21e38d | ||
|
|
0d7f00aef6 | ||
|
|
1640763a7f | ||
|
|
0ec5346574 | ||
|
|
977845e2f4 | ||
|
|
4bb2ab73c1 | ||
|
|
af5584d244 | ||
|
|
b289e41a0d | ||
|
|
c0b4eef948 | ||
|
|
b8e6c2fe78 | ||
|
|
b10e86e484 | ||
|
|
1c31cffe4b | ||
|
|
b9822cd5d1 | ||
|
|
ef1c70123c | ||
|
|
a0e5a707fb | ||
|
|
c245531d65 | ||
|
|
4555a4968d | ||
|
|
522495fd11 | ||
|
|
b2a551dc63 | ||
|
|
840107c69e | ||
|
|
ae34e4749b | ||
|
|
2a06a1ffdf | ||
|
|
8a01209ded | ||
|
|
2b8dfc9dbc | ||
|
|
e9f25eb562 | ||
|
|
475c7fd6db | ||
|
|
fa5d64022d | ||
|
|
5c0f6cf65e | ||
|
|
0f8d366e31 | ||
|
|
9e2e593825 | ||
|
|
028bfc11eb | ||
|
|
b6cf7a4534 | ||
|
|
0779679b99 | ||
|
|
a48ef69e41 | ||
|
|
64d7953e50 | ||
|
|
21c35b31d4 | ||
|
|
2d64acf299 | ||
|
|
e80819fc06 | ||
|
|
7e40d58d04 | ||
|
|
ba8fafcc95 | ||
|
|
42694d2004 | ||
|
|
5e67ce0ab2 | ||
|
|
97b73e1a01 | ||
|
|
4239257000 | ||
|
|
6bd94761d0 | ||
|
|
bc54e57fd4 | ||
|
|
17751e52fd | ||
|
|
6016ebd3b4 | ||
|
|
8a44fc19ee | ||
|
|
f69450e2ae | ||
|
|
3161ccced3 | ||
|
|
909aad22a6 | ||
|
|
c8b4fd1690 | ||
|
|
dac2271084 | ||
|
|
5320d20f71 | ||
|
|
20d8cc2b47 | ||
|
|
d3fdbdbca5 | ||
|
|
d80f7ebf3b | ||
|
|
d6c84e362f | ||
|
|
635eee9590 | ||
|
|
6aba07c33b | ||
|
|
f0558c4c1e | ||
|
|
45913b0add | ||
|
|
c258409a8d | ||
|
|
26cd2d3fef | ||
|
|
4069074f41 | ||
|
|
182422249f | ||
|
|
29b70b3247 | ||
|
|
637ca8fbca | ||
|
|
e474748f4d | ||
|
|
e4e65df976 | ||
|
|
132ba905c7 | ||
|
|
29579a2aec | ||
|
|
e32b43cffb | ||
|
|
208d1b82b5 | ||
|
|
1fd7c95f1b | ||
|
|
481f195725 | ||
|
|
d6c1550a1d | ||
|
|
60b9ae0a70 | ||
|
|
bf71351e6d | ||
|
|
8e361a8776 | ||
|
|
de2e5f861b | ||
|
|
e39fd53727 | ||
|
|
b27a7fc71b | ||
|
|
76efcca64b | ||
|
|
5861ce6146 | ||
|
|
35f49d9bc0 | ||
|
|
a0a55797a9 | ||
|
|
c732ed556f | ||
|
|
be1a75e520 | ||
|
|
696a429e9e | ||
|
|
c41d10c581 | ||
|
|
29cd55e795 | ||
|
|
c2ebf61fd3 | ||
|
|
489b4d6acf | ||
|
|
6cffc47f3c | ||
|
|
50d8e08a34 | ||
|
|
3f16c0378a | ||
|
|
2b06d8a096 | ||
|
|
2a5b37b9ad | ||
|
|
cabbd234af | ||
|
|
480039ca50 | ||
|
|
81d5cddac9 | ||
|
|
bb3e83548c | ||
|
|
a3ff58c42d | ||
|
|
30bf4542f0 | ||
|
|
d11c9a4615 | ||
|
|
d4679e0029 | ||
|
|
3c4d6eb8e4 | ||
|
|
df98e1f8ac | ||
|
|
56fc2986e9 | ||
|
|
627f18c42b | ||
|
|
67b341cbd6 | ||
|
|
eff5b94db7 | ||
|
|
707323ca12 | ||
|
|
cb2ba2f53a | ||
|
|
777cd310ef | ||
|
|
9a4ec5a899 | ||
|
|
c92e4e06cc | ||
|
|
39135f8c80 | ||
|
|
5e054cfc77 | ||
|
|
3b72c13f9d | ||
|
|
65ed110ab7 | ||
|
|
5dd7ea3060 | ||
|
|
bd9cbf5e3b | ||
|
|
65f6a4eddd | ||
|
|
e9ff554f39 | ||
|
|
b65d178cf1 | ||
|
|
157a59e3c4 | ||
|
|
d24c65c3aa | ||
|
|
d4d441ff9b | ||
|
|
f43fb1223f | ||
|
|
3db275414d | ||
|
|
6e0ff6c805 | ||
|
|
9c359efbff | ||
|
|
c5cc1ea8e8 | ||
|
|
afb6a33131 | ||
|
|
539a11f54d | ||
|
|
9324e549e6 | ||
|
|
c8f6b6df87 | ||
|
|
38f632097e | ||
|
|
e63743f537 | ||
|
|
ce13295cf0 | ||
|
|
23c02a3510 | ||
|
|
01ea7b9ee1 | ||
|
|
6fed1284a1 | ||
|
|
5574d849bd | ||
|
|
c2ce3a6992 | ||
|
|
b61d15fdf4 | ||
|
|
ad5cc83fb3 | ||
|
|
2f53edd775 | ||
|
|
923459c88b | ||
|
|
1192d26448 | ||
|
|
2d5e827417 | ||
|
|
a30d9276b8 | ||
|
|
b963c2272f | ||
|
|
221cb8709b | ||
|
|
648857fc44 | ||
|
|
8091051bb4 | ||
|
|
1e468cd3e0 | ||
|
|
15d2a5bb2b | ||
|
|
5c76229ee5 | ||
|
|
0cd088122e | ||
|
|
b6f3467a89 | ||
|
|
52ce1a5959 | ||
|
|
7ce05762f5 | ||
|
|
cce736bc16 | ||
|
|
2a1127e637 | ||
|
|
8ca68b3d7a | ||
|
|
0f76d893b8 | ||
|
|
ab746b5195 | ||
|
|
828c87df39 | ||
|
|
888aeb621d | ||
|
|
ac2fddf87e | ||
|
|
614304cc81 | ||
|
|
f363d66aac | ||
|
|
1706c42695 | ||
|
|
2b44b2fc0b | ||
|
|
25986aa15c | ||
|
|
264eb01afc | ||
|
|
6db3c3f9f1 | ||
|
|
714992bdcb | ||
|
|
ca7a86b6d7 | ||
|
|
49e729e9ec | ||
|
|
7f0a690c7b | ||
|
|
d07afc603b | ||
|
|
5bd4c00257 | ||
|
|
13272aa280 | ||
|
|
260358c5fb | ||
|
|
0f07b2c095 | ||
|
|
b2fafc22dd | ||
|
|
6200ec8e0e | ||
|
|
831516c5f5 | ||
|
|
fb49d9e6ae | ||
|
|
e660726828 | ||
|
|
4519f47b19 | ||
|
|
47174bbb4d | ||
|
|
de18ce3e7b | ||
|
|
b5934633fa | ||
|
|
f314c08216 | ||
|
|
6704cded2d | ||
|
|
f11918fc41 | ||
|
|
58d5aa9944 | ||
|
|
60af2ad0a4 | ||
|
|
4ec8994c38 | ||
|
|
49d6cece50 | ||
|
|
03251d5bd5 | ||
|
|
a6e03184bc | ||
|
|
54f2a6f1b5 | ||
|
|
7eed7ae6ba | ||
|
|
b3d642fdf5 | ||
|
|
35dd9427a5 | ||
|
|
bf1087df3c | ||
|
|
523ef08697 | ||
|
|
c2310453d8 | ||
|
|
b4a6a5dc26 | ||
|
|
20ceb440fa | ||
|
|
4ee9c66524 | ||
|
|
9d04abd3af | ||
|
|
a562345876 | ||
|
|
2e7daceb3c | ||
|
|
a9aba3484a | ||
|
|
1b9bbb8eb6 | ||
|
|
25c44723a9 | ||
|
|
688a20c312 | ||
|
|
4c1b34461e | ||
|
|
c6939e7194 | ||
|
|
cd9432e395 | ||
|
|
7d5244a5bc | ||
|
|
1aa5d4f7de | ||
|
|
ad74204fe4 | ||
|
|
5cb1a47ed3 | ||
|
|
fff22d3cd1 | ||
|
|
665cd7b996 | ||
|
|
61173196d2 | ||
|
|
03e98ba562 | ||
|
|
6195290adf | ||
|
|
4f1b0055e1 | ||
|
|
dd9fe87ff4 | ||
|
|
5bda4abfce | ||
|
|
24f98aede5 | ||
|
|
f8ae1e9076 | ||
|
|
16b0dcad71 | ||
|
|
9c1265d50d | ||
|
|
8c0e781c94 | ||
|
|
a47bef1f13 | ||
|
|
053663bd76 | ||
|
|
e40b916b07 | ||
|
|
2ffe3bf722 | ||
|
|
1bc994c102 | ||
|
|
21be4d38e1 | ||
|
|
3b58c571b3 | ||
|
|
e0430cf607 | ||
|
|
f7889b785d | ||
|
|
27829a024a | ||
|
|
a0fda2552f | ||
|
|
65c185ebd2 | ||
|
|
fb425d3524 | ||
|
|
5792eb354c | ||
|
|
45300b64c5 | ||
|
|
dce7d91511 | ||
|
|
4b7ef46f82 | ||
|
|
b67e6d20f1 | ||
|
|
3d1afa74d4 | ||
|
|
0ae6575087 | ||
|
|
d8c94f5ece | ||
|
|
61f1e11a48 | ||
|
|
98249b1aca | ||
|
|
8e51b7fc1d | ||
|
|
71914b8a8b | ||
|
|
7a077a0d21 | ||
|
|
d23e4665e7 | ||
|
|
df12b54856 | ||
|
|
ae12053d74 | ||
|
|
0e7695f2d6 | ||
|
|
38cee87ee9 | ||
|
|
c64a6083e2 | ||
|
|
e0501e46e3 | ||
|
|
4a0ccc4c2f | ||
|
|
c92b7240eb | ||
|
|
6a74c3faeb | ||
|
|
c5ae872787 | ||
|
|
2e9ab0a9d7 | ||
|
|
08f67860be | ||
|
|
3fab1d92b7 | ||
|
|
6573157112 | ||
|
|
e83841eb0b | ||
|
|
6a0e18c0e9 | ||
|
|
b71b6473e3 | ||
|
|
a004f62fe8 | ||
|
|
c69e5e05a3 | ||
|
|
07ce20fd29 | ||
|
|
df74f7c6c7 | ||
|
|
2a6e2d470d | ||
|
|
643dd65113 | ||
|
|
cee0532b85 | ||
|
|
845e7fb956 | ||
|
|
f5cc2c3a37 | ||
|
|
d56c2f7a50 | ||
|
|
5c47a8f7e1 | ||
|
|
fb724e0150 | ||
|
|
94ef47c89d | ||
|
|
7e415b3fd7 | ||
|
|
fee497219e | ||
|
|
7624f09f98 | ||
|
|
946fe0a94c | ||
|
|
c7e1699546 | ||
|
|
de4a617743 | ||
|
|
228d62dd32 | ||
|
|
51f8a60096 | ||
|
|
83f1a95966 | ||
|
|
646ba94d10 | ||
|
|
8c5c08d678 | ||
|
|
3d0b47a181 | ||
|
|
ea7bece3b8 | ||
|
|
944a8e9cd6 | ||
|
|
88f7d1d572 | ||
|
|
6d78c1e302 | ||
|
|
791aa8b6d1 | ||
|
|
a078b67b36 | ||
|
|
34d4ecd4e6 | ||
|
|
5047b535c4 | ||
|
|
768d239840 | ||
|
|
89392553bd | ||
|
|
9a41217a59 | ||
|
|
f6557e4980 | ||
|
|
cc3cd95e2d | ||
|
|
8fb02136bb | ||
|
|
1c86585f03 | ||
|
|
242085966b | ||
|
|
6217075adc | ||
|
|
883f233c09 | ||
|
|
e4cc749180 | ||
|
|
9780798b75 | ||
|
|
225325cc29 | ||
|
|
2e3307065c | ||
|
|
f6a4316093 | ||
|
|
b80aad7449 | ||
|
|
5dbd1d093b | ||
|
|
64e2e1109e | ||
|
|
9c60140cec | ||
|
|
6099b664ac | ||
|
|
33d084eff0 | ||
|
|
07eb14eaa6 | ||
|
|
b2710fafd4 | ||
|
|
8f44b4571a | ||
|
|
169f010fae | ||
|
|
3e1547e0da | ||
|
|
825b9935ee | ||
|
|
2f2e086fbb | ||
|
|
2f6e0c15fe | ||
|
|
09e0c3dd63 | ||
|
|
4529ac9b92 | ||
|
|
8dac4eca55 | ||
|
|
bf56091dc7 | ||
|
|
f10f580f1c | ||
|
|
c09861d1ca | ||
|
|
8e8df2b846 | ||
|
|
d4a1d5bb9e | ||
|
|
84145aa7a7 | ||
|
|
25f32a4776 | ||
|
|
097c42d1dd | ||
|
|
91ac368a19 | ||
|
|
f959dd0519 | ||
|
|
444605920f | ||
|
|
a102d8b39f | ||
|
|
22eb861d28 | ||
|
|
779b36bf7b | ||
|
|
f36336403b | ||
|
|
b7fbade968 | ||
|
|
0535b20db7 | ||
|
|
ddd91e3078 | ||
|
|
97f734b8fc | ||
|
|
38941f02a8 | ||
|
|
84e09be199 | ||
|
|
995a89d370 | ||
|
|
6f11a269bc | ||
|
|
24a212a987 | ||
|
|
3282ba0302 | ||
|
|
a4db443f93 | ||
|
|
f8276f7e59 | ||
|
|
b545634d87 | ||
|
|
5da4cace94 | ||
|
|
1a2475dc25 | ||
|
|
2374239238 | ||
|
|
f84d77d334 | ||
|
|
5a275e6153 | ||
|
|
5f07069f1d | ||
|
|
1e1cebc3fc | ||
|
|
c40ad75fa2 | ||
|
|
c80db72bf7 | ||
|
|
727ec6bc28 | ||
|
|
9ebad734a9 | ||
|
|
0094628f6b | ||
|
|
abbb046566 | ||
|
|
a1136953d0 | ||
|
|
3298a5c6ad | ||
|
|
5e24fe9bb1 | ||
|
|
bc5b32ddef | ||
|
|
7269dbb79d | ||
|
|
6f5bb6ffa2 | ||
|
|
7d0b738918 | ||
|
|
1539d9c7ed | ||
|
|
4ae95e06ef | ||
|
|
fa1166d014 | ||
|
|
2d9e5fe984 | ||
|
|
7ae934e940 | ||
|
|
64b2eeface | ||
|
|
558dc591a3 | ||
|
|
0bfd766a0b | ||
|
|
7741713a7c | ||
|
|
454b540bce | ||
|
|
591c62b6d5 | ||
|
|
fdb4a7b055 | ||
|
|
f845ad9b31 | ||
|
|
f5f33ec865 | ||
|
|
d35faf15d7 | ||
|
|
7085bce6d4 | ||
|
|
a81890e844 | ||
|
|
8dbec21b02 | ||
|
|
a654c8229a | ||
|
|
f2e35c185b | ||
|
|
062c2643ad | ||
|
|
088c7b35ba | ||
|
|
ef439a6c42 | ||
|
|
40956a4042 | ||
|
|
8680e90e3b | ||
|
|
fac770424c | ||
|
|
042be9da6d | ||
|
|
81b0ea1eef | ||
|
|
52289d1283 | ||
|
|
1843d23203 | ||
|
|
1a32fef987 | ||
|
|
c740c8304b | ||
|
|
c3401d478b | ||
|
|
cf583bcd55 | ||
|
|
ef143a7ebb | ||
|
|
7aec483e73 | ||
|
|
db6a3b9849 | ||
|
|
657c5e1f52 | ||
|
|
3b0466d7cb | ||
|
|
8a9177b459 | ||
|
|
a0e63511d6 | ||
|
|
7c3f7d4b8e | ||
|
|
be1062c373 | ||
|
|
2bd5ab06a7 | ||
|
|
e174c1b147 | ||
|
|
7ef191be2a | ||
|
|
03a29aeedf | ||
|
|
d10b4c1e13 | ||
|
|
ab2046a2c2 | ||
|
|
bc6b2e0f3e | ||
|
|
746c99ebd6 | ||
|
|
34945e7eba | ||
|
|
507b217065 | ||
|
|
e222f17199 | ||
|
|
144cfecc0f | ||
|
|
64066bfc90 | ||
|
|
366ac4ee14 | ||
|
|
851984ee66 | ||
|
|
34bdc0e80b | ||
|
|
61d7d5e041 | ||
|
|
b5278550e7 | ||
|
|
461635c001 | ||
|
|
fcb1d8e104 | ||
|
|
6cbc2b707a | ||
|
|
9671542bdf | ||
|
|
de325c1208 | ||
|
|
802eff1faa | ||
|
|
068f9e42d7 | ||
|
|
4a483c3b27 | ||
|
|
e3bd958069 | ||
|
|
900cf0a9d0 | ||
|
|
64c424b9a6 | ||
|
|
5734c29312 | ||
|
|
362caa6ac1 | ||
|
|
b620976b70 | ||
|
|
daba4ef09e | ||
|
|
c697a34239 | ||
|
|
09b7cb3d85 | ||
|
|
d7e48662e0 | ||
|
|
9fd5c6f230 | ||
|
|
aa7825d4aa | ||
|
|
436725b38e | ||
|
|
922d935bc1 | ||
|
|
3716395453 | ||
|
|
69833f66e3 | ||
|
|
ec787b913c | ||
|
|
89f313295e | ||
|
|
7bc7a44c72 | ||
|
|
317a882386 | ||
|
|
3a9f585b6b | ||
|
|
7bbb1c0822 | ||
|
|
d3b1f6110f | ||
|
|
a6dc3d2aff | ||
|
|
d946a9e526 | ||
|
|
17dd1b193e | ||
|
|
d634fd3236 | ||
|
|
e861e5b3d6 | ||
|
|
6045f4dd91 | ||
|
|
8be2841bdf | ||
|
|
b6e37b9e67 | ||
|
|
0d0d582bd8 | ||
|
|
0c42227e5e | ||
|
|
98ac4bd5c8 | ||
|
|
a43b100781 | ||
|
|
c7f9bfbb43 | ||
|
|
b5f8e6925b | ||
|
|
993e59413a | ||
|
|
9f2fab78a1 | ||
|
|
3bdf1377c0 | ||
|
|
8f24cf07be | ||
|
|
17c40234e9 | ||
|
|
4cecddcdd0 | ||
|
|
1f4516b954 | ||
|
|
1d76ee4871 | ||
|
|
b53cac4c7a | ||
|
|
29a0644719 | ||
|
|
c833078e71 | ||
|
|
e4cba8d19f | ||
|
|
cd6620712f | ||
|
|
85619b156d | ||
|
|
10debbc286 | ||
|
|
dcac5b488a | ||
|
|
e1009bdafa | ||
|
|
38ce842ca8 | ||
|
|
aafd09569c | ||
|
|
67a9df686e | ||
|
|
9a374711fd | ||
|
|
b9138acbc8 | ||
|
|
2a65916f7c | ||
|
|
6aa1f1cca0 | ||
|
|
8c1ebfda02 | ||
|
|
81af5d7497 | ||
|
|
368bf08ade | ||
|
|
d95f623ca9 | ||
|
|
2856fbc42b | ||
|
|
ac59e15bd9 | ||
|
|
91d9bbdc97 | ||
|
|
575f4e1786 | ||
|
|
bff905fae5 | ||
|
|
c0fa135bf6 | ||
|
|
86394d8f19 | ||
|
|
72c233cb0d | ||
|
|
04e2c02eff | ||
|
|
7362744df2 | ||
|
|
01951b5c32 | ||
|
|
f2f52771bd | ||
|
|
b59167d3ca | ||
|
|
88e466562c | ||
|
|
1f85e5d7f8 | ||
|
|
50471d510e | ||
|
|
8b7cf2f725 | ||
|
|
282a5109ba | ||
|
|
3d3b4738d9 | ||
|
|
66149bb591 | ||
|
|
b0ad664ece | ||
|
|
7c29ea836c | ||
|
|
92e9e8c56a | ||
|
|
12bf26223d | ||
|
|
56d7993c8f | ||
|
|
52b63927b4 | ||
|
|
86558bdef6 | ||
|
|
e46262b021 | ||
|
|
c53feb5ccb | ||
|
|
fc6d4f0990 | ||
|
|
df948bde9d | ||
|
|
203a720ae1 | ||
|
|
3410f08cfb | ||
|
|
a553914ef4 | ||
|
|
21220141f2 | ||
|
|
caf2d8436b | ||
|
|
4cc305fa81 | ||
|
|
60f837d0b9 | ||
|
|
05bd7f8e6b | ||
|
|
e58ab34a15 | ||
|
|
d960758ef3 | ||
|
|
a36ccdcc39 | ||
|
|
d582948377 | ||
|
|
d806e0b1c3 | ||
|
|
b9e110a7e3 | ||
|
|
a2f218d56d | ||
|
|
2c475011a1 | ||
|
|
2d7fc33726 | ||
|
|
0c8d1e1dc4 | ||
|
|
bb04ce2abb | ||
|
|
9850b22c0a | ||
|
|
a2f65666a5 | ||
|
|
7730809dbb | ||
|
|
2ac818dcdd | ||
|
|
113947b9f0 | ||
|
|
44bc2d769b | ||
|
|
02ecfebb85 | ||
|
|
a1fed62591 | ||
|
|
778ed6ad91 | ||
|
|
7d539f5810 | ||
|
|
b407acbc07 | ||
|
|
3260260dce | ||
|
|
70c1290993 | ||
|
|
6bae60c51e | ||
|
|
a45adb6b3a | ||
|
|
476aaf5d3e | ||
|
|
58187b6969 | ||
|
|
f3a3d81d96 | ||
|
|
7a40b54153 | ||
|
|
9dd62d3538 | ||
|
|
76e4a6ed83 | ||
|
|
7a9eb06677 | ||
|
|
26f54e7619 | ||
|
|
117b7ae414 | ||
|
|
baeac324d6 | ||
|
|
0db0f003dc | ||
|
|
2d4f341710 | ||
|
|
b8a41dc937 | ||
|
|
2f2bb0de4f | ||
|
|
3b76d7f47e | ||
|
|
10b74e507f | ||
|
|
8a03a9462b | ||
|
|
e5bca224e9 | ||
|
|
197bf5d0cf | ||
|
|
d8b15ebcdb | ||
|
|
078466241f | ||
|
|
57c3eb5d2c | ||
|
|
a4876167c4 | ||
|
|
a38a5654a9 | ||
|
|
69a41879bb | ||
|
|
e3524a506b | ||
|
|
8447c563ea | ||
|
|
fd61a4b23a | ||
|
|
9257311896 | ||
|
|
23e870e416 | ||
|
|
8270b28d85 | ||
|
|
1a0889d3d9 | ||
|
|
5382d99a94 | ||
|
|
3e4bb88089 | ||
|
|
2f3f53a978 | ||
|
|
89755b1005 | ||
|
|
a7203ea90a | ||
|
|
afb0ac14c4 | ||
|
|
745dfc71bc | ||
|
|
82d9689d1b | ||
|
|
6afaef1654 | ||
|
|
bb42d86012 | ||
|
|
5b44580061 | ||
|
|
4eac743812 | ||
|
|
ed8ab37bd5 | ||
|
|
563c3f0f1b | ||
|
|
296e6e8e8f | ||
|
|
334aab2755 | ||
|
|
419f4f3156 | ||
|
|
ec5a26e8dd | ||
|
|
2b7cd36eea | ||
|
|
2f11731052 | ||
|
|
23a0846533 | ||
|
|
666858f8e2 | ||
|
|
2288b7f7b2 | ||
|
|
498af28efb | ||
|
|
3902ab3375 | ||
|
|
6bb0bdf66e | ||
|
|
b9ade2295e | ||
|
|
44b5f5a919 | ||
|
|
17d37494c2 | ||
|
|
f0d81e98a0 | ||
|
|
e3b13f7b4a | ||
|
|
ba2686630a | ||
|
|
e195cfa6a0 | ||
|
|
b9fbd1906f | ||
|
|
1f611bafef | ||
|
|
af7faa59dc | ||
|
|
0b21ee46ea | ||
|
|
f64996a350 | ||
|
|
d7cccd1980 | ||
|
|
b9467d9236 | ||
|
|
69096b15ae | ||
|
|
a075e62bad | ||
|
|
1ebe367e07 | ||
|
|
db229f25bf | ||
|
|
787c93b9d4 | ||
|
|
63953992a9 | ||
|
|
567cb0c0b6 | ||
|
|
f37999a3ef | ||
|
|
545761e3d4 | ||
|
|
97ea67d01d | ||
|
|
561d679a62 | ||
|
|
991cd91dd4 | ||
|
|
9e51ff0253 | ||
|
|
1a1e55e16c | ||
|
|
2fe4a39784 | ||
|
|
7d81b9ef5c | ||
|
|
b355b6dc60 | ||
|
|
819148762b | ||
|
|
4112a86fe9 | ||
|
|
7838c9b49b | ||
|
|
ca5a70e3bc | ||
|
|
d6376c3a91 | ||
|
|
e134143f16 | ||
|
|
0f1577d314 | ||
|
|
793b356c01 | ||
|
|
a36858f3ea | ||
|
|
aaa6637435 | ||
|
|
f0f6739cf8 | ||
|
|
25efdd3d4f | ||
|
|
0b2483ea15 | ||
|
|
2c38ce910c | ||
|
|
4d26ec0789 | ||
|
|
edba923f2f | ||
|
|
7d907aac0f | ||
|
|
d6981550a8 | ||
|
|
8b0636367b | ||
|
|
a6c9d0f9bc | ||
|
|
4b0d2f7abc | ||
|
|
dd28781b69 | ||
|
|
ece56032f1 | ||
|
|
61ebe9780e | ||
|
|
e887082501 | ||
|
|
8e3039dd37 | ||
|
|
1d1c130d19 | ||
|
|
1fd3f70eec | ||
|
|
d9ea33cbb9 | ||
|
|
80778aa267 | ||
|
|
445cb4f146 | ||
|
|
95db2aa14f | ||
|
|
4b0fc637eb | ||
|
|
48d6b4cfa1 | ||
|
|
fc11182bbe | ||
|
|
25b72e1af4 | ||
|
|
1848338ef7 | ||
|
|
ff0446cc12 | ||
|
|
08ceb57c31 | ||
|
|
affb332eb9 | ||
|
|
6455c38ff4 | ||
|
|
f62c2fbabb | ||
|
|
d5276c9d4a | ||
|
|
4c0fc5a407 | ||
|
|
f608cd5aef | ||
|
|
0371b62acb | ||
|
|
eddd66b5c4 | ||
|
|
c8e71d269b | ||
|
|
ae034d5387 | ||
|
|
07e5c568c4 | ||
|
|
817d6a0e15 | ||
|
|
31fdd24c3e | ||
|
|
bfa0e4ba49 | ||
|
|
dff98f0b53 | ||
|
|
d0856ff279 | ||
|
|
c89ff2c3d6 | ||
|
|
4ec88d524a | ||
|
|
d6b762efa7 | ||
|
|
b476a26759 | ||
|
|
48c218b430 | ||
|
|
1a062e2170 | ||
|
|
fe658eb877 | ||
|
|
d316836e90 | ||
|
|
8443f61f0a | ||
|
|
feed55186f | ||
|
|
56591b8655 | ||
|
|
7c52ca15f3 | ||
|
|
06c751f214 | ||
|
|
9d774eaad8 | ||
|
|
babfd4abda | ||
|
|
a029c165a0 | ||
|
|
9b51533d96 | ||
|
|
e1b7e0eb00 | ||
|
|
508a5693c9 | ||
|
|
67806f3d76 | ||
|
|
f687a30c7e | ||
|
|
15615a1077 | ||
|
|
905175c210 | ||
|
|
409e070887 | ||
|
|
f659dc1f76 | ||
|
|
b8922b39fd | ||
|
|
8137d57cdf | ||
|
|
bf290ac1a9 | ||
|
|
77fda00233 | ||
|
|
0255088e30 | ||
|
|
b74d8b12d0 | ||
|
|
2834f2982c | ||
|
|
7d07faa5fb | ||
|
|
fd70f0fc4a | ||
|
|
7744f84e85 | ||
|
|
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 | ||
|
|
6470d25d18 | ||
|
|
20f97e48a9 | ||
|
|
94c5ed8bdc | ||
|
|
4b7b4bf110 | ||
|
|
a223da8f99 | ||
|
|
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 | ||
|
|
dc235f36c8 | ||
|
|
7503c1e1e9 | ||
|
|
253e8a209c | ||
|
|
e26b692631 | ||
|
|
658d808524 | ||
|
|
28e00055ab | ||
|
|
8eef1eaa7c | ||
|
|
e361a857a4 | ||
|
|
d369ce8847 | ||
|
|
e60166dc89 | ||
|
|
87afc2fcef | ||
|
|
bd1457c435 | ||
|
|
c1f88b4a5f | ||
|
|
a080d4b692 | ||
|
|
c20e9e19cb | ||
|
|
d9056acc6d | ||
|
|
8ffbf32677 | ||
|
|
6e2124252c | ||
|
|
3dd07b8c23 | ||
|
|
fae9650f56 | ||
|
|
9019cbfd2b | ||
|
|
85f931316a | ||
|
|
717ea05d38 | ||
|
|
80612ba97d | ||
|
|
c4d2b92e34 | ||
|
|
ab26a76789 | ||
|
|
ef84703da9 | ||
|
|
75769df8e2 | ||
|
|
a81254cd18 | ||
|
|
4cff838de0 | ||
|
|
3838b3ca4f | ||
|
|
7dc3e041c8 | ||
|
|
33b92423d8 | ||
|
|
6237fffa5a | ||
|
|
0b4e2d3b6b | ||
|
|
398630d51e | ||
|
|
61400ba726 | ||
|
|
33885e2216 | ||
|
|
5719b136fe | ||
|
|
ede5ee60c3 |
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,2 @@
|
||||
# Black
|
||||
a47bef1f1336fd264d0b175f4421758339a30acb
|
||||
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Before filing, please search the issue tracker to see if the issue has already been reported.
|
||||
|
||||
If reporting a DCS AI bug, check https://github.com/dcs-liberation/dcs_liberation#dcs-bugs.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Additional information**
|
||||
|
||||
We will usually need more information for debugging. Include as much of the following as you are able:
|
||||
|
||||
- DCS Liberation save file (the `.liberation` file you save from the DCS Liberation window). By default these are located in your DCS saved games directory (`%USERPROFILE%/Saved Games/DCS`).
|
||||
- The generated mission file (the `.miz` file that you load in DCS to play the turn). By default these are located in your missions directory (`%USERPROFILE%/Saved Games/DCS/Missions`).
|
||||
- A tacview track file, especially when demonstrating an issue with AI behavior. By default these are located in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`).
|
||||
- The state.json file from the finished mission when the problem is related to results processing. By default these are located in your Liberation install directory.
|
||||
|
||||
**Version information (please complete the following information):**
|
||||
- DCS Liberation [e.g. 2.3.1]:
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
28
.github/ISSUE_TEMPLATE/campaign_update.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE/campaign_update.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Campaign update submission
|
||||
about: Submit an update to a campaign you maintain.
|
||||
title: 'Update for <campaign name>'
|
||||
labels: campaign-update-submission
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
This form should only be used for submitted updated miz/json files for campaigns
|
||||
distributed with Liberation. If you are _requesting_ an update to a campaign, see
|
||||
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance. If the
|
||||
campaign has an owner, it will be updated before release. If it does not, you can
|
||||
volunteer to own it.
|
||||
|
||||
If you are not the owner of the campaign listed on
|
||||
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance, please start
|
||||
there.
|
||||
|
||||
Otherwise, delete everything above the line below and fill out the following form. Note:
|
||||
GitHub does not accept .miz files. You can either rename the file to .miz.txt or add the
|
||||
file to a .zip file.
|
||||
|
||||
---
|
||||
|
||||
* Campaign name:
|
||||
* Files:
|
||||
* Update summary (optional):
|
||||
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Before filing, please search the issue tracker to see if this feature has already been requested.
|
||||
|
||||
If requesting a DCS AI feature, check If reporting a DCS AI bug, check https://github.com/dcs-liberation/dcs_liberation#dcs-bugs.
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
13
.github/workflows/black.yml
vendored
Normal file
13
.github/workflows/black.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Lint
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: psf/black@stable
|
||||
with:
|
||||
args: ". --check"
|
||||
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install environment
|
||||
run: |
|
||||
@@ -36,11 +36,6 @@ jobs:
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy gen
|
||||
|
||||
- name: mypy theater
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy theater
|
||||
|
||||
- name: update build number
|
||||
run: |
|
||||
|
||||
24
.github/workflows/release.yml
vendored
24
.github/workflows/release.yml
vendored
@@ -13,10 +13,10 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install environment
|
||||
run: |
|
||||
@@ -43,11 +43,6 @@ jobs:
|
||||
./venv/scripts/activate
|
||||
mypy gen
|
||||
|
||||
- name: mypy theater
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy theater
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
@@ -58,11 +53,6 @@ jobs:
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref }}
|
||||
run: |
|
||||
$version = ($env:TAG_NAME -split "/") | Select-Object -Last 1
|
||||
(Get-Content .\installer\dcs_liberation.iss) -replace "{{version}}",$version | Out-File .\build\installer.iss
|
||||
cd .\installer
|
||||
iscc.exe ..\build\installer.iss
|
||||
cd ..
|
||||
Copy-Item .\changelog.md .\dist
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
@@ -105,15 +95,7 @@ jobs:
|
||||
body_path: releasenotes.md
|
||||
draft: false
|
||||
prerelease: ${{ steps.version.outputs.prerelease }}
|
||||
|
||||
- uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./dcs_liberation.exe
|
||||
asset_name: dcs_liberation.${{ steps.version.outputs.number }}.exe
|
||||
asset_content_type: application/exe
|
||||
|
||||
|
||||
- uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,11 +5,13 @@ resources/payloads/*.lua
|
||||
venv
|
||||
logs.txt
|
||||
.DS_Store
|
||||
.vscode/settings.json
|
||||
dist/**
|
||||
a.py
|
||||
resources/tools/a.miz
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
.env
|
||||
|
||||
/kneeboards
|
||||
/liberation_preferences.json
|
||||
|
||||
6
.pre-commit-config.yaml
Normal file
6
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 20.8b1
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at khopa.studio@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
26
CONTRIBUTING.md
Normal file
26
CONTRIBUTING.md
Normal file
@@ -0,0 +1,26 @@
|
||||
First, note that we have a code of conduct, please follow it in all your interactions with the project.
|
||||
|
||||
## Contributing as a non-developer
|
||||
|
||||
* Report bugs by opening issues here on Github.
|
||||
* Help others users on Discord by answering their questions.
|
||||
* Raise awareness about the project, by making a video and/or a tutorial.
|
||||
|
||||
Should you report a bug, please use the search bar at the top of the page to see if it has already been reported.
|
||||
Note that you may need to remove the filter for open bugs if it's something we've recently fixed.
|
||||
|
||||
## Making content for Liberation
|
||||
|
||||
You can create new campaigns : See [campaign creation wiki](https://github.com/dcs-liberation/dcs_liberation/wiki/Custom-Campaigns).
|
||||
You can also improve existing campaigns.
|
||||
|
||||
You can then submit new campaigns on the "campaigns" channel on Discord, or by making a pull request if you are comfortable with git.
|
||||
|
||||
## Develop new features
|
||||
|
||||
If you want to develop a new feature, we recommend you first open an issue describing the new feature and discuss it with us on Discord before starting development.
|
||||
However, feel free to work on any existing issue.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Please submit your pull requests on the **develop** branch. We expect a description of its content, and when applicable, a reference to the issue(s) it is resolving.
|
||||
165
LICENSE
Normal file
165
LICENSE
Normal file
@@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
31
README.md
31
README.md
@@ -1,15 +1,14 @@
|
||||

|
||||
|
||||
[](https://www.paypal.com/paypalme/KhopaDCSL)
|
||||
[](https://patreon.com/khopa)
|
||||
|
||||
[](https://github.com/Khopa/dcs_liberation/releases)
|
||||
[](https://github.com/dcs-liberation/dcs_liberation/releases)
|
||||
|
||||
[](https://discord.gg/bKrtrkJ)
|
||||
|
||||
[](https://github.com/Khopa/dcs_liberation)
|
||||
[](https://github.com/Khopa/dcs_liberation/issues)
|
||||

|
||||
[](https://github.com/dcs-liberation/dcs_liberation)
|
||||
[](https://github.com/dcs-liberation/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.
|
||||
@@ -19,11 +18,29 @@ It is an external program that generates full and complex DCS missions and manag
|
||||
|
||||
## Downloads
|
||||
|
||||
Latest release is available here : https://github.com/Khopa/dcs_liberation/releases
|
||||
Latest release is available here : https://github.com/dcs-liberation/dcs_liberation/releases
|
||||
|
||||
To download preview builds of the next version of DCS Liberation, see https://github.com/dcs-liberation/dcs_liberation/wiki/Preview-builds.
|
||||
|
||||
## DCS bugs
|
||||
|
||||
These DCS bugs prevent us from improving AI behavior. Please upvote them! (But please
|
||||
_don't_ spam them with comments):
|
||||
|
||||
* [A2A and SEAD escorts don't escort](https://forums.eagle.ru/topic/251798-options-for-alternate-ai-escort-behavior/?tab=comments#comment-4668033)
|
||||
* [DEAD can't use mixed loadouts effectively](https://forums.eagle.ru/topic/271941-ai-rtbs-after-firing-decoys-despite-full-load-of-bombs/)
|
||||
|
||||
## Bugs and feature requests
|
||||
|
||||
If you need to report a bug or want to suggest a new feature, you can do this on our [bug tracker](https://github.com/dcs-liberation/dcs_liberation/issues). In either case, please use the search bar at the top of the page to see if it has already been reported. Note that you may need to remove the filter for open bugs if it's something we've recently fixed.
|
||||
|
||||
## Roadmap
|
||||
|
||||
Our plans for future releases can be found on our [Projects page](https://github.com/dcs-liberation/dcs_liberation/projects). Each planned release has a Project, and the page for that project has columns for to do, in progress, and done. Items in the Done column are in the [preview build](https://github.com/dcs-liberation/dcs_liberation/wiki/Preview-builds) for that release. Items in the To do column are planned to be added to that release.
|
||||
|
||||
## Resources
|
||||
|
||||
Tutorials, contributors and developer's guides are available in the project's [Wiki](https://github.com/Khopa/dcs_liberation/wiki/)
|
||||
Tutorials, contributors and developer's guides are available in the project's [Wiki](https://github.com/dcs-liberation/dcs_liberation/wiki/)
|
||||
|
||||
## Special Thanks
|
||||
|
||||
|
||||
329
changelog.md
329
changelog.md
@@ -1,6 +1,333 @@
|
||||
# 3.1.0
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign AI]** Fix procurement for factions that lack some unit types.
|
||||
* **[Mission Generation]** Fixed problem with mission load when control point name contained an apostrophe.
|
||||
* **[UI]** Made non-interactive map elements less obstructive.
|
||||
|
||||
# 3.0.0
|
||||
|
||||
Saves from 2.5 are not compatible with 3.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Campaign]** Ground units can now be transferred by road, airlift, and cargo ship. See https://github.com/dcs-liberation/dcs_liberation/wiki/Unit-Transfers for more information.
|
||||
* **[Campaign]** Ground units can no longer be sold. To move units to a new location, transfer them.
|
||||
* **[Campaign]** Ground units must now be recruited at a base with a factory and transferred to their destination. When buying units in the UI, the purchase will automatically be fulfilled at the closest factory, and a transfer will be created on the next turn.
|
||||
* **[Campaign]** Non-control point FOBs will no longer spawn.
|
||||
* **[Campaign]** Added squadrons and pilots. See https://github.com/dcs-liberation/dcs_liberation/wiki/Squadrons-and-pilots for more information.
|
||||
* **[Campaign]** Capturing a base now depopulates all of its attached objectives with units: air defenses, EWRs, ships, armor groups, etc. Buildings are captured.
|
||||
* **[Campaign]** Ammunition Depots determine how many ground units can be deployed on the frontline by a control point.
|
||||
* **[Campaign AI]** AI now considers Ju-88s for CAS, strike, and DEAD missions.
|
||||
* **[Campaign AI]** AI planned AEW&C missions will now be scheduled ASAP.
|
||||
* **[Campaign AI]** AI now considers the range to the SAM's threat zone rather than the range to the SAM itself when determining target priorities.
|
||||
* **[Campaign AI]** Auto purchase of ground units will now maintain unit composition instead of buying randomly. The unit composition is predefined.
|
||||
* **[Campaign AI]** Auto purchase will aim to purchase enough ground units to support the frontline, plus 30% reserve units.
|
||||
* **[Campaign AI]** Auto purchase will now adjust its air/ground balance to favor whichever is under-funded.
|
||||
* **[Flight Planner]** Desired mission length is now configurable (defaults to 60 minutes). A BARCAP will be planned every 30 minutes. Other packages will simply have their takeoffs spread out or compressed such that the last flight will take off around the mission end time.
|
||||
* **[Flight Planner]** Flight plans now include bullseye waypoints.
|
||||
* **[Flight Planner]** Differentiated SEAD and SEAD escort. SEAD is tasked with suppressing the package target, SEAD escort is tasked with protecting the package from all SAMs along its route.
|
||||
* **[Flight Planner]** Planned airspeed increased to 0.85 mach for supersonic airframes and 85% of max speed for subsonic.
|
||||
* **[Flight Planner]** Taxi time estimation for airfields increased from 5 minutes to 8 minutes.
|
||||
* **[Flight Planner]** Reduce expected error margin for flight plans from 10% to 5%.
|
||||
* **[Flight Planner]** SEAD flights are scheduled one minute ahead of the package's TOT so that they can suppress the site ahead of the strike.
|
||||
* **[Flight Planner]** Automatic ATO generation for the player's coalition can now be disabled in the settings.
|
||||
* **[Payloads]** AI flights for most air to ground mission types (CAS excluded) will have their guns emptied to prevent strafing fully armed and operational battle stations. Gun-reliant airframes like A-10s and warbirds will keep their bullets.
|
||||
* **[Kneeboard]** ATC table overflow alleviated by wrapping long airfield names and splitting ATC frequency and channel into separate rows.
|
||||
* **[UI]** Overhauled the map implementation. Now uses satellite imagery instead of low res map images. Display options have moved from the toolbar to panels in the map.
|
||||
* **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present.
|
||||
* **[UI]** DCS loadouts are now selectable in the loadout setup menu.
|
||||
* **[UI]** Added global aircraft inventory view under Air Wing dialog.
|
||||
* **[UI]** Base menu now shows information about ground unit deployment limits.
|
||||
* **[Modding]** Campaigns now choose locations for factories to spawn.
|
||||
* **[Modding]** Campaigns now choose locations for ammunition depots to spawn.
|
||||
* **[Modding]** Campaigns now use map structures as strike targets.
|
||||
* **[Modding]** Campaigns may now set *any* objective type to be a required spawn rather than random chance. Support for random objective generation was removed.
|
||||
* **[Modding]** Campaigns may now place AAA objectives.
|
||||
* **[Modding]** Can now install custom factions to <DCS saved games>/Liberation/Factions instead of the Liberation install directory.
|
||||
* **[Performance Settings]** Added a settings to lower the number of smoke effects generated on frontlines. Lowered default settings for frontline smoke generators, so less smoke should be generated by default.
|
||||
* **[Configuration]** Liberation preferences (DCS install and save game location) are now saved to `%LOCALAPPDATA%/DCSLiberation` to prevent needing to reconfigure each new install.
|
||||
* **[Skynet]** Updated to 2.1.0.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign AI]** Fix purchase of aircraft by priority (the faction's list was being used as the priority list rather than the game's).
|
||||
* **[Campaign AI]** Fixed bug causing AI to over-purchase cheap aircraft.
|
||||
* **[Campaign AI]** Auto planner will no longer attempt to plan missions for which the faction has no compatible aircraft.
|
||||
* **[Campaign AI]** Stop purchasing aircraft after the first unaffordable package to attempt to complete more packages rather than filling airfields with cheap escorts that will never be used.
|
||||
* **[Campaign]** Fixed bug where offshore strike locations were being used to spawn ship objectives.
|
||||
* **[Campaign]** EWR sites are now purchasable.
|
||||
* **[Flight Planner]** AI strike flight plans now include the correct target actions for building groups.
|
||||
* **[Flight Planner]** AI BAI/DEAD/SEAD flights now have tasks to attack all groups at the target location, not just the primary group (for multi-group SAM sites).
|
||||
* **[Flight Planner]** Fixed some contexts where damaged runways would be used. Destroying a carrier will no longer break the game.
|
||||
|
||||
# 2.5.1
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[UI]** Engagement ranges are now displayed by default.
|
||||
* **[UI]** Engagement range display generalized to work for all patrolling flight plans (BARCAP, TARCAP, and CAS).
|
||||
* **[Flight Planner]** Front lines no longer project threat zones to avoid pushing BARCAPs back so much. TARCAPs will be forcibly planned but strike packages will not route around front lines even if it is reasonable to do so.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaigns]** EWRs associated with a base will now only be generated near the base.
|
||||
* **[Flight Planner]** Fixed error when generating AEW&C flight plans in campaigns with no front lines.
|
||||
|
||||
# 2.5.0
|
||||
|
||||
Saves from 2.4 are not compatible with 2.5.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** DCS 2.7 Support
|
||||
* **[UI]** Improved FOB menu, added a custom banner, and do not display aircraft recruitment menu
|
||||
* **[Flight Planner]** Added AEW&C missions. (by siKruger)
|
||||
* **[Kneeboard]** Added dark kneeboard option (by GvonH)
|
||||
* **[Campaigns]** Multiple EWR sites may now be generated, and EWR sites may be generated outside bases (by SnappyComebacks)
|
||||
* **[Mission Generation]** Cloudy and rainy (but not thunderstorm) weather will use the cloud presets from DCS 2.7.
|
||||
* **[Plugins]** Added LotATC export plugin (by drsoran)
|
||||
* **[Plugins]** Added Splash Damage Plugin (by Wheelijoe)
|
||||
* **[Loadouts]** Replaced Litening with ATFLIR for all default F/A-18C loadouts.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Flight Planner]** Front lines now project threat zones, so TARCAP/escorts will not be pruned for flights near the front. Packages may also route around the front line when practical.
|
||||
* **[Flight Planner]** Fixed error when planning BAI at SAMs with dead subgroups.
|
||||
* **[Flight Planner]** Mig-19 was not allowed for CAS roles fixed
|
||||
* **[Flight Planner]** Increased size of navigation planning area to avoid plannign failures with distant waypoints.
|
||||
* **[Flight Planner]** Fixed UI refresh when unchecking the "default loadout" box in the loadout editor.
|
||||
* **[Objective names]** Fixed typos in objective name : ARMADILLLO -> ARMADILLO (by SnappyComebacks)
|
||||
* **[Payloads]** F-86 Sabre was missing a custom payload
|
||||
* **[Payloads]** Added GAR-8 period restrictions (by Mustang-25)
|
||||
* **[Campaign]** Date now progresses.
|
||||
* **[Campaign]** Added game over message when a coalition runs out of functioning airbases.
|
||||
* **[Mission Generation]** Fixed "invalid face handle" error in kneeboard generation that occurred on some machines.
|
||||
|
||||
## Regressions
|
||||
|
||||
* **[Mod Support]** Stopped support for 2.5.5 Rafale Mode, and removed factions that were using it
|
||||
* **[Mod Support]** Su-57 mod support might be out of date
|
||||
|
||||
# 2.4.3
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[New Game Wizard]** Added the possibility to setup custom start date
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Mods]** Updated C-130J mod data to version 6.4
|
||||
* **[Mods]** Updated F-22A mod to latest version
|
||||
|
||||
# 2.4.2
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Factions]** Introduction dates and fallback weapons added for US, Russian, UK, and French weapons. Huge thanks to @TheCandianVendingMachine for the massive amount of data entry!
|
||||
* **[Campaigns]** Added 1995 start dates.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Economy]** Pending ground unit purchases will also be transferred when a connected base is captured.
|
||||
* **[UI]** Fixed rounding of budget in recruitment menu.
|
||||
|
||||
# 2.4.1
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Units]** Fixed syntax error with the SH-60B payload file.
|
||||
* **[Culling]** Missile sites generate reasonably sized non-cull zones rather than 100km ones.
|
||||
* **[UI]** Budget display is also now rounded to 2 decimal places.
|
||||
* **[UI]** Fixed some areas where the old, non-pretty name was displayed to users.
|
||||
|
||||
# 2.4.0
|
||||
|
||||
Saves from 2.3 are not compatible with 2.4.
|
||||
|
||||
## Highlights
|
||||
|
||||
* Improved flight plan generation to avoid loitering in or traveling through threatened areas when practical.
|
||||
* Improved AI aircraft purchasing behavior.
|
||||
* Era-restricted weapons (work in progress).
|
||||
* Tons of UI polish.
|
||||
* Rebalanced economy to keep opfor competitive over the course of the game.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Flight Planner]** Air-to-air and SEAD escorts will no longer be automatically planned for packages that are not in range of threats.
|
||||
* **[Flight Planner]** Non-custom flight plans will now navigate around threat areas en route to the target area when practical.
|
||||
* **[Flight Planner]** Flight plans along front lines now ensure that the race track start is closer to the departure airfield than the race track end.
|
||||
* **[Campaign AI]** Auto-purchase now prefers airfields that are not within range of the enemy.
|
||||
* **[Campaign AI]** Auto-purchase now prefers the best aircraft for the task, but will attempt to maintain some variety.
|
||||
* **[Campaign AI]** Opfor now sells off odd aircraft since they're unlikely to be used.
|
||||
* **[Campaign AI]** Multiple rounds of CAP will be planned (roughly 90 minutes of coverage). Default starting budget has increased to account for the increased need for aircraft.
|
||||
* **[Mission Generator]** Multiple groups are created for complex SAM sites (SAMs with additional point defense or SHORADS), improving Skynet behavior.
|
||||
* **[Mission Generator]** Default start type can now be chosen in the settings. This replaces the non-functional "AI Parking Start" option. **Selecting any type other than cold will break OCA/Aircraft missions.**
|
||||
* **[Cheat Menu]** Added ability to toggle base capture and frontline advance/retreat cheats.
|
||||
* **[Skynet]** Updated to 2.0.1.
|
||||
* **[Skynet]** Point defenses are now configured to remain on to protect the site they accompany.
|
||||
* **[Hercules]** Updated the Hercules Cargo list file.
|
||||
* **[Balance]** Opfor now gains income using the same rules as the player, significantly increasing their income relative to the player for most campaigns.
|
||||
* **[Balance]** Units now retreat from captured bases when able. Units with no retreat path will be captured and sold.
|
||||
* **[Economy]** FOBs generate only $10M per turn (previously $20M like airbases).
|
||||
* **[Economy]** Carriers and off-map spawns generate no income (previously $20M like airbases).
|
||||
* **[Economy]** Sales of aircraft and ground vehicles can now be cancelled before the next turn begins.
|
||||
* **[UI]** Multi-SAM objectives now show threat and detection rings per group.
|
||||
* **[UI]** New icon for AA sites with no active threat.
|
||||
* **[UI]** Unit names are now prettier and more accurate, and can now be set per-country for added historical flavour.
|
||||
* **[UI]** Default loadout is now shown for flights with no custom loadout selected.
|
||||
* **[UI]** Aircraft for a new flight are now only selectable if they match the task type for that flight.
|
||||
* **[UI]** WIP - There is now a unit info button for each unit in the recruitment list, that should help newer players learn what each unit does.
|
||||
* **[UI]** Docs for time-on-target and creating new theaters/factions/loadouts are now linked in the UI at the appropriate places.
|
||||
* **[UI]** ASAP is now a checkbox rather than a button. Enabling this will disable the TOT selector but changes to the package structure will automatically re-ASAP the package.
|
||||
* **[UI]** Arrival airfield is now shown in the flight list if it differs from the departure airfield.
|
||||
* **[UI]** Start type can now be selected when creating a flight.
|
||||
* **[UI]** Arrival and divert airfields can be edited after the flight is created.
|
||||
* **[Factions]** Added option for date-based loadout restriction. Active radar homing missiles are handled, patches welcome for the other thousand weapons.
|
||||
* **[Factions]** Added Poland 2010 faction.
|
||||
* **[Factions]** Added Greece 2005 faction.
|
||||
* **[Factions]** Added Iran 1988 faction.
|
||||
* **[Units]** Support for E-2 Hawkeye, SH-60B Seahawk, S-3B Viking (thanks to awinterquest) and SpGH Dana - these are now being used by appropriate factions.
|
||||
* **[Culling]** Missile sites are no longer culled.
|
||||
* **[Campaigns]** Added campaign "Black Sea Lite" by Starfire
|
||||
* **[Campaigns]** Added campaign "Exercise Vegas Nerve" by Starfire
|
||||
* **[New game Wizard]** The theater page is now the first page of the campaign wizard, recommended factions will be selected automatically on the faction selection page
|
||||
* **[New game Wizard]** Added information text about the selected campaign performance.
|
||||
* **[Mod Support]** Added support for High Digit SAMs mod 1.4.0
|
||||
* **[Mod Support]** Added SAMs sites generator : KS19Generator, SA10BGenerator, SA12Generator, SA17Generator, SA20Generator, SA20BGenerator, SA23Generator
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Hercules]** Updated the default Hercules radio frequency.
|
||||
* **[Economy]** Pending unit orders at captured bases will be refunded.
|
||||
* **[UI]** Carrier group SAM threat rings now move with the carrier.
|
||||
* **[UI]** Base intel menu no longer compresses text, and is now scrollable.
|
||||
* **[UI]** Edit Flight window is now dynamically sized to adapt to the width of waypoint names, so they no longer get truncated.
|
||||
* **[UI]** Budget income display is now rounded to 2 decimal places.
|
||||
* **[UI]** Fixed incorrect income per turn displayed for strike target tooltip.
|
||||
* **[Factions]** USA with C-130 faction now links to the required mod.
|
||||
* **[Campaign]** Fixed issue where destroyed buildings would sometimes not count as destroyed and thus respawn.
|
||||
* **[Campaign]** Fixed issue where destroyed runways were not registered.
|
||||
* **[Units]** J-11A is no longer spawned with empty loadout.
|
||||
* **[Units]** F-14B is no longer spawned with empty loadout for fighter sweep tasks.
|
||||
* **[Units]** Pyotr Velikiy cruiser has been removed for now as it's nearly unkillable.
|
||||
* **[Units]** Submarines have been removed for now as they aren't wholly functional.
|
||||
* **[Units]** Fixed "FACTION ERROR : Unable to find OliverHazardPerryGroupGenerator in pydcs" error at startup.
|
||||
* **[Mission Generator]** Fixed a bug where units set to Aggressive stance sometimes did not move.
|
||||
* **[Mission Generator]** Flyover points for OCA/Aircraft missions are now generated correctly.
|
||||
* **[Flight Planner]** Fixed not being able to create custom waypoints for buildings.
|
||||
* **[Flight Planner]** Strike missions will no longer be automatically planned against SAMs.
|
||||
* **[Flight Planner]** Strike missions will no longer be automatically planned against FOB structures.
|
||||
|
||||
# 2.3.4
|
||||
|
||||
## Fixes:
|
||||
[Mission Generator] Mission generator would crash when generating fire missions for destroyed SCUD sites - fixed
|
||||
|
||||
# 2.3.3
|
||||
|
||||
## Features/Improvements
|
||||
* **[Campaigns]** Reworked Golan Heights campaign on Syria, (Added FOB and preset locations for SAMS)
|
||||
* **[Campaigns]** Added a lite version of the Golan Heights campaign
|
||||
* **[Campaigns]** Reworked Syrian Civil War campaign (Added FOB and preset locations for SAMS)
|
||||
* **[Campaigns]** Reworked Emirates campaign
|
||||
* **[Campaigns]** AA units added to frontlines and updated all factions to include some frontline AA units.
|
||||
* **[Mission Generator]** Infantry will only be generated for APC and IFV groups
|
||||
* **[Mission Generator]** Infantry squads size is not randomized anymore
|
||||
* **[Mission Generator]** Infantry squads can have a mortar.
|
||||
* **[Mission Generator]** SCUD missiles sites will now fire on enemy controls points in range when possible
|
||||
* **[Factions]** Updated Nato Desert Storm to include F-14A
|
||||
* **[Factions]** Updated Iraq 1991 factions to include Zsu-57 and Mig-29A
|
||||
* **[Factions]** Germany 1944, added Stug III and Stug IV
|
||||
* **[Factions]** Added factions Insurgents (Hard) with better and more weapons
|
||||
* **[Plugins]** [The EWRS plugin](https://github.com/Bob7heBuilder/EWRS) is now included.
|
||||
* **[UI]** Added enemy intelligence summary and details window.
|
||||
|
||||
## Fixes:
|
||||
* **[Factions]** AI would never buy artillery units for the frontline - fixed
|
||||
* **[Factions]** Removed the F-111 unit from the NATO desert storm faction. (Recruiting it would cause crashes in DCS, since it is not a valid unit)
|
||||
* **[Campaign]** Automatic redeployment of ground units would sometimes fail - fixed
|
||||
* **[Mission Generator]** Artillery groups would retreat in the wrong direction - fixed
|
||||
* **[Units]** Fixed SPG_Stryker_M1128_MGS not being in db
|
||||
* **[UI]** Fixed and added many missing ground units icons
|
||||
* **[UI]** Ship groups could be replaced by SAM sites in the UI, which would lead to broken mission being generated - fixed
|
||||
* **[New Game Wizard]** Removed the "mid game" campaign generator option which is currently broken
|
||||
* **[Mission Generator]** Empty navy groups will no longer be generated
|
||||
* **[Mission Generator]** Fixed BAI, SEAD, and DEAD flights ocassionally being assigned the wrong targets.
|
||||
* **[Flight Planner]** Fixed not being able to plan packages against opfor carriers
|
||||
* **[UI]** Repaired SAMs no longer show as dead.
|
||||
* **[UI]** Fixed not being able to manage a disbanded site after disbanding and closing the base menu.
|
||||
|
||||
# 2.3.2
|
||||
|
||||
## Features/Improvements
|
||||
* **[Units]** Support for newly added BTR-82A, T-72B3
|
||||
* **[Units]** Added ZSU-57 AAA sites
|
||||
* **[Culling]** BARCAP missions no longer create culling exclusion zones.
|
||||
* **[Flight Planner]** Improved TOT planning. Negative start times no longer occur with TARCAPs and hold times no longer affect planning for flight plans without hold points.
|
||||
* **[Factions]** Added Iraq 1991 faction (thanks again to Hawkmoon!)
|
||||
|
||||
## Fixes:
|
||||
* **[Mission Generator]** Fix mission generation error when there are too many radio frequency to setup for the Mig-21
|
||||
* **[Mission Generator]** Fix ground units not moving forward
|
||||
* **[Mission Generator]** Fixed assigned radio channels overlapping with beacons.
|
||||
* **[Flight Planner]** Fix creation of custom waypoints.
|
||||
* **[Campaigns]** Fixed many cases of SAMs spawning on the runways/taxiways in Syria Full.
|
||||
|
||||
# 2.3.1
|
||||
|
||||
## Features/Improvements
|
||||
* **[UX]** Added a warning message when the player is attempting to buy more planes at an already full airbase.
|
||||
* **[Campaigns]** Migrated Syria full map to new format. (Thanks to Hawkmoon)
|
||||
* **[Faction]** Added NATO desert Storm faction (Thanks to Hawkmoon)
|
||||
|
||||
## Fixes:
|
||||
* **[AI]** CAP flights will engage enemies again.
|
||||
* **[Campaigns]** Fixed a missing path on the Caucasus Full Map campaign
|
||||
|
||||
# 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 procurement 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
|
||||
* **[Mission Generator]** Support for Anubis C-130 Hercules mod
|
||||
* **[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
|
||||
* **[UI]** Added toolbar buttons to change map display settings
|
||||
* **[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
|
||||
## 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.
|
||||
|
||||
@@ -2,20 +2,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.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||
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.SPAAA_ZU_23_2_Mounted_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Insurgent_Closed_Emplacement,
|
||||
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Insurgent_Emplacement,
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
AirDefence.AAA_Flak_38,
|
||||
AirDefence.AAA_Flak_38_20mm,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
AirDefence.AAA_8_8cm_Flak_37,
|
||||
AirDefence.AAA_Flak_Vierling_38,
|
||||
AirDefence.AAA_Kdo_G_40,
|
||||
AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
|
||||
AirDefence.AAA_SP_Kdo_G_40,
|
||||
AirDefence.AAA_8_8cm_Flak_41,
|
||||
AirDefence.AAA_Bofors_40mm
|
||||
]
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
]
|
||||
|
||||
40
game/data/alic.py
Normal file
40
game/data/alic.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from dcs.unit import Unit
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
|
||||
class AlicCodes:
|
||||
CODES = {
|
||||
AirDefence.EWR_1L13.id: 101,
|
||||
AirDefence.EWR_55G6.id: 102,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR.id: 103,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR.id: 104,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR.id: 107,
|
||||
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR.id: 108,
|
||||
AirDefence.MCC_SR_Sborka_Dog_Ear_SR.id: 109,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR.id: 110,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL.id: 115,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL.id: 117,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL.id: 118,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet.id: 119,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison.id: 120,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id: 121,
|
||||
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3.id: 122,
|
||||
AirDefence.SAM_SA_3_S_125_Low_Blow_TR.id: 123,
|
||||
AirDefence.SAM_Rapier_Blindfire_TR.id: 124,
|
||||
AirDefence.SAM_Rapier_LN.id: 125,
|
||||
AirDefence.SAM_SA_2_S_75_Fan_Song_TR.id: 126,
|
||||
AirDefence.HQ_7_Self_Propelled_LN.id: 127,
|
||||
AirDefence.HQ_7_Self_Propelled_STR.id: 128,
|
||||
AirDefence.SAM_Roland_ADS.id: 201,
|
||||
AirDefence.SAM_Patriot_STR.id: 202,
|
||||
AirDefence.SAM_Hawk_SR__AN_MPQ_50.id: 203,
|
||||
AirDefence.SAM_Hawk_TR__AN_MPQ_46.id: 204,
|
||||
AirDefence.SAM_Roland_EWR.id: 205,
|
||||
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55.id: 206,
|
||||
AirDefence.SPAAA_Gepard.id: 207,
|
||||
AirDefence.SPAAA_Vulcan_M163.id: 208,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def code_for(cls, unit: Unit) -> int:
|
||||
return cls.CODES[unit.type]
|
||||
@@ -1,17 +1,63 @@
|
||||
import inspect
|
||||
import dcs
|
||||
|
||||
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick']
|
||||
DEFAULT_AVAILABLE_BUILDINGS = [
|
||||
"fuel",
|
||||
"comms",
|
||||
"oil",
|
||||
"ware",
|
||||
"farp",
|
||||
"power",
|
||||
"derrick",
|
||||
]
|
||||
|
||||
WW2_FREE = ['fuel', 'factory', 'ware']
|
||||
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp']
|
||||
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp']
|
||||
WW2_FREE = ["fuel", "ware"]
|
||||
WW2_GERMANY_BUILDINGS = [
|
||||
"fuel",
|
||||
"ww2bunker",
|
||||
"ww2bunker",
|
||||
"ww2bunker",
|
||||
"allycamp",
|
||||
"allycamp",
|
||||
]
|
||||
WW2_ALLIES_BUILDINGS = [
|
||||
"fuel",
|
||||
"allycamp",
|
||||
"allycamp",
|
||||
"allycamp",
|
||||
"allycamp",
|
||||
"allycamp",
|
||||
]
|
||||
|
||||
FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2',
|
||||
'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5',
|
||||
'Haystack 1', 'Haystack 2', 'Haystack 3', 'Haystack 4', 'Hemmkurvenvenhindernis',
|
||||
'Log posts 1', 'Log posts 2', 'Log posts 3', 'Log ramps 1', 'Log ramps 2', 'Log ramps 3',
|
||||
'Belgian Gate', 'Container white']
|
||||
FORTIFICATION_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)]
|
||||
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)
|
||||
]
|
||||
|
||||
@@ -16,7 +16,7 @@ from dcs.planes import (
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW
|
||||
SpitfireLFMkIXCW,
|
||||
)
|
||||
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
@@ -26,7 +26,6 @@ This list contains the aircraft that do not use the guns as the last resort weap
|
||||
They'll RTB when they don't have gun ammo left
|
||||
"""
|
||||
GUNFIGHTERS = [
|
||||
|
||||
# Cold War
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
@@ -34,11 +33,9 @@ GUNFIGHTERS = [
|
||||
F_86F_Sabre,
|
||||
A_4E_C,
|
||||
F_5E_3,
|
||||
|
||||
# Trainers
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
|
||||
# WW2
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
@@ -51,5 +48,4 @@ GUNFIGHTERS = [
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
I_16,
|
||||
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from dcs.task import Reconnaissance
|
||||
|
||||
from game.utils import nm_to_meter, feet_to_meter
|
||||
from game.utils import Distance, feet, nautical_miles
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroundUnitProcurementRatios:
|
||||
ratios: dict[GroundUnitClass, float]
|
||||
|
||||
def for_unit_class(self, unit_class: GroundUnitClass) -> float:
|
||||
try:
|
||||
return self.ratios[unit_class] / sum(self.ratios.values())
|
||||
except KeyError:
|
||||
return 0.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -12,30 +25,46 @@ class Doctrine:
|
||||
strike: bool
|
||||
antiship: bool
|
||||
|
||||
strike_max_range: int
|
||||
sead_max_range: int
|
||||
rendezvous_altitude: Distance
|
||||
hold_distance: Distance
|
||||
push_distance: Distance
|
||||
join_distance: Distance
|
||||
split_distance: Distance
|
||||
ingress_egress_distance: Distance
|
||||
ingress_altitude: Distance
|
||||
egress_altitude: Distance
|
||||
|
||||
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
|
||||
min_patrol_altitude: Distance
|
||||
max_patrol_altitude: Distance
|
||||
pattern_altitude: Distance
|
||||
|
||||
#: The duration that CAP flights will remain on-station.
|
||||
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
|
||||
|
||||
#: The minimum length of the CAP race track.
|
||||
cap_min_track_length: Distance
|
||||
|
||||
#: The maximum length of the CAP race track.
|
||||
cap_max_track_length: Distance
|
||||
|
||||
#: The minimum distance between the defended position and the *end* of the
|
||||
#: CAP race track.
|
||||
cap_min_distance_from_cp: Distance
|
||||
|
||||
#: The maximum distance between the defended position and the *end* of the
|
||||
#: CAP race track.
|
||||
cap_max_distance_from_cp: Distance
|
||||
|
||||
#: The engagement range of CAP flights. Any enemy aircraft within this range
|
||||
#: of the CAP's current position will be engaged by the CAP.
|
||||
cap_engagement_range: Distance
|
||||
|
||||
cas_duration: timedelta
|
||||
|
||||
sweep_distance: Distance
|
||||
|
||||
ground_unit_procurement_ratios: GroundUnitProcurementRatios
|
||||
|
||||
|
||||
MODERN_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
@@ -43,25 +72,36 @@ MODERN_DOCTRINE = Doctrine(
|
||||
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),
|
||||
rendezvous_altitude=feet(25000),
|
||||
hold_distance=nautical_miles(15),
|
||||
push_distance=nautical_miles(20),
|
||||
join_distance=nautical_miles(20),
|
||||
split_distance=nautical_miles(20),
|
||||
ingress_egress_distance=nautical_miles(45),
|
||||
ingress_altitude=feet(20000),
|
||||
egress_altitude=feet(20000),
|
||||
min_patrol_altitude=feet(15000),
|
||||
max_patrol_altitude=feet(33000),
|
||||
pattern_altitude=feet(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),
|
||||
cap_min_track_length=nautical_miles(15),
|
||||
cap_max_track_length=nautical_miles(40),
|
||||
cap_min_distance_from_cp=nautical_miles(10),
|
||||
cap_max_distance_from_cp=nautical_miles(40),
|
||||
cap_engagement_range=nautical_miles(50),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(60),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
GroundUnitClass.Tank: 3,
|
||||
GroundUnitClass.Atgm: 2,
|
||||
GroundUnitClass.Apc: 2,
|
||||
GroundUnitClass.Ifv: 3,
|
||||
GroundUnitClass.Artillery: 1,
|
||||
GroundUnitClass.Shorads: 2,
|
||||
GroundUnitClass.Recon: 1,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
COLDWAR_DOCTRINE = Doctrine(
|
||||
@@ -70,25 +110,36 @@ COLDWAR_DOCTRINE = Doctrine(
|
||||
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),
|
||||
rendezvous_altitude=feet(22000),
|
||||
hold_distance=nautical_miles(10),
|
||||
push_distance=nautical_miles(10),
|
||||
join_distance=nautical_miles(10),
|
||||
split_distance=nautical_miles(10),
|
||||
ingress_egress_distance=nautical_miles(30),
|
||||
ingress_altitude=feet(18000),
|
||||
egress_altitude=feet(18000),
|
||||
min_patrol_altitude=feet(10000),
|
||||
max_patrol_altitude=feet(24000),
|
||||
pattern_altitude=feet(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),
|
||||
cap_min_track_length=nautical_miles(12),
|
||||
cap_max_track_length=nautical_miles(24),
|
||||
cap_min_distance_from_cp=nautical_miles(8),
|
||||
cap_max_distance_from_cp=nautical_miles(25),
|
||||
cap_engagement_range=nautical_miles(35),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(40),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
GroundUnitClass.Tank: 4,
|
||||
GroundUnitClass.Atgm: 2,
|
||||
GroundUnitClass.Apc: 3,
|
||||
GroundUnitClass.Ifv: 2,
|
||||
GroundUnitClass.Artillery: 1,
|
||||
GroundUnitClass.Shorads: 2,
|
||||
GroundUnitClass.Recon: 1,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
WWII_DOCTRINE = Doctrine(
|
||||
@@ -97,23 +148,33 @@ WWII_DOCTRINE = Doctrine(
|
||||
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),
|
||||
hold_distance=nautical_miles(5),
|
||||
push_distance=nautical_miles(5),
|
||||
join_distance=nautical_miles(5),
|
||||
split_distance=nautical_miles(5),
|
||||
rendezvous_altitude=feet(10000),
|
||||
ingress_egress_distance=nautical_miles(7),
|
||||
ingress_altitude=feet(8000),
|
||||
egress_altitude=feet(8000),
|
||||
min_patrol_altitude=feet(4000),
|
||||
max_patrol_altitude=feet(15000),
|
||||
pattern_altitude=feet(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),
|
||||
cap_min_track_length=nautical_miles(8),
|
||||
cap_max_track_length=nautical_miles(18),
|
||||
cap_min_distance_from_cp=nautical_miles(0),
|
||||
cap_max_distance_from_cp=nautical_miles(5),
|
||||
cap_engagement_range=nautical_miles(20),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(10),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
GroundUnitClass.Tank: 3,
|
||||
GroundUnitClass.Atgm: 3,
|
||||
GroundUnitClass.Apc: 3,
|
||||
GroundUnitClass.Artillery: 1,
|
||||
GroundUnitClass.Shorads: 3,
|
||||
GroundUnitClass.Recon: 1,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
239
game/data/groundunitclass.py
Normal file
239
game/data/groundunitclass.py
Normal file
@@ -0,0 +1,239 @@
|
||||
from enum import unique, Enum
|
||||
from typing import Type
|
||||
|
||||
from dcs.vehicles import AirDefence, Infantry, Unarmed, Artillery, Armor
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from pydcs_extensions.frenchpack import frenchpack
|
||||
|
||||
|
||||
@unique
|
||||
class GroundUnitClass(Enum):
|
||||
Tank = (
|
||||
"Tank",
|
||||
(
|
||||
Armor.MBT_T_55,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.MBT_T_72B3,
|
||||
Armor.MBT_T_80U,
|
||||
Armor.MBT_T_90,
|
||||
Armor.MBT_Leopard_2A4,
|
||||
Armor.MBT_Leopard_2A4_Trs,
|
||||
Armor.MBT_Leopard_2A5,
|
||||
Armor.MBT_Leopard_2A6M,
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.MBT_Leclerc,
|
||||
Armor.MBT_Challenger_II,
|
||||
Armor.MBT_Chieftain_Mk_3,
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.MBT_Merkava_IV,
|
||||
Armor.ZTZ_96B,
|
||||
# WW2
|
||||
# Axis
|
||||
Armor.Tk_PzIV_H,
|
||||
Armor.SPG_Sturmpanzer_IV_Brummbar,
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||
Armor.HT_Pz_Kpfw_VI_Tiger_I,
|
||||
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
|
||||
# Allies
|
||||
Armor.Tk_M4_Sherman,
|
||||
Armor.CT_Centaur_IV,
|
||||
Armor.CT_Cromwell_IV,
|
||||
Armor.HIT_Churchill_VII,
|
||||
# Mods
|
||||
frenchpack.DIM__TOYOTA_BLUE,
|
||||
frenchpack.DIM__TOYOTA_GREEN,
|
||||
frenchpack.DIM__TOYOTA_DESERT,
|
||||
frenchpack.DIM__KAMIKAZE,
|
||||
frenchpack.AMX_30B2,
|
||||
frenchpack.Leclerc_Serie_XXI,
|
||||
),
|
||||
)
|
||||
|
||||
Atgm = (
|
||||
"ATGM",
|
||||
(
|
||||
Armor.ATGM_HMMWV,
|
||||
Armor.ATGM_VAB_Mephisto,
|
||||
Armor.ATGM_Stryker,
|
||||
Armor.IFV_BMP_2,
|
||||
# WW2 (Tank Destroyers)
|
||||
# Axxis
|
||||
Armor.SPG_StuG_III_Ausf__G,
|
||||
Armor.SPG_StuG_IV,
|
||||
Armor.SPG_Jagdpanzer_IV,
|
||||
Armor.SPG_Jagdpanther_G1,
|
||||
Armor.SPG_Sd_Kfz_184_Elefant,
|
||||
# Allies
|
||||
Armor.SPG_M10_GMC,
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
# Mods
|
||||
frenchpack.VBAE_CRAB_MMP,
|
||||
frenchpack.VAB_MEPHISTO,
|
||||
frenchpack.TRM_2000_PAMELA,
|
||||
),
|
||||
)
|
||||
|
||||
Ifv = (
|
||||
"IFV",
|
||||
(
|
||||
Armor.IFV_BMP_3,
|
||||
Armor.IFV_BMP_2,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.IFV_Marder,
|
||||
Armor.IFV_Warrior,
|
||||
Armor.SPG_Stryker_MGS,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.ZBD_04A,
|
||||
# Mods
|
||||
frenchpack.VBAE_CRAB,
|
||||
frenchpack.VAB_T20_13,
|
||||
),
|
||||
)
|
||||
|
||||
Apc = (
|
||||
"APC",
|
||||
(
|
||||
Armor.IFV_M1126_Stryker_ICV,
|
||||
Armor.APC_M113,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.IFV_BTR_82A,
|
||||
Armor.APC_MTLB,
|
||||
Armor.APC_AAV_7_Amphibious,
|
||||
Armor.APC_TPz_Fuchs,
|
||||
Armor.APC_BTR_RD,
|
||||
# WW2
|
||||
Armor.APC_M2A1_Halftrack,
|
||||
Armor.APC_Sd_Kfz_251_Halftrack,
|
||||
# Mods
|
||||
frenchpack.VAB__50,
|
||||
frenchpack.VBL__50,
|
||||
frenchpack.VBL_AANF1,
|
||||
),
|
||||
)
|
||||
|
||||
Artillery = (
|
||||
"Artillery",
|
||||
(
|
||||
Artillery.Grad_MRL_FDDM__FC,
|
||||
Artillery.MLRS_9A52_Smerch_HE_300mm,
|
||||
Artillery.SPH_2S1_Gvozdika_122mm,
|
||||
Artillery.SPH_2S3_Akatsia_152mm,
|
||||
Artillery.MLRS_BM_21_Grad_122mm,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27_220mm,
|
||||
Artillery.SPH_M109_Paladin_155mm,
|
||||
Artillery.MLRS_M270_227mm,
|
||||
Artillery.SPM_2S9_Nona_120mm_M,
|
||||
Artillery.SPH_Dana_vz77_152mm,
|
||||
Artillery.SPH_T155_Firtina_155mm,
|
||||
Artillery.PLZ_05,
|
||||
Artillery.SPH_2S19_Msta_152mm,
|
||||
Artillery.MLRS_9A52_Smerch_CM_300mm,
|
||||
# WW2
|
||||
Artillery.SPG_M12_GMC_155mm,
|
||||
),
|
||||
)
|
||||
|
||||
Logistics = (
|
||||
"Logistics",
|
||||
(
|
||||
Unarmed.Carrier_M30_Cargo,
|
||||
Unarmed.Truck_M818_6x6,
|
||||
Unarmed.Truck_KAMAZ_43101,
|
||||
Unarmed.Truck_Ural_375,
|
||||
Unarmed.Truck_GAZ_66,
|
||||
Unarmed.Truck_GAZ_3307,
|
||||
Unarmed.Truck_GAZ_3308,
|
||||
Unarmed.Truck_Ural_4320_31_Arm_d,
|
||||
Unarmed.Truck_Ural_4320T,
|
||||
Unarmed.Truck_Opel_Blitz,
|
||||
Unarmed.LUV_Kubelwagen_82,
|
||||
Unarmed.Carrier_Sd_Kfz_7_Tractor,
|
||||
Unarmed.LUV_Kettenrad,
|
||||
Unarmed.Car_Willys_Jeep,
|
||||
Unarmed.LUV_Land_Rover_109,
|
||||
Unarmed.Truck_Land_Rover_101_FC,
|
||||
# Mods
|
||||
frenchpack.VBL,
|
||||
frenchpack.VAB,
|
||||
),
|
||||
)
|
||||
|
||||
Recon = (
|
||||
"Recon",
|
||||
(
|
||||
Armor.Scout_HMMWV,
|
||||
Armor.Scout_Cobra,
|
||||
Armor.LT_PT_76,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.Scout_BRDM_2,
|
||||
# WW2
|
||||
Armor.LT_Mk_VII_Tetrarch,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.Car_M8_Greyhound_Armored,
|
||||
Armor.Car_Daimler_Armored,
|
||||
# Mods
|
||||
frenchpack.ERC_90,
|
||||
frenchpack.AMX_10RCR,
|
||||
frenchpack.AMX_10RCR_SEPAR,
|
||||
),
|
||||
)
|
||||
|
||||
Infantry = (
|
||||
"Infantry",
|
||||
(
|
||||
Infantry.Insurgent_AK_74,
|
||||
Infantry.Infantry_AK_74,
|
||||
Infantry.Infantry_M1_Garand,
|
||||
Infantry.Infantry_Mauser_98,
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
Infantry.Infantry_M4_Georgia,
|
||||
Infantry.Infantry_AK_74_Rus,
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Infantry_M249,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Infantry_RPG,
|
||||
),
|
||||
)
|
||||
|
||||
Shorads = (
|
||||
"SHORADS",
|
||||
(
|
||||
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
|
||||
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
|
||||
AirDefence.SPAAA_ZSU_57_2,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
AirDefence.SAM_Linebacker___Bradley_M6,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
AirDefence.SAM_Avenger__Stinger,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
AirDefence.AAA_8_8cm_Flak_37,
|
||||
AirDefence.AAA_8_8cm_Flak_41,
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
AirDefence.AAA_M1_37mm,
|
||||
AirDefence.AAA_QF_3_7,
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, class_name: str, unit_list: tuple[Type[VehicleType], ...]
|
||||
) -> None:
|
||||
self.class_name = class_name
|
||||
self.unit_list = unit_list
|
||||
|
||||
def __contains__(self, unit_type: Type[VehicleType]) -> bool:
|
||||
return unit_type in self.unit_list
|
||||
@@ -1,6 +1,6 @@
|
||||
from dcs.ships import (
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
CG_1164_Moskva,
|
||||
Battlecruiser_1144_2_Pyotr_Velikiy,
|
||||
Cruiser_1164_Moskva,
|
||||
CVN_70_Carl_Vinson,
|
||||
CVN_71_Theodore_Roosevelt,
|
||||
CVN_72_Abraham_Lincoln,
|
||||
@@ -8,67 +8,101 @@ from dcs.ships import (
|
||||
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,
|
||||
Frigate_11540_Neustrashimy,
|
||||
Corvette_1124_4_Grisha,
|
||||
Frigate_1135M_Rezky,
|
||||
Corvette_1241_1_Molniya,
|
||||
LHA_1_Tarawa,
|
||||
Oliver_Hazzard_Perry_class,
|
||||
Ticonderoga_class,
|
||||
FFG_Oliver_Hazzard_Perry,
|
||||
CG_Ticonderoga,
|
||||
Type_052B_Destroyer,
|
||||
Type_052C_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
)
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
UNITS_WITH_RADAR = [
|
||||
TELARS = {
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
}
|
||||
|
||||
# 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,
|
||||
TRACK_RADARS = {
|
||||
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
|
||||
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
|
||||
AirDefence.SAM_Hawk_TR__AN_MPQ_46,
|
||||
AirDefence.SAM_Patriot_STR,
|
||||
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
|
||||
AirDefence.SAM_Rapier_Blindfire_TR,
|
||||
AirDefence.HQ_7_Self_Propelled_STR,
|
||||
}
|
||||
|
||||
LAUNCHER_TRACKER_PAIRS = {
|
||||
AirDefence.SAM_SA_6_Kub_Gainful_TEL: AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
|
||||
AirDefence.SAM_SA_3_S_125_Goa_LN: AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
|
||||
AirDefence.SAM_Hawk_LN_M192: AirDefence.SAM_Hawk_TR__AN_MPQ_46,
|
||||
AirDefence.SAM_Patriot_LN: AirDefence.SAM_Patriot_STR,
|
||||
AirDefence.SAM_SA_2_S_75_Guideline_LN: AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
|
||||
AirDefence.SAM_Rapier_LN: AirDefence.SAM_Rapier_Blindfire_TR,
|
||||
AirDefence.HQ_7_Self_Propelled_LN: AirDefence.HQ_7_Self_Propelled_STR,
|
||||
}
|
||||
|
||||
UNITS_WITH_RADAR = {
|
||||
# Radars
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
AirDefence.EWR_1L13,
|
||||
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR,
|
||||
AirDefence.EWR_55G6,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR,
|
||||
AirDefence.MCC_SR_Sborka_Dog_Ear_SR,
|
||||
AirDefence.SAM_Hawk_TR__AN_MPQ_46,
|
||||
AirDefence.SAM_Hawk_SR__AN_MPQ_50,
|
||||
AirDefence.SAM_Patriot_STR,
|
||||
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
|
||||
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
|
||||
AirDefence.SAM_Roland_EWR,
|
||||
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
|
||||
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
|
||||
AirDefence.SAM_Rapier_Blindfire_TR,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.HQ_7_Self_Propelled_STR,
|
||||
AirDefence.EWR_FuMG_401_Freya_LZ,
|
||||
AirDefence.EWR_FuSe_65_Würzburg_Riese,
|
||||
# Ships
|
||||
CVN_70_Carl_Vinson,
|
||||
Oliver_Hazzard_Perry_class,
|
||||
Ticonderoga_class,
|
||||
FFL_1124_4_Grisha,
|
||||
FFG_Oliver_Hazzard_Perry,
|
||||
CG_Ticonderoga,
|
||||
Corvette_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,
|
||||
Corvette_1241_1_Molniya,
|
||||
Cruiser_1164_Moskva,
|
||||
Frigate_11540_Neustrashimy,
|
||||
Battlecruiser_1144_2_Pyotr_Velikiy,
|
||||
Frigate_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,
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
LHA_1_Tarawa,
|
||||
Type_052B_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
Type_052C_Destroyer
|
||||
]
|
||||
Type_052C_Destroyer,
|
||||
}
|
||||
|
||||
1176
game/data/weapons.py
Normal file
1176
game/data/weapons.py
Normal file
File diff suppressed because it is too large
Load Diff
1183
game/db.py
1183
game/db.py
File diff suppressed because it is too large
Load Diff
@@ -1,189 +1,375 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import typing
|
||||
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.transfers import CargoShip
|
||||
from game.unitmap import (
|
||||
AirliftUnits,
|
||||
Building,
|
||||
ConvoyUnit,
|
||||
FrontLineUnit,
|
||||
GroundObjectUnit,
|
||||
UnitMap,
|
||||
FlyingUnit,
|
||||
)
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
DEBRIEFING_LOG_EXTENSION = "log"
|
||||
|
||||
class DebriefingDeadUnitInfo:
|
||||
country_id = -1
|
||||
player_unit = False
|
||||
type = None
|
||||
|
||||
def __init__(self, country_id, player_unit , type):
|
||||
self.country_id = country_id
|
||||
self.player_unit = player_unit
|
||||
self.type = type
|
||||
@dataclass(frozen=True)
|
||||
class AirLosses:
|
||||
player: List[FlyingUnit]
|
||||
enemy: List[FlyingUnit]
|
||||
|
||||
@property
|
||||
def losses(self) -> Iterator[FlyingUnit]:
|
||||
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.flight.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 == 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_convoy: List[ConvoyUnit] = field(default_factory=list)
|
||||
enemy_convoy: List[ConvoyUnit] = field(default_factory=list)
|
||||
|
||||
player_cargo_ships: List[CargoShip] = field(default_factory=list)
|
||||
enemy_cargo_ships: List[CargoShip] = field(default_factory=list)
|
||||
|
||||
player_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||
enemy_airlifts: List[AirliftUnits] = 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 BaseCaptureEvent:
|
||||
control_point: ControlPoint
|
||||
captured_by_player: bool
|
||||
|
||||
|
||||
@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.
|
||||
#
|
||||
# Also normalize dead map objects (which are ints) to strings. The unit map
|
||||
# only stores strings.
|
||||
killed_ground_units=list({str(u) for u in data["killed_ground_units"]}),
|
||||
destroyed_statics=data["destroyed_objects_positions"],
|
||||
base_capture_events=data["base_capture_events"],
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.country_id) + " " + str(self.player_unit) + " " + str(self.type)
|
||||
|
||||
class Debriefing:
|
||||
def __init__(self, state_data, game):
|
||||
self.state_data = state_data
|
||||
self.killed_aircrafts = state_data["killed_aircrafts"]
|
||||
self.killed_ground_units = state_data["killed_ground_units"]
|
||||
self.weapons_fired = state_data["weapons_fired"]
|
||||
self.mission_ended = state_data["mission_ended"]
|
||||
self.destroyed_units = state_data["destroyed_objects_positions"]
|
||||
|
||||
self.__destroyed_units = []
|
||||
logging.info("--------------------------------")
|
||||
logging.info("Starting Debriefing preprocessing")
|
||||
logging.info("--------------------------------")
|
||||
logging.info(self.base_capture_events)
|
||||
logging.info(self.killed_aircrafts)
|
||||
logging.info(self.killed_ground_units)
|
||||
logging.info(self.weapons_fired)
|
||||
logging.info(self.mission_ended)
|
||||
logging.info(self.destroyed_units)
|
||||
logging.info("--------------------------------")
|
||||
def __init__(
|
||||
self, state_data: Dict[str, Any], game: Game, unit_map: UnitMap
|
||||
) -> None:
|
||||
self.state_data = StateData.from_json(state_data)
|
||||
self.game = game
|
||||
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.dead_aircraft = []
|
||||
self.dead_units = []
|
||||
self.dead_aaa_groups = []
|
||||
self.dead_buildings = []
|
||||
|
||||
for aircraft in self.killed_aircrafts:
|
||||
try:
|
||||
country = int(aircraft.split("|")[1])
|
||||
type = db.unit_type_from_name(aircraft.split("|")[4])
|
||||
player_unit = (country == self.player_country_id)
|
||||
aircraft = DebriefingDeadUnitInfo(country, player_unit, type)
|
||||
if type is not None:
|
||||
self.dead_aircraft.append(aircraft)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
|
||||
for unit in self.killed_ground_units:
|
||||
try:
|
||||
country = int(unit.split("|")[1])
|
||||
type = db.unit_type_from_name(unit.split("|")[4])
|
||||
player_unit = (country == self.player_country_id)
|
||||
unit = DebriefingDeadUnitInfo(country, player_unit, type)
|
||||
if type is not None:
|
||||
self.dead_units.append(unit)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
|
||||
for unit in self.killed_ground_units:
|
||||
for cp in game.theater.controlpoints:
|
||||
|
||||
logging.info(cp.name)
|
||||
logging.info(cp.captured)
|
||||
|
||||
if cp.captured:
|
||||
country = self.player_country_id
|
||||
else:
|
||||
country = self.enemy_country_id
|
||||
player_unit = (country == self.player_country_id)
|
||||
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
logging.info(unit)
|
||||
logging.info(ground_object.group_name)
|
||||
if ground_object.is_same_group(unit):
|
||||
unit = DebriefingDeadUnitInfo(country, player_unit, ground_object.dcs_identifier)
|
||||
self.dead_buildings.append(unit)
|
||||
elif ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]:
|
||||
for g in ground_object.groups:
|
||||
for u in g.units:
|
||||
if u.name == unit:
|
||||
unit = DebriefingDeadUnitInfo(country, player_unit, db.unit_type_from_name(u.type))
|
||||
self.dead_units.append(unit)
|
||||
|
||||
self.player_dead_aircraft = [a for a in self.dead_aircraft if a.country_id == self.player_country_id]
|
||||
self.enemy_dead_aircraft = [a for a in self.dead_aircraft if a.country_id == self.enemy_country_id]
|
||||
self.player_dead_units = [a for a in self.dead_units if a.country_id == self.player_country_id]
|
||||
self.enemy_dead_units = [a for a in self.dead_units if a.country_id == self.enemy_country_id]
|
||||
self.player_dead_buildings = [a for a in self.dead_buildings if a.country_id == self.player_country_id]
|
||||
self.enemy_dead_buildings = [a for a in self.dead_buildings if a.country_id == self.enemy_country_id]
|
||||
|
||||
logging.info(self.player_dead_aircraft)
|
||||
logging.info(self.enemy_dead_aircraft)
|
||||
logging.info(self.player_dead_units)
|
||||
logging.info(self.enemy_dead_units)
|
||||
|
||||
self.player_dead_aircraft_dict = {}
|
||||
for a in self.player_dead_aircraft:
|
||||
if a.type in self.player_dead_aircraft_dict.keys():
|
||||
self.player_dead_aircraft_dict[a.type] = self.player_dead_aircraft_dict[a.type] + 1
|
||||
else:
|
||||
self.player_dead_aircraft_dict[a.type] = 1
|
||||
|
||||
self.enemy_dead_aircraft_dict = {}
|
||||
for a in self.enemy_dead_aircraft:
|
||||
if a.type in self.enemy_dead_aircraft_dict.keys():
|
||||
self.enemy_dead_aircraft_dict[a.type] = self.enemy_dead_aircraft_dict[a.type] + 1
|
||||
else:
|
||||
self.enemy_dead_aircraft_dict[a.type] = 1
|
||||
|
||||
self.player_dead_units_dict = {}
|
||||
for a in self.player_dead_units:
|
||||
if a.type in self.player_dead_units_dict.keys():
|
||||
self.player_dead_units_dict[a.type] = self.player_dead_units_dict[a.type] + 1
|
||||
else:
|
||||
self.player_dead_units_dict[a.type] = 1
|
||||
|
||||
self.enemy_dead_units_dict = {}
|
||||
for a in self.enemy_dead_units:
|
||||
if a.type in self.enemy_dead_units_dict.keys():
|
||||
self.enemy_dead_units_dict[a.type] = self.enemy_dead_units_dict[a.type] + 1
|
||||
else:
|
||||
self.enemy_dead_units_dict[a.type] = 1
|
||||
|
||||
self.player_dead_buildings_dict = {}
|
||||
for a in self.player_dead_buildings:
|
||||
if a.type in self.player_dead_buildings_dict.keys():
|
||||
self.player_dead_buildings_dict[a.type] = self.player_dead_buildings_dict[a.type] + 1
|
||||
else:
|
||||
self.player_dead_buildings_dict[a.type] = 1
|
||||
|
||||
self.enemy_dead_buildings_dict = {}
|
||||
for a in self.enemy_dead_buildings:
|
||||
if a.type in self.enemy_dead_buildings_dict.keys():
|
||||
self.enemy_dead_buildings_dict[a.type] = self.enemy_dead_buildings_dict[a.type] + 1
|
||||
else:
|
||||
self.enemy_dead_buildings_dict[a.type] = 1
|
||||
|
||||
logging.info("--------------------------------")
|
||||
logging.info("Debriefing pre process results :")
|
||||
logging.info("--------------------------------")
|
||||
logging.info(self.player_dead_aircraft_dict)
|
||||
logging.info(self.enemy_dead_aircraft_dict)
|
||||
logging.info(self.player_dead_units_dict)
|
||||
logging.info(self.enemy_dead_units_dict)
|
||||
logging.info(self.player_dead_buildings_dict)
|
||||
logging.info(self.enemy_dead_buildings_dict)
|
||||
self.air_losses = self.dead_aircraft()
|
||||
self.ground_losses = self.dead_ground_units()
|
||||
self.base_captures = self.base_capture_events()
|
||||
|
||||
@property
|
||||
def base_capture_events(self):
|
||||
"""Keeps only the last instance of a base capture event for each base ID"""
|
||||
reversed_captures = [i for i in self.state_data["base_capture_events"][::-1]]
|
||||
last_base_cap_indexes = []
|
||||
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
|
||||
if base in [x[1] for x in last_base_cap_indexes]:
|
||||
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 convoy_losses(self) -> Iterator[ConvoyUnit]:
|
||||
yield from self.ground_losses.player_convoy
|
||||
yield from self.ground_losses.enemy_convoy
|
||||
|
||||
@property
|
||||
def cargo_ship_losses(self) -> Iterator[CargoShip]:
|
||||
yield from self.ground_losses.player_cargo_ships
|
||||
yield from self.ground_losses.enemy_cargo_ships
|
||||
|
||||
@property
|
||||
def airlift_losses(self) -> Iterator[AirliftUnits]:
|
||||
yield from self.ground_losses.player_airlifts
|
||||
yield from self.ground_losses.enemy_airlifts
|
||||
|
||||
@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 convoy_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_convoy
|
||||
else:
|
||||
losses = self.ground_losses.enemy_convoy
|
||||
for loss in losses:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def cargo_ship_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
|
||||
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
|
||||
if player:
|
||||
ships = self.ground_losses.player_cargo_ships
|
||||
else:
|
||||
ships = self.ground_losses.enemy_cargo_ships
|
||||
for ship in ships:
|
||||
for unit_type, count in ship.units.items():
|
||||
losses_by_type[unit_type] += count
|
||||
return losses_by_type
|
||||
|
||||
def airlift_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_airlifts
|
||||
else:
|
||||
losses = self.ground_losses.enemy_airlifts
|
||||
for loss in losses:
|
||||
for unit_type in loss.cargo:
|
||||
losses_by_type[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:
|
||||
aircraft = self.unit_map.flight(unit_name)
|
||||
if aircraft is None:
|
||||
logging.error(f"Could not find Flight matching {unit_name}")
|
||||
continue
|
||||
if aircraft.flight.departure.captured:
|
||||
player_losses.append(aircraft)
|
||||
else:
|
||||
last_base_cap_indexes.append((idx, base))
|
||||
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
|
||||
enemy_losses.append(aircraft)
|
||||
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
|
||||
|
||||
convoy_unit = self.unit_map.convoy_unit(unit_name)
|
||||
if convoy_unit is not None:
|
||||
if convoy_unit.convoy.player_owned:
|
||||
losses.player_convoy.append(convoy_unit)
|
||||
else:
|
||||
losses.enemy_convoy.append(convoy_unit)
|
||||
continue
|
||||
|
||||
cargo_ship = self.unit_map.cargo_ship(unit_name)
|
||||
if cargo_ship is not None:
|
||||
if cargo_ship.player_owned:
|
||||
losses.player_cargo_ships.append(cargo_ship)
|
||||
else:
|
||||
losses.enemy_cargo_ships.append(cargo_ship)
|
||||
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)
|
||||
# Try appending object to the name, because we do this for building statics.
|
||||
if building is None:
|
||||
building = self.unit_map.building_or_fortification(
|
||||
f"{unit_name} object"
|
||||
)
|
||||
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."
|
||||
)
|
||||
|
||||
for unit_name in self.state_data.killed_aircraft:
|
||||
airlift_unit = self.unit_map.airlift_unit(unit_name)
|
||||
if airlift_unit is not None:
|
||||
if airlift_unit.transfer.player:
|
||||
losses.player_airlifts.append(airlift_unit)
|
||||
else:
|
||||
losses.enemy_airlifts.append(airlift_unit)
|
||||
continue
|
||||
|
||||
return losses
|
||||
|
||||
def base_capture_events(self) -> List[BaseCaptureEvent]:
|
||||
"""Keeps only the last instance of a base capture event for each base ID."""
|
||||
blue_coalition_id = 2
|
||||
seen = set()
|
||||
captures = []
|
||||
for capture in reversed(self.state_data.base_capture_events):
|
||||
cp_id_str, new_owner_id_str, _name = capture.split("||")
|
||||
cp_id = int(cp_id_str)
|
||||
|
||||
# Only the most recent capture event matters.
|
||||
if cp_id in seen:
|
||||
continue
|
||||
seen.add(cp_id)
|
||||
|
||||
try:
|
||||
control_point = self.game.theater.find_control_point_by_id(cp_id)
|
||||
except KeyError:
|
||||
# Captured base is not a part of the campaign. This happens when neutral
|
||||
# bases are near the conflict. Nothing to do.
|
||||
continue
|
||||
|
||||
captured_by_player = int(new_owner_id_str) == blue_coalition_id
|
||||
if control_point.is_friendly(to_player=captured_by_player):
|
||||
# Base is currently friendly to the new owner. Was captured and
|
||||
# recaptured in the same mission. Nothing to do.
|
||||
continue
|
||||
|
||||
captures.append(BaseCaptureEvent(control_point, captured_by_player))
|
||||
return captures
|
||||
|
||||
|
||||
class PollDebriefingFileThread(threading.Thread):
|
||||
"""Thread class with a stop() method. The thread itself has to check
|
||||
regularly for the stopped() condition."""
|
||||
|
||||
def __init__(self, callback: typing.Callable, game):
|
||||
super(PollDebriefingFileThread, self).__init__()
|
||||
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()
|
||||
@@ -197,17 +383,21 @@ class PollDebriefingFileThread(threading.Thread):
|
||||
else:
|
||||
last_modified = 0
|
||||
while not self.stopped():
|
||||
if os.path.isfile("state.json") and os.path.getmtime("state.json") > last_modified:
|
||||
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)
|
||||
debriefing = Debriefing(json_data, self.game, self.unit_map)
|
||||
self.callback(debriefing)
|
||||
break
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def wait_for_debriefing(callback: typing.Callable, game)->PollDebriefingFileThread:
|
||||
thread = PollDebriefingFileThread(callback, game)
|
||||
def wait_for_debriefing(
|
||||
callback: Callable[[Debriefing], None], game: Game, unit_map
|
||||
) -> PollDebriefingFileThread:
|
||||
thread = PollDebriefingFileThread(callback, game, unit_map)
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
|
||||
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,26 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, List, Optional, Type, TYPE_CHECKING
|
||||
from typing import List, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import Task
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game import db, persistency
|
||||
from game.debriefing import Debriefing
|
||||
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 theater import ControlPoint
|
||||
from ..unitmap import UnitMap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..game import Game
|
||||
|
||||
DIFFICULTY_LOG_BASE = 1.1
|
||||
EVENT_DEPARTURE_MAX_DISTANCE = 340000
|
||||
|
||||
|
||||
MINOR_DEFEAT_INFLUENCE = 0.1
|
||||
DEFEAT_INFLUENCE = 0.3
|
||||
@@ -30,21 +28,23 @@ STRONG_DEFEAT_INFLUENCE = 0.5
|
||||
class Event:
|
||||
silent = False
|
||||
informational = False
|
||||
is_awacs_enabled = False
|
||||
ca_slots = 0
|
||||
|
||||
game = None # type: Game
|
||||
location = None # type: Point
|
||||
from_cp = None # type: ControlPoint
|
||||
to_cp = None # type: ControlPoint
|
||||
|
||||
operation = None # type: Operation
|
||||
difficulty = 1 # type: int
|
||||
BONUS_BASE = 5
|
||||
|
||||
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
|
||||
def __init__(
|
||||
self,
|
||||
game,
|
||||
from_cp: ControlPoint,
|
||||
target_cp: ControlPoint,
|
||||
location: Point,
|
||||
attacker_name: str,
|
||||
defender_name: str,
|
||||
):
|
||||
self.game = game
|
||||
self.departure_cp: Optional[ControlPoint] = None
|
||||
self.from_cp = from_cp
|
||||
self.to_cp = target_cp
|
||||
self.location = location
|
||||
@@ -55,174 +55,232 @@ class Event:
|
||||
def is_player_attacking(self) -> bool:
|
||||
return self.attacker_name == self.game.player_name
|
||||
|
||||
@property
|
||||
def enemy_cp(self) -> Optional[ControlPoint]:
|
||||
if self.attacker_name == self.game.player_name:
|
||||
return self.to_cp
|
||||
else:
|
||||
return self.departure_cp
|
||||
|
||||
@property
|
||||
def tasks(self) -> List[Type[Task]]:
|
||||
return []
|
||||
|
||||
@property
|
||||
def global_cp_available(self) -> bool:
|
||||
return False
|
||||
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 is_departure_available_from(self, cp: ControlPoint) -> bool:
|
||||
if not cp.captured:
|
||||
return False
|
||||
|
||||
if self.location.distance_to_point(cp.position) > EVENT_DEPARTURE_MAX_DISTANCE:
|
||||
return False
|
||||
|
||||
if cp.is_global and not self.global_cp_available:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def bonus(self) -> int:
|
||||
return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE)
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing) -> bool:
|
||||
return self.operation.is_successfull(debriefing)
|
||||
|
||||
def generate(self):
|
||||
self.operation.is_awacs_enabled = self.is_awacs_enabled
|
||||
self.operation.ca_slots = self.ca_slots
|
||||
|
||||
self.operation.prepare(self.game.theater.terrain, is_quick=False)
|
||||
self.operation.generate()
|
||||
self.operation.current_mission.save(persistency.mission_path_for("liberation_nextturn.miz"))
|
||||
self.environment_settings = self.operation.environment_settings
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
|
||||
logging.info("Commiting mission results")
|
||||
|
||||
# ------------------------------
|
||||
# Destroyed aircrafts
|
||||
cp_map = {cp.id: cp for cp in self.game.theater.controlpoints}
|
||||
for destroyed_aircraft in debriefing.killed_aircrafts:
|
||||
try:
|
||||
cpid = int(destroyed_aircraft.split("|")[3])
|
||||
type = db.unit_type_from_name(destroyed_aircraft.split("|")[4])
|
||||
if cpid in cp_map.keys():
|
||||
cp = cp_map[cpid]
|
||||
if type in cp.base.aircraft.keys():
|
||||
logging.info("Aircraft destroyed : " + str(type))
|
||||
cp.base.aircraft[type] = max(0, cp.base.aircraft[type]-1)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# ------------------------------
|
||||
# Destroyed ground units
|
||||
killed_unit_count_by_cp = {cp.id: 0 for cp in self.game.theater.controlpoints}
|
||||
cp_map = {cp.id: cp for cp in self.game.theater.controlpoints}
|
||||
for killed_ground_unit in debriefing.killed_ground_units:
|
||||
try:
|
||||
cpid = int(killed_ground_unit.split("|")[3])
|
||||
type = db.unit_type_from_name(killed_ground_unit.split("|")[4])
|
||||
if cpid in cp_map.keys():
|
||||
killed_unit_count_by_cp[cpid] = killed_unit_count_by_cp[cpid] + 1
|
||||
cp = cp_map[cpid]
|
||||
if type in cp.base.armor.keys():
|
||||
logging.info("Ground unit destroyed : " + str(type))
|
||||
cp.base.armor[type] = max(0, cp.base.armor[type] - 1)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# ------------------------------
|
||||
# Static ground objects
|
||||
for destroyed_ground_unit_name in debriefing.killed_ground_units:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if not cp.ground_objects:
|
||||
@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
|
||||
|
||||
# -- Static ground objects
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
if ground_object.is_dead:
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
def commit_air_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.air_losses.losses:
|
||||
if (
|
||||
not loss.pilot.player
|
||||
or not self.game.settings.invulnerable_player_pilots
|
||||
):
|
||||
loss.pilot.kill()
|
||||
aircraft = loss.flight.unit_type
|
||||
cp = loss.flight.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_pilot_experience(ato: AirTaskingOrder) -> None:
|
||||
for package in ato.packages:
|
||||
for flight in package.flights:
|
||||
for idx, pilot in enumerate(flight.roster.pilots):
|
||||
if pilot is None:
|
||||
logging.error(
|
||||
f"Cannot award experience to pilot #{idx} of {flight} "
|
||||
"because no pilot is assigned"
|
||||
)
|
||||
continue
|
||||
|
||||
if (
|
||||
(ground_object.group_name == destroyed_ground_unit_name)
|
||||
or
|
||||
(ground_object.is_same_group(destroyed_ground_unit_name))
|
||||
):
|
||||
logging.info("cp {} killing ground object {}".format(cp, ground_object.group_name))
|
||||
cp.ground_objects[i].is_dead = True
|
||||
pilot.record.missions_flown += 1
|
||||
|
||||
info = Information("Building destroyed",
|
||||
ground_object.dcs_identifier + " has been destroyed at location " + ground_object.obj_name,
|
||||
self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
def commit_pilot_experience(self) -> None:
|
||||
self._commit_pilot_experience(self.game.blue_ato)
|
||||
self._commit_pilot_experience(self.game.red_ato)
|
||||
|
||||
@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
|
||||
|
||||
# -- AA Site groups
|
||||
destroyed_units = 0
|
||||
info = Information("Units destroyed at " + ground_object.obj_name,
|
||||
"",
|
||||
self.game.turn)
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
if ground_object.dcs_identifier in ["AA", "CARRIER", "LHA", "EWR"]:
|
||||
for g in ground_object.groups:
|
||||
if not hasattr(g, "units_losts"):
|
||||
g.units_losts = []
|
||||
for u in g.units:
|
||||
if u.name == destroyed_ground_unit_name:
|
||||
g.units.remove(u)
|
||||
g.units_losts.append(u)
|
||||
destroyed_units = destroyed_units + 1
|
||||
info.text = u.type
|
||||
ucount = sum([len(g.units) for g in ground_object.groups])
|
||||
if ucount == 0:
|
||||
ground_object.is_dead = True
|
||||
if destroyed_units > 0:
|
||||
self.game.informations.append(info)
|
||||
logging.info(f"{unit_type} destroyed from {control_point}")
|
||||
control_point.base.armor[unit_type] -= 1
|
||||
|
||||
# ------------------------------
|
||||
# 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
|
||||
@staticmethod
|
||||
def commit_convoy_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.convoy_losses:
|
||||
unit_type = loss.unit_type
|
||||
convoy = loss.convoy
|
||||
available = loss.convoy.units.get(unit_type, 0)
|
||||
convoy_name = f"convoy from {convoy.origin} to {convoy.destination}"
|
||||
if available <= 0:
|
||||
logging.error(
|
||||
f"Found killed {unit_type} in {convoy_name} but that convoy has "
|
||||
"none available."
|
||||
)
|
||||
continue
|
||||
|
||||
for captured in debriefing.base_capture_events:
|
||||
logging.info(f"{unit_type} destroyed in {convoy_name}")
|
||||
convoy.kill_unit(unit_type)
|
||||
|
||||
@staticmethod
|
||||
def commit_cargo_ship_losses(debriefing: Debriefing) -> None:
|
||||
for ship in debriefing.cargo_ship_losses:
|
||||
logging.info(
|
||||
f"All units destroyed in cargo ship from {ship.origin} to "
|
||||
f"{ship.destination}."
|
||||
)
|
||||
ship.kill_all()
|
||||
|
||||
@staticmethod
|
||||
def commit_airlift_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.airlift_losses:
|
||||
transfer = loss.transfer
|
||||
airlift_name = f"airlift from {transfer.origin} to {transfer.destination}"
|
||||
for unit_type in loss.cargo:
|
||||
try:
|
||||
transfer.kill_unit(unit_type)
|
||||
logging.info(f"{unit_type} destroyed in {airlift_name}")
|
||||
except KeyError:
|
||||
logging.exception(
|
||||
f"Found killed {unit_type} in {airlift_name} but that airlift "
|
||||
"has none available."
|
||||
)
|
||||
|
||||
@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)
|
||||
|
||||
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.building_losses:
|
||||
loss.ground_object.kill()
|
||||
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_captures(self, debriefing: Debriefing) -> None:
|
||||
for captured in debriefing.base_captures:
|
||||
try:
|
||||
id = int(captured.split("||")[0])
|
||||
new_owner_coalition = int(captured.split("||")[1])
|
||||
if captured.captured_by_player:
|
||||
info = Information(
|
||||
f"{captured.control_point} captured!",
|
||||
f"We took control of {captured.control_point}.",
|
||||
self.game.turn,
|
||||
)
|
||||
else:
|
||||
info = Information(
|
||||
f"{captured.control_point} lost!",
|
||||
f"The enemy took control of {captured.control_point}.",
|
||||
self.game.turn,
|
||||
)
|
||||
|
||||
captured_cps = []
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.id == id:
|
||||
self.game.informations.append(info)
|
||||
captured.control_point.capture(self.game, captured.captured_by_player)
|
||||
logging.info(f"Will run redeploy for {captured.control_point}")
|
||||
self.redeploy_units(captured.control_point)
|
||||
except Exception:
|
||||
logging.exception(f"Could not process base capture {captured}")
|
||||
|
||||
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
|
||||
def commit(self, debriefing: Debriefing):
|
||||
logging.info("Committing mission results")
|
||||
|
||||
cp.capture(self.game, for_player)
|
||||
|
||||
for cp in captured_cps:
|
||||
logging.info("Will run redeploy for " + cp.name)
|
||||
self.redeploy_units(cp)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.commit_air_losses(debriefing)
|
||||
self.commit_pilot_experience()
|
||||
self.commit_front_line_losses(debriefing)
|
||||
self.commit_convoy_losses(debriefing)
|
||||
self.commit_airlift_losses(debriefing)
|
||||
self.commit_ground_object_losses(debriefing)
|
||||
self.commit_building_losses(debriefing)
|
||||
self.commit_damaged_runways(debriefing)
|
||||
self.commit_captures(debriefing)
|
||||
self.complete_aircraft_transfers(debriefing)
|
||||
|
||||
# Destroyed units carcass
|
||||
# -------------------------
|
||||
for destroyed_unit in debriefing.destroyed_units:
|
||||
for destroyed_unit in debriefing.state_data.destroyed_statics:
|
||||
self.game.add_destroyed_units(destroyed_unit)
|
||||
|
||||
# -----------------------------------
|
||||
@@ -230,12 +288,17 @@ class Event:
|
||||
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)
|
||||
print(
|
||||
"Compute frontline progression for : "
|
||||
+ cp.name
|
||||
+ " to "
|
||||
+ enemy_cp.name
|
||||
)
|
||||
|
||||
delta = 0.0
|
||||
player_won = True
|
||||
ally_casualties = killed_unit_count_by_cp[cp.id]
|
||||
enemy_casualties = killed_unit_count_by_cp[enemy_cp.id]
|
||||
ally_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
|
||||
|
||||
@@ -246,7 +309,11 @@ class Event:
|
||||
|
||||
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
|
||||
|
||||
player_aggresive = cp.stances[enemy_cp.id] in [CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]
|
||||
player_aggresive = cp.stances[enemy_cp.id] in [
|
||||
CombatStance.AGGRESSIVE,
|
||||
CombatStance.ELIMINATION,
|
||||
CombatStance.BREAKTHROUGH,
|
||||
]
|
||||
|
||||
if ally_units_alive == 0:
|
||||
player_won = False
|
||||
@@ -271,11 +338,17 @@ class Event:
|
||||
delta = DEFEAT_INFLUENCE
|
||||
elif ally_casualties > enemy_casualties:
|
||||
|
||||
if ally_units_alive > 2*enemy_units_alive and player_aggresive:
|
||||
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:
|
||||
elif (
|
||||
ally_units_alive > 3 * enemy_units_alive
|
||||
and player_aggresive
|
||||
):
|
||||
player_won = True
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
else:
|
||||
@@ -287,7 +360,10 @@ class Event:
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
|
||||
# No progress with defensive strategies
|
||||
if player_won and cp.stances[enemy_cp.id] in [CombatStance.DEFENSIVE, CombatStance.AMBUSH]:
|
||||
if player_won and cp.stances[enemy_cp.id] in [
|
||||
CombatStance.DEFENSIVE,
|
||||
CombatStance.AMBUSH,
|
||||
]:
|
||||
print("Defensive stance, progress is limited")
|
||||
delta = MINOR_DEFEAT_INFLUENCE
|
||||
|
||||
@@ -295,88 +371,89 @@ class Event:
|
||||
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)
|
||||
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)
|
||||
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):
|
||||
""""
|
||||
def redeploy_units(self, cp: ControlPoint) -> None:
|
||||
""" "
|
||||
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]
|
||||
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
|
||||
|
||||
# From each ally cp, send reinforcements
|
||||
for ally_cp in ally_connected_cps:
|
||||
self.redeploy_between(cp, ally_cp)
|
||||
|
||||
def redeploy_between(self, destination: ControlPoint, source: ControlPoint) -> None:
|
||||
total_units_redeployed = 0
|
||||
moved_units = {}
|
||||
|
||||
if source.has_active_frontline or not destination.captured:
|
||||
# If there are still active front lines to defend at the
|
||||
# transferring CP we should not transfer all units.
|
||||
#
|
||||
# Opfor also does not transfer all of their units.
|
||||
# TODO: Balance the CPs rather than moving half from everywhere.
|
||||
move_factor = 0.5
|
||||
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]
|
||||
# Otherwise we can move everything.
|
||||
move_factor = 1
|
||||
|
||||
moved_units = {}
|
||||
for frontline_unit, count in source.base.armor.items():
|
||||
moved_units[frontline_unit] = int(count * move_factor)
|
||||
total_units_redeployed = total_units_redeployed + int(count * move_factor)
|
||||
|
||||
# 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
|
||||
destination.base.commision_units(moved_units)
|
||||
source.base.commit_losses(moved_units)
|
||||
|
||||
cp.base.commision_units(moved_units)
|
||||
ally_cp.base.commit_losses(moved_units)
|
||||
# Also transfer pending deliveries.
|
||||
for unit_type, count in source.pending_unit_deliveries.units.items():
|
||||
if not issubclass(unit_type, VehicleType):
|
||||
continue
|
||||
if count <= 0:
|
||||
# Don't transfer *sales*...
|
||||
continue
|
||||
move_count = int(count * move_factor)
|
||||
source.pending_unit_deliveries.sell({unit_type: move_count})
|
||||
destination.pending_unit_deliveries.order({unit_type: move_count})
|
||||
total_units_redeployed += move_count
|
||||
|
||||
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
|
||||
|
||||
def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game):
|
||||
super(UnitsDeliveryEvent, self).__init__(game=game,
|
||||
location=to_cp.position,
|
||||
from_cp=from_cp,
|
||||
target_cp=to_cp,
|
||||
attacker_name=attacker_name,
|
||||
defender_name=defender_name)
|
||||
|
||||
self.units: Dict[UnitType, int] = {}
|
||||
|
||||
def __str__(self):
|
||||
return "Pending delivery to {}".format(self.to_cp)
|
||||
|
||||
def deliver(self, units: Dict[UnitType, int]):
|
||||
for k, v in units.items():
|
||||
self.units[k] = self.units.get(k, 0) + v
|
||||
|
||||
def skip(self):
|
||||
|
||||
for k, v in self.units.items():
|
||||
info = Information("Ally Reinforcement", str(k.id) + " x " + str(v) + " at " + self.to_cp.name, self.game.turn)
|
||||
if total_units_redeployed > 0:
|
||||
text = (
|
||||
f"{total_units_redeployed} units have been redeployed from "
|
||||
f"{source.name} to {destination.name}"
|
||||
)
|
||||
info = Information("Units redeployed", text, self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
|
||||
self.to_cp.base.commision_units(self.units)
|
||||
logging.info(text)
|
||||
|
||||
@@ -1,49 +1,12 @@
|
||||
from typing import List, Type
|
||||
|
||||
from dcs.task import CAP, CAS, Task
|
||||
|
||||
from game import db
|
||||
from game.operation.frontlineattack import FrontlineAttackOperation
|
||||
from .event import Event
|
||||
from ..debriefing import Debriefing
|
||||
|
||||
|
||||
class FrontlineAttackEvent(Event):
|
||||
|
||||
@property
|
||||
def tasks(self) -> List[Type[Task]]:
|
||||
if self.is_player_attacking:
|
||||
return [CAS, CAP]
|
||||
else:
|
||||
return [CAP]
|
||||
|
||||
@property
|
||||
def global_cp_available(self) -> bool:
|
||||
return True
|
||||
"""
|
||||
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"
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing):
|
||||
attackers_success = True
|
||||
if self.from_cp.captured:
|
||||
return attackers_success
|
||||
else:
|
||||
return not attackers_success
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
super(FrontlineAttackEvent, self).commit(debriefing)
|
||||
|
||||
def skip(self):
|
||||
if self.to_cp.captured:
|
||||
self.to_cp.base.affect_strength(-0.1)
|
||||
|
||||
def player_attacking(self, flights: db.TaskForceDict):
|
||||
assert self.departure_cp is not None
|
||||
op = FrontlineAttackOperation(game=self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
from_cp=self.from_cp,
|
||||
departure_cp=self.departure_cp,
|
||||
to_cp=self.to_cp)
|
||||
self.operation = op
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
@@ -10,13 +11,26 @@ 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 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:
|
||||
#: List of locales to use when generating random names. If not set, Faker will
|
||||
#: choose the default locale.
|
||||
locales: Optional[List[str]]
|
||||
|
||||
# Country used by this faction
|
||||
country: str = field(default="")
|
||||
@@ -31,31 +45,28 @@ class Faction:
|
||||
description: str = field(default="")
|
||||
|
||||
# Available aircraft
|
||||
aircrafts: List[UnitType] = field(default_factory=list)
|
||||
aircrafts: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available awacs aircraft
|
||||
awacs: List[UnitType] = field(default_factory=list)
|
||||
awacs: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available tanker aircraft
|
||||
tankers: List[UnitType] = field(default_factory=list)
|
||||
tankers: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available frontline units
|
||||
frontline_units: List[VehicleType] = field(default_factory=list)
|
||||
frontline_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Available artillery units
|
||||
artillery_units: List[VehicleType] = field(default_factory=list)
|
||||
artillery_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Infantry units used
|
||||
infantry_units: List[VehicleType] = field(default_factory=list)
|
||||
infantry_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Logistics units used
|
||||
logistics_units: List[VehicleType] = field(default_factory=list)
|
||||
|
||||
# List of units that can be deployed as SHORAD
|
||||
shorads: List[str] = field(default_factory=list)
|
||||
logistics_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Possible SAMS site generators for this faction
|
||||
sams: List[str] = field(default_factory=list)
|
||||
air_defenses: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible EWR generators for this faction.
|
||||
ewrs: List[str] = field(default_factory=list)
|
||||
@@ -63,14 +74,17 @@ class Faction:
|
||||
# Possible Missile site generators for this faction
|
||||
missiles: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible costal site generators for this faction
|
||||
coastal_defenses: List[str] = field(default_factory=list)
|
||||
|
||||
# Required mods or asset packs
|
||||
requirements: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
# possible aircraft carrier units
|
||||
aircraft_carrier: List[UnitType] = field(default_factory=list)
|
||||
aircraft_carrier: List[Type[UnitType]] = field(default_factory=list)
|
||||
|
||||
# possible helicopter carrier units
|
||||
helicopter_carrier: List[UnitType] = field(default_factory=list)
|
||||
helicopter_carrier: List[Type[UnitType]] = field(default_factory=list)
|
||||
|
||||
# Possible carrier names
|
||||
carrier_names: List[str] = field(default_factory=list)
|
||||
@@ -82,10 +96,10 @@ class Faction:
|
||||
navy_generators: List[str] = field(default_factory=list)
|
||||
|
||||
# Available destroyers
|
||||
destroyers: List[str] = field(default_factory=list)
|
||||
destroyers: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# Available cruisers
|
||||
cruisers: List[str] = field(default_factory=list)
|
||||
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)
|
||||
@@ -93,11 +107,14 @@ class Faction:
|
||||
# How many missiles group should we try to generate per CP on startup for this faction
|
||||
missiles_group_count: int = field(default=1)
|
||||
|
||||
# How many coastal group should we try to generate per CP on startup for this faction
|
||||
coastal_group_count: int = field(default=1)
|
||||
|
||||
# Whether this faction has JTAC access
|
||||
has_jtac: bool = field(default=False)
|
||||
|
||||
# Unit to use as JTAC for this faction
|
||||
jtac_unit: Optional[FlyingType] = field(default=None)
|
||||
jtac_unit: Optional[Type[FlyingType]] = field(default=None)
|
||||
|
||||
# doctrine
|
||||
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
|
||||
@@ -106,16 +123,38 @@ class Faction:
|
||||
building_set: List[str] = field(default_factory=list)
|
||||
|
||||
# List of default livery overrides
|
||||
liveries_overrides: Dict[UnitType, List[str]] = field(default_factory=dict)
|
||||
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
|
||||
|
||||
def has_access_to_unittype(self, unitclass: GroundUnitClass) -> bool:
|
||||
has_access = False
|
||||
for vehicle in unitclass.unit_list:
|
||||
if vehicle in self.frontline_units:
|
||||
return True
|
||||
if vehicle in self.artillery_units:
|
||||
return True
|
||||
|
||||
return has_access
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
|
||||
faction = Faction()
|
||||
faction = Faction(locales=json.get("locales"))
|
||||
|
||||
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))
|
||||
raise AssertionError(
|
||||
'Faction\'s country ("{}") is not a valid DCS country ID'.format(
|
||||
faction.country
|
||||
)
|
||||
)
|
||||
|
||||
faction.name = json.get("name", "")
|
||||
if not faction.name:
|
||||
@@ -128,29 +167,30 @@ class Faction:
|
||||
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.aircrafts = list(set(faction.aircrafts + faction.awacs))
|
||||
|
||||
faction.frontline_units = load_all_vehicles(json.get("frontline_units", []))
|
||||
faction.artillery_units = load_all_vehicles(json.get("artillery_units", []))
|
||||
faction.infantry_units = load_all_vehicles(json.get("infantry_units", []))
|
||||
faction.logistics_units = load_all_vehicles(json.get("logistics_units", []))
|
||||
|
||||
faction.sams = json.get("sams", [])
|
||||
faction.ewrs = json.get("ewrs", [])
|
||||
faction.shorads = json.get("shorads", [])
|
||||
|
||||
faction.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.coastal_defenses = json.get("coastal_defenses", [])
|
||||
faction.requirements = json.get("requirements", {})
|
||||
|
||||
faction.carrier_names = json.get("carrier_names", [])
|
||||
faction.helicopter_carrier_names = json.get(
|
||||
"helicopter_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.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)
|
||||
@@ -161,6 +201,7 @@ class Faction:
|
||||
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))
|
||||
faction.coastal_group_count = int(json.get("coastal_group_count", 0))
|
||||
|
||||
# Load doctrine
|
||||
doctrine = json.get("doctrine", "modern")
|
||||
@@ -194,16 +235,24 @@ class Faction:
|
||||
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[UnitType]:
|
||||
return (self.infantry_units + self.aircrafts + self.awacs +
|
||||
self.artillery_units + self.frontline_units +
|
||||
self.tankers + self.logistics_units)
|
||||
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[UnitType]:
|
||||
def unit_loader(unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
|
||||
"""
|
||||
Find unit by name
|
||||
:param unit: Unit name as string
|
||||
@@ -226,13 +275,14 @@ def unit_loader(unit: str, class_repository: List[Any]) -> Optional[UnitType]:
|
||||
return None
|
||||
|
||||
|
||||
def load_aircraft(name: str) -> Optional[FlyingType]:
|
||||
return cast(Optional[FlyingType], unit_loader(
|
||||
name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]
|
||||
))
|
||||
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[FlyingType]:
|
||||
def load_all_aircraft(data) -> List[Type[FlyingType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_aircraft(name)
|
||||
@@ -241,13 +291,16 @@ def load_all_aircraft(data) -> List[FlyingType]:
|
||||
return items
|
||||
|
||||
|
||||
def load_vehicle(name: str) -> Optional[VehicleType]:
|
||||
return cast(Optional[FlyingType], unit_loader(
|
||||
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
|
||||
))
|
||||
def load_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[VehicleType]:
|
||||
def load_all_vehicles(data) -> List[Type[VehicleType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_vehicle(name)
|
||||
@@ -256,11 +309,11 @@ def load_all_vehicles(data) -> List[VehicleType]:
|
||||
return items
|
||||
|
||||
|
||||
def load_ship(name: str) -> Optional[ShipType]:
|
||||
def load_ship(name: str) -> Optional[Type[ShipType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(name, [dcs.ships]))
|
||||
|
||||
|
||||
def load_all_ships(data) -> List[ShipType]:
|
||||
def load_all_ships(data) -> List[Type[ShipType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_ship(name)
|
||||
|
||||
@@ -2,8 +2,9 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterator, Optional, Type
|
||||
from typing import Dict, Iterator, List, Optional, Type
|
||||
|
||||
from game import persistency
|
||||
from game.factions.faction import Faction
|
||||
|
||||
FACTION_DIRECTORY = Path("./resources/factions/")
|
||||
@@ -23,15 +24,22 @@ class FactionLoader:
|
||||
if self._factions is None:
|
||||
self._factions = self.load_factions()
|
||||
|
||||
@staticmethod
|
||||
def find_faction_files_in(path: Path) -> List[Path]:
|
||||
return [f for f in path.glob("*.json") if f.is_file()]
|
||||
|
||||
@classmethod
|
||||
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
|
||||
files = [f for f in FACTION_DIRECTORY.glob("*.json") if f.is_file()]
|
||||
user_faction_path = Path(persistency.base_path()) / "Liberation/Factions"
|
||||
files = cls.find_faction_files_in(
|
||||
FACTION_DIRECTORY
|
||||
) + cls.find_faction_files_in(user_faction_path)
|
||||
factions = {}
|
||||
|
||||
for f in files:
|
||||
try:
|
||||
with f.open("r", encoding="utf-8") as fdata:
|
||||
data = json.load(fdata, encoding="utf-8")
|
||||
data = json.load(fdata)
|
||||
factions[data["name"]] = Faction.from_json(data)
|
||||
logging.info("Loaded faction : " + str(f))
|
||||
except Exception:
|
||||
|
||||
535
game/game.py
535
game/game.py
@@ -1,35 +1,46 @@
|
||||
import itertools
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Dict, List
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Iterator
|
||||
|
||||
from dcs.action import Coalition
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike, Task
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.task import CAP, CAS, PinpointStrike
|
||||
from dcs.vehicles import AirDefence
|
||||
from faker import Faker
|
||||
|
||||
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 game.theater.theatergroundobject import MissileSiteGroundObject
|
||||
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.flights.flight import FlightType
|
||||
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
||||
from theater import ConflictTheater, ControlPoint
|
||||
from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
|
||||
from . import persistency
|
||||
from .debriefing import Debriefing
|
||||
from .event.event import Event, UnitsDeliveryEvent
|
||||
from .event.event import Event
|
||||
from .event.frontlineattack import FrontlineAttackEvent
|
||||
from .factions.faction import Faction
|
||||
from .income import Income
|
||||
from .infos.information import Information
|
||||
from .settings import Settings
|
||||
from .navmesh import NavMesh
|
||||
from .procurement import AircraftProcurementRequest, ProcurementAi
|
||||
from .profiling import logged_duration
|
||||
from .settings import Settings, AutoAtoBehavior
|
||||
from .squadrons import Pilot, AirWing
|
||||
from .theater import ConflictTheater
|
||||
from .theater.bullseye import Bullseye
|
||||
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
||||
from .threatzones import ThreatZones
|
||||
from .transfers import PendingTransfers
|
||||
from .unitmap import UnitMap
|
||||
from .weather import Conditions, TimeOfDay
|
||||
|
||||
COMMISION_UNIT_VARIETY = 4
|
||||
@@ -62,17 +73,27 @@ ENEMY_BASE_STRENGTH_RECOVERY = 0.05
|
||||
# cost of AWACS for single operation
|
||||
AWACS_BUDGET_COST = 4
|
||||
|
||||
# Initial budget value
|
||||
PLAYER_BUDGET_INITIAL = 650
|
||||
|
||||
# Bonus multiplier logarithm base
|
||||
PLAYER_BUDGET_IMPORTANCE_LOG = 2
|
||||
|
||||
|
||||
class TurnState(Enum):
|
||||
WIN = 0
|
||||
LOSS = 1
|
||||
CONTINUE = 2
|
||||
|
||||
|
||||
class Game:
|
||||
def __init__(self, player_name: str, enemy_name: str,
|
||||
theater: ConflictTheater, start_date: datetime,
|
||||
settings: Settings):
|
||||
def __init__(
|
||||
self,
|
||||
player_name: str,
|
||||
enemy_name: str,
|
||||
theater: ConflictTheater,
|
||||
start_date: datetime,
|
||||
settings: Settings,
|
||||
player_budget: float,
|
||||
enemy_budget: float,
|
||||
) -> None:
|
||||
self.settings = settings
|
||||
self.events: List[Event] = []
|
||||
self.theater = theater
|
||||
@@ -80,35 +101,91 @@ class Game:
|
||||
self.player_country = db.FACTIONS[player_name].country
|
||||
self.enemy_name = enemy_name
|
||||
self.enemy_country = db.FACTIONS[enemy_name].country
|
||||
self.turn = 0
|
||||
# pass_turn() will be called when initialization is complete which will
|
||||
# increment this to turn 0 before it reaches the player.
|
||||
self.turn = -1
|
||||
# NB: This is the *start* date. It is never updated.
|
||||
self.date = date(start_date.year, start_date.month, start_date.day)
|
||||
self.game_stats = GameStats()
|
||||
self.game_stats.update(self)
|
||||
self.ground_planners: Dict[int, GroundPlanner] = {}
|
||||
self.informations = []
|
||||
self.informations.append(Information("Game Start", "-" * 40, 0))
|
||||
self.__culling_points = self.compute_conflicts_position()
|
||||
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
|
||||
self.__culling_zones: List[Point] = []
|
||||
self.__destroyed_units: List[str] = []
|
||||
self.savepath = ""
|
||||
self.budget = PLAYER_BUDGET_INITIAL
|
||||
self.budget = player_budget
|
||||
self.enemy_budget = enemy_budget
|
||||
self.current_unit_id = 0
|
||||
self.current_group_id = 0
|
||||
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
self.blue_transit_network = TransitNetwork()
|
||||
self.red_transit_network = TransitNetwork()
|
||||
|
||||
self.blue_procurement_requests: List[AircraftProcurementRequest] = []
|
||||
self.red_procurement_requests: List[AircraftProcurementRequest] = []
|
||||
|
||||
self.blue_ato = AirTaskingOrder()
|
||||
self.red_ato = AirTaskingOrder()
|
||||
|
||||
self.aircraft_inventory = GlobalAircraftInventory(
|
||||
self.theater.controlpoints
|
||||
)
|
||||
self.blue_bullseye = Bullseye(Point(0, 0))
|
||||
self.red_bullseye = Bullseye(Point(0, 0))
|
||||
|
||||
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
|
||||
|
||||
self.transfers = PendingTransfers(self)
|
||||
|
||||
self.sanitize_sides()
|
||||
|
||||
self.blue_faker = Faker(self.player_faction.locales)
|
||||
self.red_faker = Faker(self.enemy_faction.locales)
|
||||
|
||||
self.blue_air_wing = AirWing(self, player=True)
|
||||
self.red_air_wing = AirWing(self, player=False)
|
||||
|
||||
self.on_load(game_still_initializing=True)
|
||||
|
||||
def __getstate__(self) -> Dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
# Avoid persisting any volatile types that can be deterministically
|
||||
# recomputed on load for the sake of save compatibility.
|
||||
del state["blue_threat_zone"]
|
||||
del state["red_threat_zone"]
|
||||
del state["blue_navmesh"]
|
||||
del state["red_navmesh"]
|
||||
del state["blue_faker"]
|
||||
del state["red_faker"]
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: Dict[str, Any]) -> None:
|
||||
self.__dict__.update(state)
|
||||
# Regenerate any state that was not persisted.
|
||||
self.on_load()
|
||||
|
||||
def ato_for(self, player: bool) -> AirTaskingOrder:
|
||||
if player:
|
||||
return self.blue_ato
|
||||
return self.red_ato
|
||||
|
||||
def procurement_requests_for(
|
||||
self, player: bool
|
||||
) -> List[AircraftProcurementRequest]:
|
||||
if player:
|
||||
return self.blue_procurement_requests
|
||||
return self.red_procurement_requests
|
||||
|
||||
def transit_network_for(self, player: bool) -> TransitNetwork:
|
||||
if player:
|
||||
return self.blue_transit_network
|
||||
return self.red_transit_network
|
||||
|
||||
def generate_conditions(self) -> Conditions:
|
||||
return Conditions.generate(self.theater, self.date,
|
||||
self.current_turn_time_of_day, self.settings)
|
||||
return Conditions.generate(
|
||||
self.theater, self.current_day, self.current_turn_time_of_day, self.settings
|
||||
)
|
||||
|
||||
def sanitize_sides(self):
|
||||
"""
|
||||
@@ -131,6 +208,31 @@ class Game:
|
||||
def enemy_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.enemy_name]
|
||||
|
||||
def faction_for(self, player: bool) -> Faction:
|
||||
if player:
|
||||
return self.player_faction
|
||||
return self.enemy_faction
|
||||
|
||||
def faker_for(self, player: bool) -> Faker:
|
||||
if player:
|
||||
return self.blue_faker
|
||||
return self.red_faker
|
||||
|
||||
def air_wing_for(self, player: bool) -> AirWing:
|
||||
if player:
|
||||
return self.blue_air_wing
|
||||
return self.red_air_wing
|
||||
|
||||
def country_for(self, player: bool) -> str:
|
||||
if player:
|
||||
return self.player_country
|
||||
return self.enemy_country
|
||||
|
||||
def bullseye_for(self, player: bool) -> Bullseye:
|
||||
if player:
|
||||
return self.blue_bullseye
|
||||
return self.red_bullseye
|
||||
|
||||
def _roll(self, prob, mult):
|
||||
if self.settings.version == "dev":
|
||||
# always generate all events for dev
|
||||
@@ -139,56 +241,48 @@ class Game:
|
||||
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))
|
||||
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)
|
||||
for front_line in self.theater.conflicts():
|
||||
self._generate_player_event(
|
||||
FrontlineAttackEvent,
|
||||
front_line.blue_cp,
|
||||
front_line.red_cp,
|
||||
)
|
||||
|
||||
@property
|
||||
def budget_reward_amount(self):
|
||||
reward = 0
|
||||
if len(self.theater.player_points()) > 0:
|
||||
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():
|
||||
reward = reward + REWARDS[g.category]
|
||||
return reward
|
||||
def adjust_budget(self, amount: float, player: bool) -> None:
|
||||
if player:
|
||||
self.budget += amount
|
||||
else:
|
||||
return reward
|
||||
self.enemy_budget += amount
|
||||
|
||||
def _budget_player(self):
|
||||
self.budget += self.budget_reward_amount
|
||||
def process_player_income(self):
|
||||
self.budget += Income(self, player=True).total
|
||||
|
||||
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
|
||||
self.enemy_budget += Income(self, player=False).total
|
||||
|
||||
def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent:
|
||||
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
|
||||
def initiate_event(self, event: Event) -> UnitMap:
|
||||
# assert event in self.events
|
||||
logging.info("Generating {} (regular)".format(event))
|
||||
event.generate()
|
||||
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()
|
||||
|
||||
if event in self.events:
|
||||
self.events.remove(event)
|
||||
@@ -197,37 +291,48 @@ class Game:
|
||||
|
||||
def is_player_attack(self, event):
|
||||
if isinstance(event, Event):
|
||||
return event and event.attacker_name and event.attacker_name == self.player_name
|
||||
return (
|
||||
event
|
||||
and event.attacker_name
|
||||
and event.attacker_name == self.player_name
|
||||
)
|
||||
else:
|
||||
return event and event.name and event.name == self.player_name
|
||||
raise RuntimeError(f"{event} was passed when an Event type was expected")
|
||||
|
||||
def on_load(self) -> None:
|
||||
def on_load(self, game_still_initializing: bool = False) -> None:
|
||||
LuaPluginManager.load_settings(self.settings)
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
self.compute_conflicts_position()
|
||||
if not game_still_initializing:
|
||||
self.compute_threat_zones()
|
||||
self.blue_faker = Faker(self.faction_for(player=True).locales)
|
||||
self.red_faker = Faker(self.faction_for(player=False).locales)
|
||||
|
||||
# Save game compatibility.
|
||||
def reset_ato(self) -> None:
|
||||
self.blue_ato.clear()
|
||||
self.red_ato.clear()
|
||||
|
||||
# TODO: Remove in 2.3.
|
||||
if not hasattr(self, "conditions"):
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
def pass_turn(self, no_action: bool = False) -> None:
|
||||
logging.info("Pass turn")
|
||||
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
|
||||
def finish_turn(self, skipped: bool = False) -> None:
|
||||
self.informations.append(
|
||||
Information("End of turn #" + str(self.turn), "-" * 40, 0)
|
||||
)
|
||||
self.turn += 1
|
||||
|
||||
for event in self.events:
|
||||
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()
|
||||
# Need to recompute before transfers and deliveries to account for captures.
|
||||
# This happens in in initialize_turn as well, because cheating doesn't advance a
|
||||
# turn but can capture bases so we need to recompute there as well.
|
||||
self.compute_transit_networks()
|
||||
|
||||
self._enemy_reinforcement()
|
||||
self._budget_player()
|
||||
# Must happen *before* unit deliveries are handled, or else new units will spawn
|
||||
# one hop ahead. ControlPoint.process_turn handles unit deliveries.
|
||||
self.transfers.perform_transfers()
|
||||
|
||||
if not no_action and self.turn > 1:
|
||||
# Needs to happen *before* planning transfers so we don't cancel the
|
||||
self.reset_ato()
|
||||
for control_point in self.theater.controlpoints:
|
||||
control_point.process_turn(self)
|
||||
|
||||
if not skipped and self.turn > 1:
|
||||
for cp in self.theater.player_points():
|
||||
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
|
||||
else:
|
||||
@@ -237,106 +342,124 @@ class Game:
|
||||
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
self.process_enemy_income()
|
||||
self.process_player_income()
|
||||
|
||||
def begin_turn_0(self) -> None:
|
||||
self.turn = 0
|
||||
self.initialize_turn()
|
||||
|
||||
def pass_turn(self, no_action: bool = False) -> None:
|
||||
logging.info("Pass turn")
|
||||
with logged_duration("Turn finalization"):
|
||||
self.finish_turn(no_action)
|
||||
with logged_duration("Turn initialization"):
|
||||
self.initialize_turn()
|
||||
|
||||
# Autosave progress
|
||||
persistency.autosave(self)
|
||||
|
||||
def check_win_loss(self):
|
||||
player_airbases = {
|
||||
cp for cp in self.theater.player_points() if cp.runway_is_operational()
|
||||
}
|
||||
if not player_airbases:
|
||||
return TurnState.LOSS
|
||||
|
||||
enemy_airbases = {
|
||||
cp for cp in self.theater.enemy_points() if cp.runway_is_operational()
|
||||
}
|
||||
if not enemy_airbases:
|
||||
return TurnState.WIN
|
||||
|
||||
return TurnState.CONTINUE
|
||||
|
||||
def set_bullseye(self) -> None:
|
||||
player_cp, enemy_cp = self.theater.closest_opposing_control_points()
|
||||
self.blue_bullseye = Bullseye(enemy_cp.position)
|
||||
self.red_bullseye = Bullseye(player_cp.position)
|
||||
|
||||
def initialize_turn(self) -> None:
|
||||
self.events = []
|
||||
self._generate_events()
|
||||
|
||||
self.set_bullseye()
|
||||
|
||||
# Update statistics
|
||||
self.game_stats.update(self)
|
||||
|
||||
self.blue_air_wing.reset()
|
||||
self.red_air_wing.reset()
|
||||
self.aircraft_inventory.reset()
|
||||
for cp in self.theater.controlpoints:
|
||||
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.__culling_points = self.compute_conflicts_position()
|
||||
with logged_duration("Computing conflict positions"):
|
||||
self.compute_conflicts_position()
|
||||
with logged_duration("Threat zone computation"):
|
||||
self.compute_threat_zones()
|
||||
with logged_duration("Transit network identification"):
|
||||
self.compute_transit_networks()
|
||||
self.ground_planners = {}
|
||||
self.blue_ato.clear()
|
||||
self.red_ato.clear()
|
||||
CoalitionMissionPlanner(self, is_player=True).plan_missions()
|
||||
CoalitionMissionPlanner(self, is_player=False).plan_missions()
|
||||
|
||||
self.blue_procurement_requests.clear()
|
||||
self.red_procurement_requests.clear()
|
||||
|
||||
with logged_duration("Procurement of airlift assets"):
|
||||
self.transfers.order_airlift_assets()
|
||||
with logged_duration("Transport planning"):
|
||||
self.transfers.plan_transports()
|
||||
|
||||
with logged_duration("Blue mission planning"):
|
||||
if self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled:
|
||||
blue_planner = CoalitionMissionPlanner(self, is_player=True)
|
||||
blue_planner.plan_missions()
|
||||
|
||||
with logged_duration("Red mission planning"):
|
||||
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
|
||||
|
||||
def _enemy_reinforcement(self):
|
||||
"""
|
||||
Compute and commision reinforcement for enemy bases
|
||||
"""
|
||||
self.plan_procurement()
|
||||
|
||||
MAX_ARMOR = 30 * self.settings.multiplier
|
||||
MAX_AIRCRAFT = 25 * self.settings.multiplier
|
||||
def plan_procurement(self) -> None:
|
||||
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
|
||||
# gets much more of the budget that turn. Otherwise budget (after
|
||||
# repairs) is split evenly between air and ground. For the default
|
||||
# starting budget of 2000 this gives 600 to ground forces and 1400 to
|
||||
# aircraft. After that the budget will be spend proportionally based on how much is already invested
|
||||
|
||||
production = 0.0
|
||||
for enemy_point in self.theater.enemy_points():
|
||||
for g in enemy_point.ground_objects:
|
||||
if g.category in REWARDS.keys():
|
||||
production = production + REWARDS[g.category]
|
||||
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)
|
||||
|
||||
production = production * 0.75
|
||||
budget_for_armored_units = production / 2
|
||||
budget_for_aircraft = production / 2
|
||||
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)
|
||||
|
||||
potential_cp_armor = []
|
||||
for cp in self.theater.enemy_points():
|
||||
for cpe in cp.connected_points:
|
||||
if cpe.captured and cp.base.total_armor < MAX_ARMOR:
|
||||
potential_cp_armor.append(cp)
|
||||
if len(potential_cp_armor) == 0:
|
||||
potential_cp_armor = self.theater.enemy_points()
|
||||
|
||||
i = 0
|
||||
potential_units = db.FACTIONS[self.enemy_name].frontline_units
|
||||
|
||||
print("Enemy Recruiting")
|
||||
print(potential_cp_armor)
|
||||
print(budget_for_armored_units)
|
||||
print(potential_units)
|
||||
|
||||
if len(potential_units) > 0 and len(potential_cp_armor) > 0:
|
||||
while budget_for_armored_units > 0:
|
||||
i = i + 1
|
||||
if i > 50 or budget_for_armored_units <= 0:
|
||||
break
|
||||
target_cp = random.choice(potential_cp_armor)
|
||||
if target_cp.base.total_armor >= MAX_ARMOR:
|
||||
continue
|
||||
unit = random.choice(potential_units)
|
||||
price = db.PRICES[unit] * 2
|
||||
budget_for_armored_units -= price * 2
|
||||
target_cp.base.armor[unit] = target_cp.base.armor.get(unit, 0) + 2
|
||||
info = Information("Enemy Reinforcement", unit.id + " x 2 at " + target_cp.name, self.turn)
|
||||
print(str(info))
|
||||
self.informations.append(info)
|
||||
|
||||
if budget_for_armored_units > 0:
|
||||
budget_for_aircraft += budget_for_armored_units
|
||||
|
||||
potential_units = [u for u in db.FACTIONS[self.enemy_name].aircrafts
|
||||
if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
|
||||
|
||||
if len(potential_units) > 0 and len(potential_cp_armor) > 0:
|
||||
while budget_for_aircraft > 0:
|
||||
i = i + 1
|
||||
if i > 50 or budget_for_aircraft <= 0:
|
||||
break
|
||||
target_cp = random.choice(potential_cp_armor)
|
||||
if target_cp.base.total_planes >= MAX_AIRCRAFT:
|
||||
continue
|
||||
unit = random.choice(potential_units)
|
||||
price = db.PRICES[unit] * 2
|
||||
budget_for_aircraft -= price * 2
|
||||
target_cp.base.aircraft[unit] = target_cp.base.aircraft.get(unit, 0) + 2
|
||||
info = Information("Enemy Reinforcement", unit.id + " x 2 at " + target_cp.name, self.turn)
|
||||
print(str(info))
|
||||
self.informations.append(info)
|
||||
def message(self, text: str) -> None:
|
||||
self.informations.append(Information(text, turn=self.turn))
|
||||
|
||||
@property
|
||||
def current_turn_time_of_day(self) -> TimeOfDay:
|
||||
@@ -360,24 +483,55 @@ class Game:
|
||||
self.current_group_id += 1
|
||||
return self.current_group_id
|
||||
|
||||
def compute_transit_networks(self) -> None:
|
||||
self.blue_transit_network = self.compute_transit_network_for(player=True)
|
||||
self.red_transit_network = self.compute_transit_network_for(player=False)
|
||||
|
||||
def compute_transit_network_for(self, player: bool) -> TransitNetwork:
|
||||
return TransitNetworkBuilder(self.theater, player).build()
|
||||
|
||||
def compute_threat_zones(self) -> None:
|
||||
self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
|
||||
self.red_threat_zone = ThreatZones.for_faction(self, player=False)
|
||||
self.blue_navmesh = NavMesh.from_threat_zones(
|
||||
self.red_threat_zone, self.theater
|
||||
)
|
||||
self.red_navmesh = NavMesh.from_threat_zones(
|
||||
self.blue_threat_zone, self.theater
|
||||
)
|
||||
|
||||
def threat_zone_for(self, player: bool) -> ThreatZones:
|
||||
if player:
|
||||
return self.blue_threat_zone
|
||||
return self.red_threat_zone
|
||||
|
||||
def navmesh_for(self, player: bool) -> NavMesh:
|
||||
if player:
|
||||
return self.blue_navmesh
|
||||
return self.red_navmesh
|
||||
|
||||
def compute_conflicts_position(self):
|
||||
"""
|
||||
Compute the current conflict center position(s), mainly used for culling calculation
|
||||
:return: List of points of interests
|
||||
"""
|
||||
points = []
|
||||
zones = []
|
||||
|
||||
# By default, use the existing frontline conflict position
|
||||
for front_line in self.theater.conflicts():
|
||||
position = Conflict.frontline_position(self.theater,
|
||||
front_line.control_point_a,
|
||||
front_line.control_point_b)
|
||||
points.append(position[0])
|
||||
points.append(front_line.control_point_a.position)
|
||||
points.append(front_line.control_point_b.position)
|
||||
position = Conflict.frontline_position(front_line, self.theater)
|
||||
zones.append(position[0])
|
||||
zones.append(front_line.blue_cp.position)
|
||||
zones.append(front_line.red_cp.position)
|
||||
|
||||
for cp in self.theater.controlpoints:
|
||||
# If do_not_cull_carrier is enabled, add carriers as culling point
|
||||
if self.settings.perf_do_not_cull_carrier:
|
||||
if cp.is_carrier or cp.is_lha:
|
||||
zones.append(cp.position)
|
||||
|
||||
# If there is no conflict take the center point between the two nearest opposing bases
|
||||
if len(points) == 0:
|
||||
if len(zones) == 0:
|
||||
cpoint = None
|
||||
min_distance = sys.maxsize
|
||||
for cp in self.theater.player_points():
|
||||
@@ -385,21 +539,35 @@ class Game:
|
||||
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)
|
||||
cpoint = Point(
|
||||
(cp.position.x + cp2.position.x) / 2,
|
||||
(cp.position.y + cp2.position.y) / 2,
|
||||
)
|
||||
zones.append(cp.position)
|
||||
zones.append(cp2.position)
|
||||
break
|
||||
if cpoint is not None:
|
||||
break
|
||||
if cpoint is not None:
|
||||
points.append(cpoint)
|
||||
zones.append(cpoint)
|
||||
|
||||
packages = itertools.chain(self.blue_ato.packages, self.red_ato.packages)
|
||||
for package in packages:
|
||||
if package.primary_task is FlightType.BARCAP:
|
||||
# BARCAPs will be planned at most locations on smaller theaters,
|
||||
# rendering culling fairly useless. BARCAP packages don't really
|
||||
# need the ground detail since they're defensive. SAMs nearby
|
||||
# are only interesting if there are enemies in the area, and if
|
||||
# there are they won't be culled because of the enemy's mission.
|
||||
continue
|
||||
zones.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))
|
||||
if len(zones) == 0:
|
||||
zones.append(Point(0, 0))
|
||||
|
||||
return points
|
||||
self.__culling_zones = zones
|
||||
|
||||
def add_destroyed_units(self, data):
|
||||
pos = Point(data["x"], data["z"])
|
||||
@@ -415,20 +583,19 @@ class Game:
|
||||
: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:
|
||||
if not self.settings.perf_culling:
|
||||
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
|
||||
for z in self.__culling_zones:
|
||||
if z.distance_to_point(pos) < self.settings.perf_culling_distance * 1000:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_culling_points(self):
|
||||
def get_culling_zones(self):
|
||||
"""
|
||||
Check culling points
|
||||
:return: List of culling points
|
||||
:return: List of culling zones
|
||||
"""
|
||||
return self.__culling_points
|
||||
return self.__culling_zones
|
||||
|
||||
# 1 = red, 2 = blue
|
||||
def get_player_coalition_id(self):
|
||||
@@ -447,4 +614,14 @@ class Game:
|
||||
return "blue"
|
||||
|
||||
def get_enemy_color(self):
|
||||
return "red"
|
||||
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."
|
||||
)
|
||||
|
||||
55
game/income.py
Normal file
55
game/income.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.db import REWARDS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BuildingIncome:
|
||||
name: str
|
||||
category: str
|
||||
number: int
|
||||
income_per_building: int
|
||||
|
||||
@property
|
||||
def income(self) -> int:
|
||||
return self.number * self.income_per_building
|
||||
|
||||
|
||||
class Income:
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
if player:
|
||||
self.multiplier = game.settings.player_income_multiplier
|
||||
else:
|
||||
self.multiplier = game.settings.enemy_income_multiplier
|
||||
self.control_points = []
|
||||
self.buildings = []
|
||||
|
||||
names = set()
|
||||
for cp in game.theater.control_points_for(player):
|
||||
if cp.income_per_turn:
|
||||
self.control_points.append(cp)
|
||||
for tgo in cp.ground_objects:
|
||||
names.add(tgo.obj_name)
|
||||
|
||||
for name in names:
|
||||
count = 0
|
||||
tgos = game.theater.find_ground_objects_by_obj_name(name)
|
||||
category = tgos[0].category
|
||||
if category not in REWARDS:
|
||||
continue
|
||||
for tgo in tgos:
|
||||
if not tgo.is_dead:
|
||||
count += 1
|
||||
self.buildings.append(
|
||||
BuildingIncome(name, category, count, REWARDS[category])
|
||||
)
|
||||
|
||||
self.from_bases = sum(cp.income_per_turn for cp in self.control_points)
|
||||
self.total_buildings = sum(b.income for b in self.buildings)
|
||||
self.total = (self.total_buildings + self.from_bases) * self.multiplier
|
||||
@@ -1,11 +1,19 @@
|
||||
import datetime
|
||||
|
||||
class Information():
|
||||
|
||||
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):
|
||||
s = "[" + str(self.turn) + "] " + self.title + "\n" + self.text
|
||||
return s
|
||||
return "[{}][{}] {} {}".format(
|
||||
self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if self.timestamp is not None
|
||||
else "",
|
||||
self.turn,
|
||||
self.title,
|
||||
self.text,
|
||||
)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
"""Inventory management APIs."""
|
||||
from collections import defaultdict
|
||||
from typing import Dict, Iterable, Iterator, Set, Tuple
|
||||
from __future__ import annotations
|
||||
|
||||
from dcs.unittype import UnitType
|
||||
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
|
||||
from theater import ControlPoint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ControlPoint
|
||||
|
||||
|
||||
class ControlPointAircraftInventory:
|
||||
@@ -13,9 +17,9 @@ class ControlPointAircraftInventory:
|
||||
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
self.control_point = control_point
|
||||
self.inventory: Dict[UnitType, int] = defaultdict(int)
|
||||
self.inventory: Dict[Type[FlyingType], int] = defaultdict(int)
|
||||
|
||||
def add_aircraft(self, aircraft: UnitType, count: int) -> None:
|
||||
def add_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
|
||||
"""Adds aircraft to the inventory.
|
||||
|
||||
Args:
|
||||
@@ -24,7 +28,7 @@ class ControlPointAircraftInventory:
|
||||
"""
|
||||
self.inventory[aircraft] += count
|
||||
|
||||
def remove_aircraft(self, aircraft: UnitType, count: int) -> None:
|
||||
def remove_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
|
||||
"""Removes aircraft from the inventory.
|
||||
|
||||
Args:
|
||||
@@ -43,7 +47,7 @@ class ControlPointAircraftInventory:
|
||||
)
|
||||
self.inventory[aircraft] -= count
|
||||
|
||||
def available(self, aircraft: UnitType) -> int:
|
||||
def available(self, aircraft: Type[FlyingType]) -> int:
|
||||
"""Returns the number of available aircraft of the given type.
|
||||
|
||||
Args:
|
||||
@@ -55,14 +59,14 @@ class ControlPointAircraftInventory:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def types_available(self) -> Iterator[UnitType]:
|
||||
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[UnitType, int]]:
|
||||
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:
|
||||
@@ -75,6 +79,7 @@ class ControlPointAircraftInventory:
|
||||
|
||||
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
|
||||
@@ -96,18 +101,20 @@ class GlobalAircraftInventory:
|
||||
inventory.add_aircraft(aircraft, count)
|
||||
|
||||
def for_control_point(
|
||||
self,
|
||||
control_point: ControlPoint) -> ControlPointAircraftInventory:
|
||||
self, control_point: ControlPoint
|
||||
) -> ControlPointAircraftInventory:
|
||||
"""Returns the inventory specific to the given control point."""
|
||||
return self.inventories[control_point]
|
||||
|
||||
@property
|
||||
def available_types_for_player(self) -> Iterator[UnitType]:
|
||||
def available_types_for_player(self) -> Iterator[Type[FlyingType]]:
|
||||
"""Iterates over all aircraft types available to the player."""
|
||||
seen: Set[UnitType] = set()
|
||||
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
|
||||
|
||||
@@ -7,8 +7,7 @@ class DestroyedUnit:
|
||||
y: int
|
||||
name: str
|
||||
|
||||
def __init__(self, x , y, name):
|
||||
def __init__(self, x, y, name):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.name = name
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
from theater import ControlPoint
|
||||
|
||||
|
||||
class FrontlineData:
|
||||
"""
|
||||
This Data structure will store information about an existing frontline
|
||||
"""
|
||||
|
||||
def __init__(self, from_cp:ControlPoint, to_cp: ControlPoint):
|
||||
self.to_cp = to_cp
|
||||
self.from_cp = from_cp
|
||||
self.enemy_units_position = []
|
||||
self.blue_units_position = []
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import List
|
||||
|
||||
|
||||
class FactionTurnMetadata:
|
||||
"""
|
||||
Store metadata about a faction
|
||||
@@ -20,8 +21,8 @@ class GameTurnMetadata:
|
||||
Store metadata about a game turn
|
||||
"""
|
||||
|
||||
allied_units:FactionTurnMetadata
|
||||
enemy_units:FactionTurnMetadata
|
||||
allied_units: FactionTurnMetadata
|
||||
enemy_units: FactionTurnMetadata
|
||||
|
||||
def __init__(self):
|
||||
self.allied_units = FactionTurnMetadata()
|
||||
@@ -53,4 +54,3 @@ class GameStats:
|
||||
turn_data.enemy_units.vehicles_count += sum(cp.base.armor.values())
|
||||
|
||||
self.data_per_turn.append(turn_data)
|
||||
|
||||
|
||||
273
game/navmesh.py
Normal file
273
game/navmesh.py
Normal file
@@ -0,0 +1,273 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from dcs.mapping import Point
|
||||
from shapely.geometry import (
|
||||
LineString,
|
||||
MultiPolygon,
|
||||
Point as ShapelyPoint,
|
||||
Polygon,
|
||||
box,
|
||||
)
|
||||
from shapely.ops import nearest_points, triangulate
|
||||
|
||||
from game.theater import ConflictTheater
|
||||
from game.threatzones import ThreatZones
|
||||
from game.utils import nautical_miles
|
||||
|
||||
|
||||
class NavMeshError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class NavMeshPoly:
|
||||
def __init__(self, ident: int, poly: Polygon, threatened: bool) -> None:
|
||||
self.ident = ident
|
||||
self.poly = poly
|
||||
self.threatened = threatened
|
||||
self.neighbors: Dict[NavMeshPoly, Union[LineString, ShapelyPoint]] = {}
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, NavMeshPoly):
|
||||
return False
|
||||
return self.ident == other.ident
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return self.ident
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NavPoint:
|
||||
point: ShapelyPoint
|
||||
poly: NavMeshPoly
|
||||
|
||||
@property
|
||||
def world_point(self) -> Point:
|
||||
return Point(self.point.x, self.point.y)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.poly.ident)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if id(self) == id(other):
|
||||
return True
|
||||
|
||||
if not isinstance(other, NavPoint):
|
||||
return False
|
||||
|
||||
if not self.point.almost_equals(other.point):
|
||||
return False
|
||||
|
||||
return self.poly == other.poly
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.point} in {self.poly.ident}"
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class FrontierNode:
|
||||
cost: float
|
||||
point: NavPoint = field(compare=False)
|
||||
|
||||
|
||||
class NavFrontier:
|
||||
def __init__(self) -> None:
|
||||
self.nodes: List[FrontierNode] = []
|
||||
|
||||
def push(self, poly: NavPoint, cost: float) -> None:
|
||||
heapq.heappush(self.nodes, FrontierNode(cost, poly))
|
||||
|
||||
def pop(self) -> Optional[NavPoint]:
|
||||
try:
|
||||
return heapq.heappop(self.nodes).point
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
class NavMesh:
|
||||
def __init__(self, polys: List[NavMeshPoly]) -> None:
|
||||
self.polys = polys
|
||||
|
||||
def localize(self, point: Point) -> Optional[NavMeshPoly]:
|
||||
# This is a naive implementation but it's O(n). Runs at about 10k
|
||||
# lookups a second on a 5950X. Flights usually have 5-10 waypoints, so
|
||||
# that's 1k-2k flights before we lose a full second to localization as a
|
||||
# part of flight plan creation.
|
||||
#
|
||||
# Can improve the algorithm later if needed, but that seems unnecessary
|
||||
# currently.
|
||||
p = ShapelyPoint(point.x, point.y)
|
||||
for navpoly in self.polys:
|
||||
if navpoly.poly.intersects(p):
|
||||
return navpoly
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def travel_cost(a: NavPoint, b: NavPoint) -> float:
|
||||
modifier = 1.0
|
||||
if a.poly.threatened:
|
||||
modifier = 3.0
|
||||
return a.point.distance(b.point) * modifier
|
||||
|
||||
def travel_heuristic(self, a: NavPoint, b: NavPoint) -> float:
|
||||
return self.travel_cost(a, b)
|
||||
|
||||
@staticmethod
|
||||
def reconstruct_path(
|
||||
came_from: Dict[NavPoint, Optional[NavPoint]],
|
||||
origin: NavPoint,
|
||||
destination: NavPoint,
|
||||
) -> List[Point]:
|
||||
current = destination
|
||||
path: List[Point] = []
|
||||
while current != origin:
|
||||
path.append(current.world_point)
|
||||
previous = came_from[current]
|
||||
if previous is None:
|
||||
raise NavMeshError(
|
||||
f"Could not reconstruct path to {destination} from {origin}"
|
||||
)
|
||||
current = previous
|
||||
path.append(origin.world_point)
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
def dcs_to_shapely_point(point: Point) -> ShapelyPoint:
|
||||
return ShapelyPoint(point.x, point.y)
|
||||
|
||||
def shortest_path(self, origin: Point, destination: Point) -> List[Point]:
|
||||
origin_poly = self.localize(origin)
|
||||
if origin_poly is None:
|
||||
raise NavMeshError(f"Origin point {origin} is outside the navmesh")
|
||||
destination_poly = self.localize(destination)
|
||||
if destination_poly is None:
|
||||
raise NavMeshError(
|
||||
f"Destination point {destination} is outside the navmesh"
|
||||
)
|
||||
|
||||
return self._shortest_path(
|
||||
NavPoint(self.dcs_to_shapely_point(origin), origin_poly),
|
||||
NavPoint(self.dcs_to_shapely_point(destination), destination_poly),
|
||||
)
|
||||
|
||||
def _shortest_path(self, origin: NavPoint, destination: NavPoint) -> List[Point]:
|
||||
# Adapted from
|
||||
# https://www.redblobgames.com/pathfinding/a-star/implementation.py.
|
||||
frontier = NavFrontier()
|
||||
frontier.push(origin, 0.0)
|
||||
came_from: Dict[NavPoint, Optional[NavPoint]] = {origin: None}
|
||||
|
||||
best_known: Dict[NavPoint, float] = defaultdict(lambda: math.inf)
|
||||
best_known[origin] = 0.0
|
||||
|
||||
while (current := frontier.pop()) is not None:
|
||||
if current == destination:
|
||||
break
|
||||
|
||||
if current.poly == destination.poly:
|
||||
# Made it to the correct nav poly. Add the leg from the border
|
||||
# to the target.
|
||||
cost = best_known[current] + self.travel_cost(current, destination)
|
||||
if cost < best_known[destination]:
|
||||
best_known[destination] = cost
|
||||
estimated = cost
|
||||
frontier.push(destination, estimated)
|
||||
came_from[destination] = current
|
||||
|
||||
for neighbor, boundary in current.poly.neighbors.items():
|
||||
previous = came_from[current]
|
||||
if previous is not None and previous.poly == neighbor:
|
||||
# Don't backtrack.
|
||||
continue
|
||||
if previous is None and current != origin:
|
||||
raise RuntimeError
|
||||
_, neighbor_point = nearest_points(current.point, boundary)
|
||||
neighbor_nav = NavPoint(neighbor_point, neighbor)
|
||||
cost = best_known[current] + self.travel_cost(current, neighbor_nav)
|
||||
if cost < best_known[neighbor_nav]:
|
||||
best_known[neighbor_nav] = cost
|
||||
estimated = cost + self.travel_heuristic(neighbor_nav, destination)
|
||||
frontier.push(neighbor_nav, estimated)
|
||||
came_from[neighbor_nav] = current
|
||||
|
||||
return self.reconstruct_path(came_from, origin, destination)
|
||||
|
||||
@staticmethod
|
||||
def map_bounds(theater: ConflictTheater) -> Polygon:
|
||||
points = []
|
||||
for cp in theater.controlpoints:
|
||||
points.append(ShapelyPoint(cp.position.x, cp.position.y))
|
||||
for tgo in cp.ground_objects:
|
||||
points.append(ShapelyPoint(tgo.position.x, tgo.position.y))
|
||||
# Needs to be a large enough boundary beyond the known points so that
|
||||
# threatened airbases at the map edges have room to retreat from the
|
||||
# threat without running off the navmesh.
|
||||
return box(*LineString(points).bounds).buffer(
|
||||
nautical_miles(200).meters, resolution=1
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_navpolys(
|
||||
polys: List[Polygon], threat_zones: ThreatZones
|
||||
) -> List[NavMeshPoly]:
|
||||
return [
|
||||
NavMeshPoly(i, p, threat_zones.threatened(p)) for i, p in enumerate(polys)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def associate_neighbors(polys: List[NavMeshPoly]) -> None:
|
||||
# Maps (rounded) points to polygons that have a vertex at that point.
|
||||
# The points are rounded to the nearest int so we can use them as dict
|
||||
# keys. This allows us to perform approximate neighbor lookups more
|
||||
# efficiently than comparing each poly to every other poly by finding
|
||||
# approximate neighbors before checking if the polys actually touch.
|
||||
points_map: Dict[Tuple[int, int], Set[NavMeshPoly]] = defaultdict(set)
|
||||
|
||||
for navpoly in polys:
|
||||
# The coordinates of the polygon's boundary are a sequence of
|
||||
# coordinates that define the polygon. The first point is repeated
|
||||
# at the end, so skip the last vertex.
|
||||
for x, y in navpoly.poly.boundary.coords[:-1]:
|
||||
point = (int(x), int(y))
|
||||
neighbors = {}
|
||||
for potential_neighbor in points_map[point]:
|
||||
intersection = navpoly.poly.intersection(potential_neighbor.poly)
|
||||
if not intersection.is_empty:
|
||||
potential_neighbor.neighbors[navpoly] = intersection
|
||||
neighbors[potential_neighbor] = intersection
|
||||
navpoly.neighbors.update(neighbors)
|
||||
points_map[point].add(navpoly)
|
||||
|
||||
@classmethod
|
||||
def from_threat_zones(
|
||||
cls, threat_zones: ThreatZones, theater: ConflictTheater
|
||||
) -> NavMesh:
|
||||
# Simplify the threat poly to reduce the number of nav zones. Increase
|
||||
# the size of the zone and then simplify it with the buffer size as the
|
||||
# error margin. This will create a simpler poly around the threat zone.
|
||||
buffer = nautical_miles(10).meters
|
||||
threat_poly = threat_zones.all.buffer(buffer).simplify(buffer)
|
||||
|
||||
# Threat zones can be disconnected. Create a list of threat zones.
|
||||
if isinstance(threat_poly, MultiPolygon):
|
||||
polys = list(threat_poly.geoms)
|
||||
else:
|
||||
polys = [threat_poly]
|
||||
|
||||
# Subtract the threat zones from the whole-map poly to build a navmesh
|
||||
# for the *safe* areas. Navigation within threatened regions is always
|
||||
# a straight line to the target or out of the threatened region.
|
||||
bounds = cls.map_bounds(theater)
|
||||
for poly in polys:
|
||||
bounds = bounds.difference(poly)
|
||||
|
||||
# Triangulate the safe-region to build the navmesh.
|
||||
navpolys = cls.create_navpolys(triangulate(bounds), threat_zones)
|
||||
cls.associate_neighbors(navpolys)
|
||||
return cls(navpolys)
|
||||
@@ -1,38 +0,0 @@
|
||||
from dcs.terrain.terrain import Terrain
|
||||
|
||||
from gen.conflictgen import Conflict
|
||||
from .operation import Operation
|
||||
from .. import db
|
||||
|
||||
MAX_DISTANCE_BETWEEN_GROUPS = 12000
|
||||
|
||||
|
||||
class FrontlineAttackOperation(Operation):
|
||||
interceptors = None # type: db.AssignedUnitsDict
|
||||
escort = None # type: db.AssignedUnitsDict
|
||||
strikegroup = None # type: db.AssignedUnitsDict
|
||||
|
||||
attackers = None # type: db.ArmorDict
|
||||
defenders = None # type: db.ArmorDict
|
||||
|
||||
def prepare(self, terrain: Terrain, is_quick: bool):
|
||||
super(FrontlineAttackOperation, self).prepare(terrain, is_quick)
|
||||
if self.defender_name == self.game.player_name:
|
||||
self.attackers_starting_position = None
|
||||
self.defenders_starting_position = None
|
||||
|
||||
conflict = Conflict.frontline_cas_conflict(
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker=self.current_mission.country(self.attacker_country),
|
||||
defender=self.current_mission.country(self.defender_country),
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp,
|
||||
theater=self.game.theater
|
||||
)
|
||||
|
||||
self.initialize(mission=self.current_mission,
|
||||
conflict=conflict)
|
||||
|
||||
def generate(self):
|
||||
super(FrontlineAttackOperation, self).generate()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,45 +7,31 @@ from typing import Optional
|
||||
_dcs_saved_game_folder: Optional[str] = None
|
||||
_file_abs_path = None
|
||||
|
||||
|
||||
def setup(user_folder: str):
|
||||
global _dcs_saved_game_folder
|
||||
_dcs_saved_game_folder = user_folder
|
||||
_file_abs_path = os.path.join(base_path(), "default.liberation")
|
||||
|
||||
|
||||
def base_path() -> str:
|
||||
global _dcs_saved_game_folder
|
||||
assert _dcs_saved_game_folder
|
||||
return _dcs_saved_game_folder
|
||||
|
||||
def _save_file() -> str:
|
||||
return os.path.join(base_path(), "default.liberation")
|
||||
|
||||
def _temporary_save_file() -> str:
|
||||
return os.path.join(base_path(), "tmpsave.liberation")
|
||||
|
||||
|
||||
def _autosave_path() -> str:
|
||||
return os.path.join(base_path(), "autosave.liberation")
|
||||
|
||||
def _save_file_exists() -> bool:
|
||||
return os.path.exists(_save_file())
|
||||
|
||||
def mission_path_for(name: str) -> str:
|
||||
return os.path.join(base_path(), "Missions", "{}".format(name))
|
||||
|
||||
|
||||
def restore_game():
|
||||
if not _save_file_exists():
|
||||
return None
|
||||
|
||||
with open(_save_file(), "rb") as f:
|
||||
try:
|
||||
save = pickle.load(f)
|
||||
return save
|
||||
except Exception:
|
||||
logging.exception("Invalid Save game")
|
||||
return None
|
||||
|
||||
|
||||
def load_game(path):
|
||||
with open(path, "rb") as f:
|
||||
try:
|
||||
@@ -81,4 +67,3 @@ def autosave(game) -> bool:
|
||||
except Exception:
|
||||
logging.exception("Could not save game")
|
||||
return False
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
import textwrap
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
from typing import List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from game.settings import Settings
|
||||
|
||||
@@ -14,20 +14,21 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class LuaPluginWorkOrder:
|
||||
|
||||
def __init__(self, parent_mnemonic: str, filename: str, mnemonic: str,
|
||||
disable: bool) -> None:
|
||||
def __init__(
|
||||
self, parent_mnemonic: str, filename: str, mnemonic: str, disable: bool
|
||||
) -> None:
|
||||
self.parent_mnemonic = parent_mnemonic
|
||||
self.filename = filename
|
||||
self.mnemonic = mnemonic
|
||||
self.disable = disable
|
||||
|
||||
def work(self, operation: Operation) -> None:
|
||||
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)
|
||||
operation.inject_plugin_script(
|
||||
self.parent_mnemonic, self.filename, self.mnemonic
|
||||
)
|
||||
|
||||
|
||||
class PluginSettings:
|
||||
@@ -45,8 +46,7 @@ class PluginSettings:
|
||||
# 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)
|
||||
self.settings.initialize_plugin_option(self.identifier, self.enabled_by_default)
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
@@ -57,8 +57,7 @@ class PluginSettings:
|
||||
|
||||
|
||||
class LuaPluginOption(PluginSettings):
|
||||
def __init__(self, identifier: str, name: str,
|
||||
enabled_by_default: bool) -> None:
|
||||
def __init__(self, identifier: str, name: str, enabled_by_default: bool) -> None:
|
||||
super().__init__(identifier, enabled_by_default)
|
||||
self.name = name
|
||||
|
||||
@@ -80,24 +79,34 @@ class LuaPluginDefinition:
|
||||
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")
|
||||
))
|
||||
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)
|
||||
))
|
||||
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)
|
||||
))
|
||||
config_work_orders.append(
|
||||
LuaPluginWorkOrder(
|
||||
name,
|
||||
work_order.get("file"),
|
||||
work_order["mnemonic"],
|
||||
work_order.get("disable", False),
|
||||
)
|
||||
)
|
||||
|
||||
return cls(
|
||||
identifier=name,
|
||||
@@ -106,16 +115,14 @@ class LuaPluginDefinition:
|
||||
enabled_by_default=data.get("defaultValue", False),
|
||||
options=options,
|
||||
work_orders=work_orders,
|
||||
config_work_orders=config_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)
|
||||
super().__init__(self.definition.identifier, self.definition.enabled_by_default)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -144,23 +151,23 @@ class LuaPlugin(PluginSettings):
|
||||
for option in self.definition.options:
|
||||
option.set_settings(self.settings)
|
||||
|
||||
def inject_scripts(self, operation: Operation) -> None:
|
||||
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: Operation) -> None:
|
||||
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}")
|
||||
option_decls.append(f" dcsLiberation.plugins.{name} = {enabled}")
|
||||
|
||||
joined_options = "\n".join(option_decls)
|
||||
|
||||
lua = textwrap.dedent(f"""\
|
||||
lua = textwrap.dedent(
|
||||
f"""\
|
||||
-- {self.identifier} plugin configuration.
|
||||
|
||||
if dcsLiberation then
|
||||
@@ -171,10 +178,10 @@ class LuaPlugin(PluginSettings):
|
||||
{joined_options}
|
||||
end
|
||||
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
operation.inject_lua_trigger(
|
||||
lua, f"{self.identifier} plugin configuration")
|
||||
operation.inject_lua_trigger(lua, f"{self.identifier} plugin configuration")
|
||||
|
||||
for work_order in self.definition.config_work_orders:
|
||||
work_order.work(operation)
|
||||
|
||||
@@ -27,7 +27,8 @@ class LuaPluginManager:
|
||||
if not plugin_path.exists():
|
||||
raise RuntimeError(
|
||||
f"Invalid plugin configuration: required plugin {name} "
|
||||
f"does not exist at {plugin_path}")
|
||||
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:
|
||||
|
||||
15
game/point_with_heading.py
Normal file
15
game/point_with_heading.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from dcs import Point
|
||||
|
||||
|
||||
class PointWithHeading(Point):
|
||||
def __init__(self):
|
||||
super(PointWithHeading, self).__init__(0, 0)
|
||||
self.heading = 0
|
||||
|
||||
@staticmethod
|
||||
def from_point(point: Point, heading: int):
|
||||
p = PointWithHeading()
|
||||
p.x = point.x
|
||||
p.y = point.y
|
||||
p.heading = heading
|
||||
return p
|
||||
369
game/procurement.py
Normal file
369
game/procurement.py
Normal file
@@ -0,0 +1,369 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple, Type
|
||||
|
||||
from dcs.unittype import FlyingType, VehicleType
|
||||
|
||||
from game import db
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.utils import Distance
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
FRONTLINE_RESERVES_FACTOR = 1.3
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AircraftProcurementRequest:
|
||||
near: MissionTarget
|
||||
range: Distance
|
||||
task_capability: FlightType
|
||||
number: int
|
||||
|
||||
def __str__(self) -> str:
|
||||
task = self.task_capability.value
|
||||
distance = self.range.nautical_miles
|
||||
target = self.near.name
|
||||
return f"{self.number} ship {task} within {distance} nm of {target}"
|
||||
|
||||
|
||||
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.air_wing = game.air_wing_for(for_player)
|
||||
self.faction = faction
|
||||
self.manage_runways = manage_runways
|
||||
self.manage_front_line = manage_front_line
|
||||
self.manage_aircraft = manage_aircraft
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
def calculate_ground_unit_budget_share(self) -> float:
|
||||
armor_investment = 0
|
||||
aircraft_investment = 0
|
||||
|
||||
# faction has no ground units
|
||||
if (
|
||||
len(self.faction.artillery_units) == 0
|
||||
and len(self.faction.frontline_units) == 0
|
||||
):
|
||||
return 0
|
||||
|
||||
# faction has no planes
|
||||
if len(self.faction.aircrafts) == 0:
|
||||
return 1
|
||||
|
||||
for cp in self.owned_points:
|
||||
cp_ground_units = cp.allocated_ground_units(self.game.transfers)
|
||||
armor_investment += cp_ground_units.total_value
|
||||
cp_aircraft = cp.allocated_aircraft(self.game)
|
||||
aircraft_investment += cp_aircraft.total_value
|
||||
|
||||
total_investment = aircraft_investment + armor_investment
|
||||
if total_investment == 0:
|
||||
# Turn 0 or all units were destroyed. Either way, split 30/70.
|
||||
return 0.3
|
||||
|
||||
# the more planes we have, the more ground units we want and vice versa
|
||||
ground_unit_share = aircraft_investment / total_investment
|
||||
if ground_unit_share > 1.0:
|
||||
raise ValueError
|
||||
|
||||
return ground_unit_share
|
||||
|
||||
def spend_budget(self, budget: float) -> float:
|
||||
if self.manage_runways:
|
||||
budget = self.repair_runways(budget)
|
||||
if self.manage_front_line:
|
||||
armor_budget = budget * self.calculate_ground_unit_budget_share()
|
||||
budget -= armor_budget
|
||||
budget += self.reinforce_front_line(armor_budget)
|
||||
|
||||
# Don't sell overstock aircraft until after we've bought runways and
|
||||
# front lines. Any budget we free up should be earmarked for aircraft.
|
||||
if not self.is_player:
|
||||
budget += self.sell_incomplete_squadrons()
|
||||
if self.manage_aircraft:
|
||||
budget = self.purchase_aircraft(budget)
|
||||
return budget
|
||||
|
||||
def sell_incomplete_squadrons(self) -> float:
|
||||
# Selling incomplete squadrons gives us more money to spend on the next
|
||||
# turn. This serves as a short term fix for
|
||||
# https://github.com/dcs-liberation/dcs_liberation/issues/41.
|
||||
#
|
||||
# Only incomplete squadrons which are unlikely to get used will be sold
|
||||
# rather than all unused aircraft because the unused aircraft are what
|
||||
# make OCA strikes worthwhile.
|
||||
#
|
||||
# This option is only used by the AI since players cannot cancel sales
|
||||
# (https://github.com/dcs-liberation/dcs_liberation/issues/365).
|
||||
total = 0.0
|
||||
for cp in self.game.theater.control_points_for(self.is_player):
|
||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||
for aircraft, available in inventory.all_aircraft:
|
||||
# We only ever plan even groups, so the odd aircraft is unlikely
|
||||
# to get used.
|
||||
if available % 2 == 0:
|
||||
continue
|
||||
inventory.remove_aircraft(aircraft, 1)
|
||||
total += db.PRICES[aircraft]
|
||||
return total
|
||||
|
||||
def repair_runways(self, budget: float) -> float:
|
||||
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 affordable_ground_unit_of_class(
|
||||
self, budget: float, unit_class: GroundUnitClass
|
||||
) -> Optional[Type[VehicleType]]:
|
||||
faction_units = set(self.faction.frontline_units) | set(
|
||||
self.faction.artillery_units
|
||||
)
|
||||
of_class = set(unit_class.unit_list) & faction_units
|
||||
|
||||
# faction has no access to needed unit type, take a random unit
|
||||
if not of_class:
|
||||
of_class = faction_units
|
||||
|
||||
affordable_units = [u for u in of_class if db.PRICES[u] <= budget]
|
||||
if not affordable_units:
|
||||
return None
|
||||
return random.choice(affordable_units)
|
||||
|
||||
def reinforce_front_line(self, budget: float) -> float:
|
||||
if not self.faction.frontline_units and not self.faction.artillery_units:
|
||||
return budget
|
||||
|
||||
# TODO: Attempt to transfer from reserves.
|
||||
|
||||
while budget > 0:
|
||||
cp = self.ground_reinforcement_candidate()
|
||||
if cp is None:
|
||||
break
|
||||
|
||||
most_needed_type = self.most_needed_unit_class(cp)
|
||||
unit = self.affordable_ground_unit_of_class(budget, most_needed_type)
|
||||
if unit is None:
|
||||
# Can't afford any more units.
|
||||
break
|
||||
|
||||
budget -= db.PRICES[unit]
|
||||
cp.pending_unit_deliveries.order({unit: 1})
|
||||
|
||||
return budget
|
||||
|
||||
def most_needed_unit_class(self, cp: ControlPoint) -> GroundUnitClass:
|
||||
worst_balanced: Optional[GroundUnitClass] = None
|
||||
worst_fulfillment = math.inf
|
||||
for unit_class in GroundUnitClass:
|
||||
if not self.faction.has_access_to_unittype(unit_class):
|
||||
continue
|
||||
|
||||
current_ratio = self.cost_ratio_of_ground_unit(cp, unit_class)
|
||||
desired_ratio = (
|
||||
self.faction.doctrine.ground_unit_procurement_ratios.for_unit_class(
|
||||
unit_class
|
||||
)
|
||||
)
|
||||
if not desired_ratio:
|
||||
continue
|
||||
if current_ratio >= desired_ratio:
|
||||
continue
|
||||
fulfillment = current_ratio / desired_ratio
|
||||
if fulfillment < worst_fulfillment:
|
||||
worst_fulfillment = fulfillment
|
||||
worst_balanced = unit_class
|
||||
if worst_balanced is None:
|
||||
return GroundUnitClass.Tank
|
||||
return worst_balanced
|
||||
|
||||
def _affordable_aircraft_for_task(
|
||||
self,
|
||||
task: FlightType,
|
||||
airbase: ControlPoint,
|
||||
number: int,
|
||||
max_price: float,
|
||||
) -> Optional[Type[FlyingType]]:
|
||||
best_choice: Optional[Type[FlyingType]] = None
|
||||
for unit in aircraft_for_task(task):
|
||||
if unit not in self.faction.aircrafts:
|
||||
continue
|
||||
if db.PRICES[unit] * number > max_price:
|
||||
continue
|
||||
if not airbase.can_operate(unit):
|
||||
continue
|
||||
|
||||
for squadron in self.air_wing.squadrons_for(unit):
|
||||
if task in squadron.auto_assignable_mission_types:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
|
||||
# Affordable, compatible, and we have a squadron capable of the task. To
|
||||
# keep some variety, skip with a 50/50 chance. Might be a good idea to have
|
||||
# the chance to skip based on the price compared to the rest of the choices.
|
||||
best_choice = unit
|
||||
if random.choice([True, False]):
|
||||
break
|
||||
return best_choice
|
||||
|
||||
def affordable_aircraft_for(
|
||||
self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
|
||||
) -> Optional[Type[FlyingType]]:
|
||||
return self._affordable_aircraft_for_task(
|
||||
request.task_capability, airbase, request.number, budget
|
||||
)
|
||||
|
||||
def fulfill_aircraft_request(
|
||||
self, request: AircraftProcurementRequest, budget: float
|
||||
) -> Tuple[float, bool]:
|
||||
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
|
||||
airbase.pending_unit_deliveries.order({unit: request.number})
|
||||
return budget, True
|
||||
return budget, False
|
||||
|
||||
def purchase_aircraft(self, budget: float) -> float:
|
||||
for request in self.game.procurement_requests_for(self.is_player):
|
||||
if not list(self.best_airbases_for(request)):
|
||||
# No airbases in range of this request. Skip it.
|
||||
continue
|
||||
budget, fulfilled = self.fulfill_aircraft_request(request, budget)
|
||||
if not fulfilled:
|
||||
# The request was not fulfilled because we could not afford any suitable
|
||||
# aircraft. Rather than continuing, which could proceed to buy tons of
|
||||
# cheap escorts that will never allow us to plan a strike package, stop
|
||||
# buying so we can save the budget until a turn where we *can* afford to
|
||||
# fill the package.
|
||||
break
|
||||
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)
|
||||
threatened = []
|
||||
for cp in distance_cache.operational_airfields_within(request.range):
|
||||
if not cp.is_friendly(self.is_player):
|
||||
continue
|
||||
if cp.unclaimed_parking(self.game) < request.number:
|
||||
continue
|
||||
if self.threat_zones.threatened(cp.position):
|
||||
threatened.append(cp)
|
||||
yield cp
|
||||
yield from threatened
|
||||
|
||||
def ground_reinforcement_candidate(self) -> Optional[ControlPoint]:
|
||||
worst_supply = math.inf
|
||||
understaffed: Optional[ControlPoint] = None
|
||||
|
||||
# Prefer to buy front line units at active front lines that are not
|
||||
# already overloaded.
|
||||
for cp in self.owned_points:
|
||||
if not cp.has_active_frontline:
|
||||
continue
|
||||
|
||||
if not cp.has_ground_unit_source(self.game):
|
||||
# No source of ground units, so can't buy anything.
|
||||
continue
|
||||
|
||||
purchase_target = cp.frontline_unit_count_limit * FRONTLINE_RESERVES_FACTOR
|
||||
allocated = cp.allocated_ground_units(self.game.transfers)
|
||||
if allocated.total >= purchase_target:
|
||||
# Control point is already sufficiently defended.
|
||||
continue
|
||||
if allocated.total < worst_supply:
|
||||
worst_supply = allocated.total
|
||||
understaffed = cp
|
||||
|
||||
if understaffed is not None:
|
||||
return understaffed
|
||||
|
||||
# Otherwise buy reserves, but don't exceed the amount defined in the settings.
|
||||
# These units do not exist in the world until the CP becomes
|
||||
# connected to an active front line, at which point all these units
|
||||
# will suddenly appear at the gates of the newly captured CP.
|
||||
#
|
||||
# To avoid sudden overwhelming numbers of units we avoid buying
|
||||
# many.
|
||||
#
|
||||
# Also, do not bother buying units at bases that will never connect
|
||||
# to a front line.
|
||||
for cp in self.owned_points:
|
||||
if cp.is_global:
|
||||
continue
|
||||
if not cp.can_recruit_ground_units(self.game):
|
||||
continue
|
||||
|
||||
allocated = cp.allocated_ground_units(self.game.transfers)
|
||||
if allocated.total >= self.game.settings.reserves_procurement_target:
|
||||
continue
|
||||
|
||||
if allocated.total < worst_supply:
|
||||
worst_supply = allocated.total
|
||||
understaffed = cp
|
||||
|
||||
return understaffed
|
||||
|
||||
def cost_ratio_of_ground_unit(
|
||||
self, control_point: ControlPoint, unit_class: GroundUnitClass
|
||||
) -> float:
|
||||
allocations = control_point.allocated_ground_units(self.game.transfers)
|
||||
class_cost = 0
|
||||
total_cost = 0
|
||||
for unit_type, count in allocations.all.items():
|
||||
cost = db.PRICES[unit_type] * count
|
||||
total_cost += cost
|
||||
if unit_type in unit_class:
|
||||
class_cost += cost
|
||||
if not total_cost:
|
||||
return 0
|
||||
return class_cost / total_cost
|
||||
35
game/profiling.py
Normal file
35
game/profiling.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import timeit
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from datetime import timedelta
|
||||
from typing import Iterator
|
||||
|
||||
|
||||
@contextmanager
|
||||
def logged_duration(event: str) -> Iterator[None]:
|
||||
start = timeit.default_timer()
|
||||
yield
|
||||
end = timeit.default_timer()
|
||||
logging.debug("%s took %s", event, timedelta(seconds=end - start))
|
||||
|
||||
|
||||
class MultiEventTracer:
|
||||
def __init__(self) -> None:
|
||||
self.events: dict[str, timedelta] = defaultdict(timedelta)
|
||||
|
||||
def __enter__(self) -> MultiEventTracer:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
for event, duration in self.events.items():
|
||||
logging.debug("%s took %s", event, duration)
|
||||
|
||||
@contextmanager
|
||||
def trace(self, event: str) -> Iterator[None]:
|
||||
start = timeit.default_timer()
|
||||
yield
|
||||
end = timeit.default_timer()
|
||||
self.events[event] += timedelta(seconds=end - start)
|
||||
89
game/scenery_group.py
Normal file
89
game/scenery_group.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from __future__ import annotations
|
||||
from game.theater.theatergroundobject import NAME_BY_CATEGORY
|
||||
from dcs.triggers import TriggerZone
|
||||
|
||||
from typing import Iterable, List
|
||||
|
||||
|
||||
class SceneryGroupError(RuntimeError):
|
||||
"""Error for when there are insufficient conditions to create a SceneryGroup."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class SceneryGroup:
|
||||
"""Store information about a scenery objective."""
|
||||
|
||||
def __init__(
|
||||
self, zone_def: TriggerZone, zones: Iterable[TriggerZone], category: str
|
||||
) -> None:
|
||||
|
||||
self.zone_def = zone_def
|
||||
self.zones = zones
|
||||
self.position = zone_def.position
|
||||
self.category = category
|
||||
|
||||
@staticmethod
|
||||
def from_trigger_zones(trigger_zones: Iterable[TriggerZone]) -> List[SceneryGroup]:
|
||||
"""Define scenery objectives based on their encompassing blue/red circle."""
|
||||
zone_definitions = []
|
||||
white_zones = []
|
||||
|
||||
scenery_groups = []
|
||||
|
||||
# Aggregate trigger zones into different groups based on color.
|
||||
for zone in trigger_zones:
|
||||
if SceneryGroup.is_blue(zone):
|
||||
zone_definitions.append(zone)
|
||||
if SceneryGroup.is_white(zone):
|
||||
white_zones.append(zone)
|
||||
|
||||
# For each objective definition.
|
||||
for zone_def in zone_definitions:
|
||||
|
||||
zone_def_radius = zone_def.radius
|
||||
zone_def_position = zone_def.position
|
||||
zone_def_name = zone_def.name
|
||||
|
||||
if len(zone_def.properties) == 0:
|
||||
raise SceneryGroupError(
|
||||
"Undefined SceneryGroup category in TriggerZone: " + zone_def_name
|
||||
)
|
||||
|
||||
# Arbitrary campaign design requirement: First property must define the category.
|
||||
zone_def_category = zone_def.properties[1].get("value").lower()
|
||||
|
||||
valid_white_zones = []
|
||||
|
||||
for zone in list(white_zones):
|
||||
if zone.position.distance_to_point(zone_def_position) < zone_def_radius:
|
||||
valid_white_zones.append(zone)
|
||||
white_zones.remove(zone)
|
||||
|
||||
if len(valid_white_zones) > 0 and zone_def_category in NAME_BY_CATEGORY:
|
||||
scenery_groups.append(
|
||||
SceneryGroup(zone_def, valid_white_zones, zone_def_category)
|
||||
)
|
||||
elif len(valid_white_zones) == 0:
|
||||
raise SceneryGroupError(
|
||||
"No white triggerzones found in: " + zone_def_name
|
||||
)
|
||||
elif zone_def_category not in NAME_BY_CATEGORY:
|
||||
raise SceneryGroupError(
|
||||
"Incorrect TriggerZone category definition for: "
|
||||
+ zone_def_name
|
||||
+ " in campaign definition. TriggerZone category: "
|
||||
+ zone_def_category
|
||||
)
|
||||
|
||||
return scenery_groups
|
||||
|
||||
@staticmethod
|
||||
def is_blue(zone: TriggerZone) -> bool:
|
||||
# Blue in RGB is [0 Red], [0 Green], [1 Blue]. Ignore the fourth position: Transparency.
|
||||
return zone.color[1] == 0 and zone.color[2] == 0 and zone.color[3] == 1
|
||||
|
||||
@staticmethod
|
||||
def is_white(zone: TriggerZone) -> bool:
|
||||
# White in RGB is [1 Red], [1 Green], [1 Blue]. Ignore the fourth position: Transparency.
|
||||
return zone.color[1] == 1 and zone.color[2] == 1 and zone.color[3] == 1
|
||||
107
game/settings.py
107
game/settings.py
@@ -1,59 +1,84 @@
|
||||
from typing import Dict
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from enum import Enum, unique
|
||||
from typing import Dict, Optional
|
||||
|
||||
from dcs.forcedoptions import ForcedOptions
|
||||
|
||||
|
||||
@unique
|
||||
class AutoAtoBehavior(Enum):
|
||||
Disabled = "Disabled"
|
||||
Never = "Never assign player pilots"
|
||||
Default = "No preference"
|
||||
Prefer = "Prefer player pilots"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
|
||||
def __init__(self):
|
||||
# Generator settings
|
||||
self.inverted = False
|
||||
self.do_not_generate_carrier = False # TODO : implement
|
||||
self.do_not_generate_lha = False # TODO : implement
|
||||
self.do_not_generate_player_navy = True # TODO : implement
|
||||
self.do_not_generate_enemy_navy = True # TODO : implement
|
||||
# Difficulty settings
|
||||
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
|
||||
version: Optional[str] = None
|
||||
player_income_multiplier: float = 1.0
|
||||
enemy_income_multiplier: float = 1.0
|
||||
|
||||
# Difficulty settings
|
||||
self.player_skill = "Good"
|
||||
self.enemy_skill = "Average"
|
||||
self.enemy_vehicle_skill = "Average"
|
||||
self.map_coalition_visibility = "All Units"
|
||||
self.labels = "Full"
|
||||
self.only_player_takeoff = True # Legacy parameter do not use
|
||||
self.night_disabled = False
|
||||
self.external_views_allowed = True
|
||||
self.supercarrier = False
|
||||
self.multiplier = 1
|
||||
self.generate_marks = True
|
||||
self.sams = True # Legacy parameter do not use
|
||||
self.cold_start = False # Legacy parameter do not use
|
||||
self.version = None
|
||||
default_start_type: str = "Cold"
|
||||
|
||||
# Performance oriented
|
||||
self.perf_red_alert_state = True
|
||||
self.perf_smoke_gen = True
|
||||
self.perf_artillery = True
|
||||
self.perf_moving_units = True
|
||||
self.perf_infantry = True
|
||||
self.perf_ai_parking_start = True
|
||||
self.perf_destroyed_units = True
|
||||
# Mission specific
|
||||
desired_player_mission_duration: timedelta = timedelta(minutes=60)
|
||||
|
||||
# Performance culling
|
||||
self.perf_culling = False
|
||||
self.perf_culling_distance = 100
|
||||
# Campaign management
|
||||
automate_runway_repair: bool = False
|
||||
automate_front_line_reinforcements: bool = False
|
||||
automate_aircraft_reinforcements: bool = False
|
||||
restrict_weapons_by_date: bool = False
|
||||
disable_legacy_aewc: bool = True
|
||||
generate_dark_kneeboard: bool = False
|
||||
invulnerable_player_pilots: bool = True
|
||||
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default
|
||||
auto_ato_player_missions_asap: bool = True
|
||||
|
||||
# LUA Plugins system
|
||||
self.plugins: Dict[str, bool] = {}
|
||||
# Performance oriented
|
||||
perf_red_alert_state: bool = True
|
||||
perf_smoke_gen: bool = True
|
||||
perf_smoke_spacing = 1600
|
||||
perf_artillery: bool = True
|
||||
perf_moving_units: bool = True
|
||||
perf_infantry: bool = True
|
||||
perf_destroyed_units: bool = True
|
||||
reserves_procurement_target: int = 10
|
||||
|
||||
# Cheating
|
||||
self.show_red_ato = False
|
||||
# Performance culling
|
||||
perf_culling: bool = False
|
||||
perf_culling_distance: int = 100
|
||||
perf_do_not_cull_carrier = True
|
||||
|
||||
self.never_delay_player_flights = False
|
||||
# LUA Plugins system
|
||||
plugins: Dict[str, bool] = field(default_factory=dict)
|
||||
|
||||
# Cheating
|
||||
show_red_ato: bool = False
|
||||
enable_frontline_cheats: bool = False
|
||||
enable_base_capture_cheat: 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:
|
||||
def initialize_plugin_option(self, identifier: str, default_value: bool) -> None:
|
||||
try:
|
||||
self.plugin_option(identifier)
|
||||
except KeyError:
|
||||
|
||||
375
game/squadrons.py
Normal file
375
game/squadrons.py
Normal file
@@ -0,0 +1,375 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from enum import unique, Enum
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Type,
|
||||
Tuple,
|
||||
TYPE_CHECKING,
|
||||
Optional,
|
||||
Iterator,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
import yaml
|
||||
from dcs.unittype import FlyingType
|
||||
from faker import Faker
|
||||
|
||||
from game.db import flying_type_from_name
|
||||
from game.settings import AutoAtoBehavior
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PilotRecord:
|
||||
missions_flown: int = field(default=0)
|
||||
|
||||
|
||||
@unique
|
||||
class PilotStatus(Enum):
|
||||
Active = "Active"
|
||||
OnLeave = "On leave"
|
||||
Dead = "Dead"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Pilot:
|
||||
name: str
|
||||
player: bool = field(default=False)
|
||||
status: PilotStatus = field(default=PilotStatus.Active)
|
||||
record: PilotRecord = field(default_factory=PilotRecord)
|
||||
|
||||
@property
|
||||
def alive(self) -> bool:
|
||||
return self.status is not PilotStatus.Dead
|
||||
|
||||
@property
|
||||
def on_leave(self) -> bool:
|
||||
return self.status is PilotStatus.OnLeave
|
||||
|
||||
def send_on_leave(self) -> None:
|
||||
if self.status is not PilotStatus.Active:
|
||||
raise RuntimeError("Only active pilots may be sent on leave")
|
||||
self.status = PilotStatus.OnLeave
|
||||
|
||||
def return_from_leave(self) -> None:
|
||||
if self.status is not PilotStatus.OnLeave:
|
||||
raise RuntimeError("Only pilots on leave may be returned from leave")
|
||||
self.status = PilotStatus.Active
|
||||
|
||||
def kill(self) -> None:
|
||||
self.status = PilotStatus.Dead
|
||||
|
||||
@classmethod
|
||||
def random(cls, faker: Faker) -> Pilot:
|
||||
return Pilot(faker.name())
|
||||
|
||||
|
||||
@dataclass
|
||||
class Squadron:
|
||||
name: str
|
||||
nickname: Optional[str]
|
||||
country: str
|
||||
role: str
|
||||
aircraft: Type[FlyingType]
|
||||
livery: Optional[str]
|
||||
mission_types: tuple[FlightType, ...]
|
||||
pilots: list[Pilot]
|
||||
available_pilots: list[Pilot] = field(init=False, hash=False, compare=False)
|
||||
auto_assignable_mission_types: set[FlightType] = field(
|
||||
init=False, hash=False, compare=False
|
||||
)
|
||||
|
||||
# We need a reference to the Game so that we can access the Faker without needing to
|
||||
# persist it to the save game, or having to reconstruct it (it's not cheap) each
|
||||
# time we create or load a squadron.
|
||||
game: Game = field(hash=False, compare=False)
|
||||
player: bool
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.available_pilots = list(self.active_pilots)
|
||||
self.auto_assignable_mission_types = set(self.mission_types)
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.nickname is None:
|
||||
return self.name
|
||||
return f'{self.name} "{self.nickname}"'
|
||||
|
||||
def claim_available_pilot(self) -> Optional[Pilot]:
|
||||
# No pilots available, so the preference is irrelevant. Create a new pilot and
|
||||
# return it.
|
||||
if not self.available_pilots:
|
||||
self.enlist_new_pilots(1)
|
||||
return self.available_pilots.pop()
|
||||
|
||||
# For opfor, so player/AI option is irrelevant.
|
||||
if not self.player:
|
||||
return self.available_pilots.pop()
|
||||
|
||||
preference = self.game.settings.auto_ato_behavior
|
||||
|
||||
# No preference, so the first pilot is fine.
|
||||
if preference is AutoAtoBehavior.Default:
|
||||
return self.available_pilots.pop()
|
||||
|
||||
prefer_players = preference is AutoAtoBehavior.Prefer
|
||||
for pilot in self.available_pilots:
|
||||
if pilot.player == prefer_players:
|
||||
self.available_pilots.remove(pilot)
|
||||
return pilot
|
||||
|
||||
# No pilot was found that matched the user's preference.
|
||||
#
|
||||
# If they chose to *never* assign players and only players remain in the pool,
|
||||
# we cannot fill the slot with the available pilots. Recruit a new one.
|
||||
#
|
||||
# If they prefer players and we're out of players, just return an AI pilot.
|
||||
if not prefer_players:
|
||||
self.enlist_new_pilots(1)
|
||||
return self.available_pilots.pop()
|
||||
|
||||
def claim_pilot(self, pilot: Pilot) -> None:
|
||||
if pilot not in self.available_pilots:
|
||||
raise ValueError(
|
||||
f"Cannot assign {pilot} to {self} because they are not available"
|
||||
)
|
||||
self.available_pilots.remove(pilot)
|
||||
|
||||
def return_pilot(self, pilot: Pilot) -> None:
|
||||
self.available_pilots.append(pilot)
|
||||
|
||||
def return_pilots(self, pilots: Sequence[Pilot]) -> None:
|
||||
# Return in reverse so that returning two pilots and then getting two more
|
||||
# results in the same ordering. This happens commonly when resetting rosters in
|
||||
# the UI, when we clear the roster because the UI is updating, then end up
|
||||
# repopulating the same size flight from the same squadron.
|
||||
self.available_pilots.extend(reversed(pilots))
|
||||
|
||||
def enlist_new_pilots(self, count: int) -> None:
|
||||
new_pilots = [Pilot(self.faker.name()) for _ in range(count)]
|
||||
self.pilots.extend(new_pilots)
|
||||
self.available_pilots.extend(new_pilots)
|
||||
|
||||
def return_all_pilots(self) -> None:
|
||||
self.available_pilots = list(self.active_pilots)
|
||||
|
||||
@property
|
||||
def faker(self) -> Faker:
|
||||
return self.game.faker_for(self.player)
|
||||
|
||||
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
|
||||
return [p for p in self.pilots if p.status == status]
|
||||
|
||||
def _pilots_without_status(self, status: PilotStatus) -> list[Pilot]:
|
||||
return [p for p in self.pilots if p.status != status]
|
||||
|
||||
@property
|
||||
def active_pilots(self) -> list[Pilot]:
|
||||
return self._pilots_with_status(PilotStatus.Active)
|
||||
|
||||
@property
|
||||
def pilots_on_leave(self) -> list[Pilot]:
|
||||
return self._pilots_with_status(PilotStatus.OnLeave)
|
||||
|
||||
@property
|
||||
def number_of_pilots_including_dead(self) -> int:
|
||||
return len(self.pilots)
|
||||
|
||||
@property
|
||||
def number_of_living_pilots(self) -> int:
|
||||
return len(self._pilots_without_status(PilotStatus.Dead))
|
||||
|
||||
def pilot_at_index(self, index: int) -> Pilot:
|
||||
return self.pilots[index]
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron:
|
||||
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
with path.open() as squadron_file:
|
||||
data = yaml.safe_load(squadron_file)
|
||||
|
||||
unit_type = flying_type_from_name(data["aircraft"])
|
||||
if unit_type is None:
|
||||
raise KeyError(f"Could not find any aircraft with the ID {unit_type}")
|
||||
|
||||
pilots = [Pilot(n, player=False) for n in data.get("pilots", [])]
|
||||
pilots.extend([Pilot(n, player=True) for n in data.get("players", [])])
|
||||
|
||||
mission_types = [FlightType.from_name(n) for n in data["mission_types"]]
|
||||
tasks = tasks_for_aircraft(unit_type)
|
||||
for mission_type in list(mission_types):
|
||||
if mission_type not in tasks:
|
||||
logging.error(
|
||||
f"Squadron has mission type {mission_type} but {unit_type} is not "
|
||||
f"capable of that task: {path}"
|
||||
)
|
||||
mission_types.remove(mission_type)
|
||||
|
||||
return Squadron(
|
||||
name=data["name"],
|
||||
nickname=data.get("nickname"),
|
||||
country=data["country"],
|
||||
role=data["role"],
|
||||
aircraft=unit_type,
|
||||
livery=data.get("livery"),
|
||||
mission_types=tuple(mission_types),
|
||||
pilots=pilots,
|
||||
game=game,
|
||||
player=player,
|
||||
)
|
||||
|
||||
def __setstate__(self, state) -> None:
|
||||
# TODO: Remove save compat.
|
||||
if "auto_assignable_mission_types" not in state:
|
||||
state["auto_assignable_mission_types"] = set(state["mission_types"])
|
||||
self.__dict__.update(state)
|
||||
|
||||
|
||||
class SquadronLoader:
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
self.game = game
|
||||
self.player = player
|
||||
|
||||
@staticmethod
|
||||
def squadron_directories() -> Iterator[Path]:
|
||||
from game import persistency
|
||||
|
||||
yield Path(persistency.base_path()) / "Liberation/Squadrons"
|
||||
yield Path("resources/squadrons")
|
||||
|
||||
def load(self) -> dict[Type[FlyingType], list[Squadron]]:
|
||||
squadrons: dict[Type[FlyingType], list[Squadron]] = defaultdict(list)
|
||||
country = self.game.country_for(self.player)
|
||||
faction = self.game.faction_for(self.player)
|
||||
any_country = country.startswith("Combined Joint Task Forces ")
|
||||
for directory in self.squadron_directories():
|
||||
for path, squadron in self.load_squadrons_from(directory):
|
||||
if not any_country and squadron.country != country:
|
||||
logging.debug(
|
||||
"Not using squadron for non-matching country (is "
|
||||
f"{squadron.country}, need {country}: {path}"
|
||||
)
|
||||
continue
|
||||
if squadron.aircraft not in faction.aircrafts:
|
||||
logging.debug(
|
||||
f"Not using squadron because {faction.name} cannot use "
|
||||
f"{squadron.aircraft}: {path}"
|
||||
)
|
||||
continue
|
||||
logging.debug(
|
||||
f"Found {squadron.name} {squadron.aircraft} {squadron.role} "
|
||||
f"compatible with {faction.name}"
|
||||
)
|
||||
squadrons[squadron.aircraft].append(squadron)
|
||||
# Convert away from defaultdict because defaultdict doesn't unpickle so we don't
|
||||
# want it in the save state.
|
||||
return dict(squadrons)
|
||||
|
||||
def load_squadrons_from(self, directory: Path) -> Iterator[Tuple[Path, Squadron]]:
|
||||
logging.debug(f"Looking for factions in {directory}")
|
||||
# First directory level is the aircraft type so that historical squadrons that
|
||||
# have flown multiple airframes can be defined as many times as needed. The main
|
||||
# load() method is responsible for filtering out squadrons that aren't
|
||||
# compatible with the faction.
|
||||
for squadron_path in directory.glob("*/*.yaml"):
|
||||
try:
|
||||
yield squadron_path, Squadron.from_yaml(
|
||||
squadron_path, self.game, self.player
|
||||
)
|
||||
except Exception as ex:
|
||||
raise RuntimeError(
|
||||
f"Failed to load squadron defined by {squadron_path}"
|
||||
) from ex
|
||||
|
||||
|
||||
class AirWing:
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
|
||||
|
||||
self.game = game
|
||||
self.player = player
|
||||
self.squadrons = SquadronLoader(game, player).load()
|
||||
|
||||
count = itertools.count(1)
|
||||
for aircraft in game.faction_for(player).aircrafts:
|
||||
if aircraft in self.squadrons:
|
||||
continue
|
||||
self.squadrons[aircraft] = [
|
||||
Squadron(
|
||||
name=f"Squadron {next(count):03}",
|
||||
nickname=self.random_nickname(),
|
||||
country=game.country_for(player),
|
||||
role="Flying Squadron",
|
||||
aircraft=aircraft,
|
||||
livery=None,
|
||||
mission_types=tuple(tasks_for_aircraft(aircraft)),
|
||||
pilots=[],
|
||||
game=game,
|
||||
player=player,
|
||||
)
|
||||
]
|
||||
|
||||
def squadrons_for(self, aircraft: Type[FlyingType]) -> Sequence[Squadron]:
|
||||
return self.squadrons[aircraft]
|
||||
|
||||
def squadrons_for_task(self, task: FlightType) -> Iterator[Squadron]:
|
||||
for squadron in self.iter_squadrons():
|
||||
if task in squadron.mission_types:
|
||||
yield squadron
|
||||
|
||||
def squadron_for(self, aircraft: Type[FlyingType]) -> Squadron:
|
||||
return self.squadrons_for(aircraft)[0]
|
||||
|
||||
def iter_squadrons(self) -> Iterator[Squadron]:
|
||||
return itertools.chain.from_iterable(self.squadrons.values())
|
||||
|
||||
def squadron_at_index(self, index: int) -> Squadron:
|
||||
return list(self.iter_squadrons())[index]
|
||||
|
||||
def reset(self) -> None:
|
||||
for squadron in self.iter_squadrons():
|
||||
squadron.return_all_pilots()
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return sum(len(s) for s in self.squadrons.values())
|
||||
|
||||
@staticmethod
|
||||
def _make_random_nickname() -> str:
|
||||
from gen.naming import ANIMALS
|
||||
|
||||
animal = random.choice(ANIMALS)
|
||||
adjective = random.choice(
|
||||
(
|
||||
None,
|
||||
"Red",
|
||||
"Blue",
|
||||
"Green",
|
||||
"Golden",
|
||||
"Black",
|
||||
"Fighting",
|
||||
"Flying",
|
||||
)
|
||||
)
|
||||
if adjective is None:
|
||||
return animal.title()
|
||||
return f"{adjective} {animal}".title()
|
||||
|
||||
def random_nickname(self) -> str:
|
||||
while True:
|
||||
nickname = self._make_random_nickname()
|
||||
for squadron in self.iter_squadrons():
|
||||
if squadron.nickname == nickname:
|
||||
break
|
||||
else:
|
||||
return nickname
|
||||
@@ -3,3 +3,4 @@ from .conflicttheater import *
|
||||
from .controlpoint import *
|
||||
from .frontline import FrontLine
|
||||
from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import SamGroundObject
|
||||
@@ -4,12 +4,12 @@ import math
|
||||
import typing
|
||||
from typing import Dict, Type
|
||||
|
||||
from dcs.planes import PlaneType
|
||||
from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task
|
||||
from dcs.unittype import UnitType, VehicleType
|
||||
from dcs.task import AWACS, CAP, CAS, Embarking, PinpointStrike, Task, Transport
|
||||
from dcs.unittype import FlyingType, UnitType, VehicleType
|
||||
from dcs.vehicles import AirDefence, Armor
|
||||
|
||||
from game import db
|
||||
from game.db import PRICES
|
||||
|
||||
STRENGTH_AA_ASSEMBLE_MIN = 0.2
|
||||
PLANES_SCRAMBLE_MIN_BASE = 2
|
||||
@@ -21,48 +21,74 @@ BASE_MIN_STRENGTH = 0
|
||||
|
||||
|
||||
class Base:
|
||||
aircraft = {} # type: typing.Dict[PlaneType, int]
|
||||
armor = {} # type: typing.Dict[VehicleType, int]
|
||||
aa = {} # type: typing.Dict[AirDefence, int]
|
||||
strength = 1 # type: float
|
||||
|
||||
def __init__(self):
|
||||
self.aircraft = {}
|
||||
self.armor = {}
|
||||
self.aa = {}
|
||||
self.aircraft: Dict[Type[FlyingType], int] = {}
|
||||
self.armor: Dict[Type[VehicleType], int] = {}
|
||||
# TODO: Appears unused.
|
||||
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
|
||||
def total_armor(self) -> int:
|
||||
return sum(self.armor.values())
|
||||
|
||||
@property
|
||||
def total_armor_value(self) -> int:
|
||||
total = 0
|
||||
for unit_type, count in self.armor.items():
|
||||
try:
|
||||
total += PRICES[unit_type] * count
|
||||
except KeyError:
|
||||
logging.exception(f"No price found for {unit_type.id}")
|
||||
return total
|
||||
|
||||
@property
|
||||
def total_aa(self) -> int:
|
||||
return sum(self.aa.values())
|
||||
|
||||
def total_units(self, task: Task) -> int:
|
||||
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t in db.UNIT_BY_TASK[task]])
|
||||
return sum(
|
||||
[
|
||||
c
|
||||
for t, c in itertools.chain(
|
||||
self.aircraft.items(), self.armor.items(), self.aa.items()
|
||||
)
|
||||
if t in db.UNIT_BY_TASK[task]
|
||||
]
|
||||
)
|
||||
|
||||
def total_units_of_type(self, unit_type) -> int:
|
||||
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t == unit_type])
|
||||
return sum(
|
||||
[
|
||||
c
|
||||
for t, c in itertools.chain(
|
||||
self.aircraft.items(), self.armor.items(), self.aa.items()
|
||||
)
|
||||
if t == unit_type
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def all_units(self):
|
||||
return itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items())
|
||||
return itertools.chain(
|
||||
self.aircraft.items(), self.armor.items(), self.aa.items()
|
||||
)
|
||||
|
||||
def _find_best_unit(self, available_units: Dict[UnitType, int],
|
||||
for_type: Task, count: int) -> Dict[UnitType, int]:
|
||||
def _find_best_unit(
|
||||
self, available_units: Dict[UnitType, int], for_type: Task, count: int
|
||||
) -> Dict[UnitType, int]:
|
||||
if count <= 0:
|
||||
logging.warning("{}: no units for {}".format(self, for_type))
|
||||
return {}
|
||||
|
||||
sorted_units = [key for key in available_units 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: Dict[UnitType, int] = {}
|
||||
@@ -83,14 +109,18 @@ class Base:
|
||||
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]:
|
||||
return self._find_best_unit(self.armor, for_type, count)
|
||||
|
||||
def append_commision_points(self, for_type, points: float) -> int:
|
||||
self.commision_points[for_type] = self.commision_points.get(for_type, 0) + points
|
||||
self.commision_points[for_type] = (
|
||||
self.commision_points.get(for_type, 0) + points
|
||||
)
|
||||
points = self.commision_points[for_type]
|
||||
if points >= 1:
|
||||
self.commision_points[for_type] = points - math.floor(points)
|
||||
@@ -99,27 +129,37 @@ class Base:
|
||||
return 0
|
||||
|
||||
def filter_units(self, applicable_units: typing.Collection):
|
||||
self.aircraft = {k: v for k, v in self.aircraft.items() if k in applicable_units}
|
||||
self.aircraft = {
|
||||
k: v for k, v in self.aircraft.items() if k in applicable_units
|
||||
}
|
||||
self.armor = {k: v for k, v in self.armor.items() if k in applicable_units}
|
||||
|
||||
def commision_units(self, units: typing.Dict[typing.Any, int]):
|
||||
for value in units.values():
|
||||
assert value > 0
|
||||
assert value == math.floor(value)
|
||||
|
||||
for unit_type, unit_count in units.items():
|
||||
if unit_count <= 0:
|
||||
continue
|
||||
|
||||
for_task = db.unit_task(unit_type)
|
||||
|
||||
target_dict = None
|
||||
if for_task == CAS or for_task == CAP or for_task == Embarking:
|
||||
if (
|
||||
for_task == AWACS
|
||||
or for_task == CAS
|
||||
or for_task == CAP
|
||||
or for_task == Embarking
|
||||
or for_task == Transport
|
||||
):
|
||||
target_dict = self.aircraft
|
||||
elif for_task == PinpointStrike:
|
||||
target_dict = self.armor
|
||||
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]):
|
||||
|
||||
@@ -136,7 +176,7 @@ class Base:
|
||||
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)
|
||||
if target_array[unit_type] == 0:
|
||||
del target_array[unit_type]
|
||||
@@ -153,12 +193,20 @@ class Base:
|
||||
|
||||
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])
|
||||
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)
|
||||
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 * 0.5)
|
||||
@@ -167,18 +215,18 @@ class Base:
|
||||
# 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_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_planes, 20))
|
||||
return self._find_best_planes(CAP, min(self.total_aircraft, 20))
|
||||
|
||||
def scramble_cas(self, multiplier: float) -> typing.Dict[PlaneType, int]:
|
||||
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_attack(self) -> typing.Dict[Armor, int]:
|
||||
@@ -189,4 +237,8 @@ class Base:
|
||||
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())
|
||||
return self._find_best_unit(
|
||||
self.aa,
|
||||
AirDefence,
|
||||
count and min(count, self.total_aa) or self.assemble_aa_count(),
|
||||
)
|
||||
26
game/theater/bullseye.py
Normal file
26
game/theater/bullseye.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
|
||||
from game.theater import LatLon
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater
|
||||
|
||||
|
||||
@dataclass
|
||||
class Bullseye:
|
||||
position: Point
|
||||
|
||||
@classmethod
|
||||
def from_pydcs(cls, bulls: Dict[str, float]) -> Bullseye:
|
||||
return cls(Point(bulls["x"], bulls["y"]))
|
||||
|
||||
def to_pydcs(self) -> Dict[str, float]:
|
||||
return {"x": self.position.x, "y": self.position.y}
|
||||
|
||||
def to_lat_lon(self, theater: ConflictTheater) -> LatLon:
|
||||
return theater.point_to_ll(self.position)
|
||||
8
game/theater/caucasus.py
Normal file
8
game/theater/caucasus.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from game.theater.projections import TransverseMercator
|
||||
|
||||
PARAMETERS = TransverseMercator(
|
||||
central_meridian=33,
|
||||
false_easting=-99516.9999999732,
|
||||
false_northing=-4998114.999999984,
|
||||
scale_factor=0.9996,
|
||||
)
|
||||
858
game/theater/conflicttheater.py
Normal file
858
game/theater/conflicttheater.py
Normal file
@@ -0,0 +1,858 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
||||
|
||||
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 (
|
||||
Bulker_Handy_Wind,
|
||||
CVN_74_John_C__Stennis,
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
LHA_1_Tarawa,
|
||||
)
|
||||
from dcs.statics import Fortification, Warehouse
|
||||
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 pyproj import CRS, Transformer
|
||||
from shapely import geometry, ops
|
||||
|
||||
from .controlpoint import (
|
||||
Airfield,
|
||||
Carrier,
|
||||
ControlPoint,
|
||||
Fob,
|
||||
Lha,
|
||||
MissionTarget,
|
||||
OffMapSpawn,
|
||||
)
|
||||
from .frontline import FrontLine
|
||||
from .landmap import Landmap, load_landmap, poly_contains
|
||||
from .latlon import LatLon
|
||||
from .projections import TransverseMercator
|
||||
from ..point_with_heading import PointWithHeading
|
||||
from ..profiling import logged_duration
|
||||
from ..scenery_group import SceneryGroup
|
||||
from ..utils import Distance, meters
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
SHIPPING_LANE_UNIT_TYPE = Bulker_Handy_Wind.id
|
||||
|
||||
FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id
|
||||
FARP_HELIPAD = "SINGLE_HELIPAD"
|
||||
|
||||
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
|
||||
SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id
|
||||
MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id
|
||||
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id
|
||||
|
||||
# Multiple options for air defenses so campaign designers can more accurately see
|
||||
# the coverage of their IADS for the expected type.
|
||||
LONG_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.SAM_Patriot_LN.id,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D.id,
|
||||
}
|
||||
|
||||
MEDIUM_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.SAM_Hawk_LN_M192.id,
|
||||
AirDefence.SAM_SA_2_S_75_Guideline_LN.id,
|
||||
AirDefence.SAM_SA_3_S_125_Goa_LN.id,
|
||||
}
|
||||
|
||||
SHORT_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.SAM_Avenger__Stinger.id,
|
||||
AirDefence.SAM_Rapier_LN.id,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison.id,
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL.id,
|
||||
}
|
||||
|
||||
AAA_UNIT_TYPES = {
|
||||
AirDefence.AAA_8_8cm_Flak_18.id,
|
||||
AirDefence.SPAAA_Vulcan_M163.id,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id,
|
||||
}
|
||||
|
||||
EWR_UNIT_TYPE = AirDefence.EWR_1L13.id
|
||||
|
||||
ARMOR_GROUP_UNIT_TYPE = Armor.MBT_M1A2_Abrams.id
|
||||
|
||||
FACTORY_UNIT_TYPE = Fortification.Workshop_A.id
|
||||
|
||||
AMMUNITION_DEPOT_UNIT_TYPE = Warehouse.Ammunition_depot.id
|
||||
|
||||
STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
|
||||
|
||||
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
|
||||
self.theater = theater
|
||||
self.mission = Mission()
|
||||
with logged_duration("Loading miz"):
|
||||
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.red.ship_group:
|
||||
if group.units[0].type == self.SHIP_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def offshore_strike_targets(self) -> Iterator[StaticGroup]:
|
||||
for group in self.red.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.red.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.red.vehicle_group:
|
||||
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def long_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.LONG_RANGE_SAM_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def medium_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.MEDIUM_RANGE_SAM_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def short_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.SHORT_RANGE_SAM_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def aaa(self) -> Iterator[VehicleGroup]:
|
||||
for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group):
|
||||
if group.units[0].type in self.AAA_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ewrs(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.EWR_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def armor_groups(self) -> Iterator[VehicleGroup]:
|
||||
for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group):
|
||||
if group.units[0].type in self.ARMOR_GROUP_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def helipads(self) -> Iterator[StaticGroup]:
|
||||
for group in self.blue.static_group:
|
||||
if group.units[0].type == self.FARP_HELIPAD:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def factories(self) -> Iterator[StaticGroup]:
|
||||
for group in self.blue.static_group:
|
||||
if group.units[0].type in self.FACTORY_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ammunition_depots(self) -> Iterator[StaticGroup]:
|
||||
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
||||
if group.units[0].type in self.AMMUNITION_DEPOT_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> Iterator[StaticGroup]:
|
||||
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
||||
if group.units[0].type in self.STRIKE_TARGET_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def scenery(self) -> List[SceneryGroup]:
|
||||
return SceneryGroup.from_trigger_zones(self.mission.triggers._zones)
|
||||
|
||||
@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.db
|
||||
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
|
||||
|
||||
@property
|
||||
def shipping_lane_groups(self) -> Iterator[ShipGroup]:
|
||||
for group in self.country(blue=True).ship_group:
|
||||
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def add_supply_routes(self) -> None:
|
||||
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. Each waypoint defines the path of the
|
||||
# cargo ship.
|
||||
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}"
|
||||
)
|
||||
|
||||
self.control_points[origin.id].create_convoy_route(destination, waypoints)
|
||||
self.control_points[destination.id].create_convoy_route(
|
||||
origin, list(reversed(waypoints))
|
||||
)
|
||||
|
||||
def add_shipping_lanes(self) -> None:
|
||||
for group in self.shipping_lane_groups:
|
||||
# The unit will have its first waypoint at the source CP and the final
|
||||
# waypoint at the destination CP. Each waypoint defines the path of the
|
||||
# cargo ship.
|
||||
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}"
|
||||
)
|
||||
|
||||
self.control_points[origin.id].create_shipping_lane(destination, waypoints)
|
||||
self.control_points[destination.id].create_shipping_lane(
|
||||
origin, list(reversed(waypoints))
|
||||
)
|
||||
|
||||
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
|
||||
closest = self.theater.closest_control_point(group.position)
|
||||
distance = meters(closest.position.distance_to_point(group.position))
|
||||
return closest, distance
|
||||
|
||||
def add_preset_locations(self) -> None:
|
||||
for group in self.offshore_strike_targets:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.offshore_strike_locations.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.ships:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.ships.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.missile_sites:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.missile_sites.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.coastal_defenses:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.coastal_defenses.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.long_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.long_range_sams.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.medium_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.medium_range_sams.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.short_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.short_range_sams.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.aaa:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.aaa.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.ewrs:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.ewrs.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.armor_groups:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.armor_groups.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.helipads:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.helipads.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.factories:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.factories.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.ammunition_depots:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.ammunition_depots.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.strike_targets:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.strike_locations.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.scenery:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.scenery.append(group)
|
||||
|
||||
def populate_theater(self) -> None:
|
||||
for control_point in self.control_points.values():
|
||||
self.theater.add_controlpoint(control_point)
|
||||
self.add_preset_locations()
|
||||
self.add_supply_routes()
|
||||
self.add_shipping_lanes()
|
||||
|
||||
|
||||
@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]]
|
||||
|
||||
def __init__(self):
|
||||
self.controlpoints: List[ControlPoint] = []
|
||||
self.point_to_ll_transformer = Transformer.from_crs(
|
||||
self.projection_parameters.to_crs(), CRS("WGS84")
|
||||
)
|
||||
self.ll_to_point_transformer = Transformer.from_crs(
|
||||
CRS("WGS84"), self.projection_parameters.to_crs()
|
||||
)
|
||||
"""
|
||||
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))
|
||||
"""
|
||||
|
||||
def __getstate__(self) -> Dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
# Avoid persisting any volatile types that can be deterministically
|
||||
# recomputed on load for the sake of save compatibility.
|
||||
del state["point_to_ll_transformer"]
|
||||
del state["ll_to_point_transformer"]
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: Dict[str, Any]) -> None:
|
||||
self.__dict__.update(state)
|
||||
# Regenerate any state that was not persisted.
|
||||
self.point_to_ll_transformer = Transformer.from_crs(
|
||||
self.projection_parameters.to_crs(), CRS("WGS84")
|
||||
)
|
||||
self.ll_to_point_transformer = Transformer.from_crs(
|
||||
CRS("WGS84"), self.projection_parameters.to_crs()
|
||||
)
|
||||
|
||||
def add_controlpoint(self, point: ControlPoint):
|
||||
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.exclusion_zones:
|
||||
if poly_contains(point.x, point.y, exclusion_zone):
|
||||
return False
|
||||
|
||||
for sea in self.landmap.sea_zones:
|
||||
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
|
||||
if poly_contains(point.x, point.y, self.landmap.inclusion_zones):
|
||||
is_point_included = True
|
||||
|
||||
if not is_point_included:
|
||||
return False
|
||||
|
||||
for exclusion_zone in self.landmap.exclusion_zones:
|
||||
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.inclusion_zones:
|
||||
nearest_pair = ops.nearest_points(point, inclusion_zone)
|
||||
nearest_points.append(nearest_pair[1])
|
||||
min_distance = point.distance(nearest_points[0]) # type: geometry.Point
|
||||
nearest_point = nearest_points[0] # type: geometry.Point
|
||||
for pt in nearest_points[1:]:
|
||||
distance = point.distance(pt)
|
||||
if 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 control_points_for(self, player: bool) -> Iterator[ControlPoint]:
|
||||
for point in self.controlpoints:
|
||||
if point.captured == player:
|
||||
yield point
|
||||
|
||||
def player_points(self) -> List[ControlPoint]:
|
||||
return list(self.control_points_for(player=True))
|
||||
|
||||
def conflicts(self) -> Iterator[FrontLine]:
|
||||
for player_cp in [x for x in self.controlpoints if x.captured]:
|
||||
for enemy_cp in [
|
||||
x for x in player_cp.connected_points if not x.is_friendly_to(player_cp)
|
||||
]:
|
||||
yield FrontLine(player_cp, enemy_cp)
|
||||
|
||||
def enemy_points(self) -> List[ControlPoint]:
|
||||
return list(self.control_points_for(player=False))
|
||||
|
||||
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_target(self, point: Point) -> MissionTarget:
|
||||
closest: MissionTarget = 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
|
||||
for tgo in control_point.ground_objects:
|
||||
distance = point.distance_to_point(tgo.position)
|
||||
if distance < closest_distance:
|
||||
closest = tgo
|
||||
closest_distance = distance
|
||||
for conflict in self.conflicts():
|
||||
distance = conflict.position.distance_to_point(point)
|
||||
if distance < closest_distance:
|
||||
closest = conflict
|
||||
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)
|
||||
"""
|
||||
seen = set()
|
||||
min_distance = math.inf
|
||||
closest_blue = None
|
||||
closest_red = None
|
||||
for blue_cp in self.player_points():
|
||||
for red_cp in self.enemy_points():
|
||||
if (blue_cp, red_cp) in seen:
|
||||
continue
|
||||
seen.add((blue_cp, red_cp))
|
||||
seen.add((red_cp, blue_cp))
|
||||
|
||||
dist = red_cp.position.distance_to_point(blue_cp.position)
|
||||
if dist < min_distance:
|
||||
closest_red = red_cp
|
||||
closest_blue = blue_cp
|
||||
min_distance = dist
|
||||
|
||||
assert closest_blue is not None
|
||||
assert closest_red is not None
|
||||
return closest_blue, closest_red
|
||||
|
||||
def find_control_point_by_id(self, id: int) -> ControlPoint:
|
||||
for i in self.controlpoints:
|
||||
if i.id == id:
|
||||
return i
|
||||
raise KeyError(f"Cannot find ControlPoint with ID {id}")
|
||||
|
||||
@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 None:
|
||||
raise RuntimeError(
|
||||
"Old format (non-miz) campaigns are no longer supported."
|
||||
)
|
||||
|
||||
with logged_duration("Importing miz data"):
|
||||
MizCampaignLoader(directory / miz, t).populate_theater()
|
||||
return t
|
||||
|
||||
@property
|
||||
def projection_parameters(self) -> TransverseMercator:
|
||||
raise NotImplementedError
|
||||
|
||||
def point_to_ll(self, point: Point) -> LatLon:
|
||||
lat, lon = self.point_to_ll_transformer.transform(point.x, point.y)
|
||||
return LatLon(lat, lon)
|
||||
|
||||
def ll_to_point(self, ll: LatLon) -> Point:
|
||||
x, y = self.ll_to_point_transformer.transform(ll.latitude, ll.longitude)
|
||||
return Point(x, y)
|
||||
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
@property
|
||||
def projection_parameters(self) -> TransverseMercator:
|
||||
from .caucasus import PARAMETERS
|
||||
|
||||
return PARAMETERS
|
||||
|
||||
|
||||
class PersianGulfTheater(ConflictTheater):
|
||||
terrain = persiangulf.PersianGulf()
|
||||
overview_image = "persiangulf.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(persiangulf.Jiroft.position, Point(1692, 1343)),
|
||||
ReferencePoint(persiangulf.Liwa_AFB.position, Point(358, 3238)),
|
||||
)
|
||||
landmap = load_landmap("resources\\gulflandmap.p")
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (8, 16),
|
||||
"dusk": (16, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
@property
|
||||
def projection_parameters(self) -> TransverseMercator:
|
||||
from .persiangulf import PARAMETERS
|
||||
|
||||
return PARAMETERS
|
||||
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
@property
|
||||
def projection_parameters(self) -> TransverseMercator:
|
||||
from .nevada import PARAMETERS
|
||||
|
||||
return PARAMETERS
|
||||
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
@property
|
||||
def projection_parameters(self) -> TransverseMercator:
|
||||
from .normandy import PARAMETERS
|
||||
|
||||
return PARAMETERS
|
||||
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
@property
|
||||
def projection_parameters(self) -> TransverseMercator:
|
||||
from .thechannel import PARAMETERS
|
||||
|
||||
return PARAMETERS
|
||||
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
@property
|
||||
def projection_parameters(self) -> TransverseMercator:
|
||||
from .syria import PARAMETERS
|
||||
|
||||
return PARAMETERS
|
||||
1137
game/theater/controlpoint.py
Normal file
1137
game/theater/controlpoint.py
Normal file
File diff suppressed because it is too large
Load Diff
179
game/theater/frontline.py
Normal file
179
game/theater/frontline.py
Normal file
@@ -0,0 +1,179 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator, List, Tuple
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
from gen.flights.flight import FlightType
|
||||
from .controlpoint import (
|
||||
ControlPoint,
|
||||
MissionTarget,
|
||||
)
|
||||
from ..utils import pairwise
|
||||
|
||||
|
||||
FRONTLINE_MIN_CP_DISTANCE = 5000
|
||||
|
||||
|
||||
@dataclass
|
||||
class FrontLineSegment:
|
||||
"""
|
||||
Describes a line segment of a FrontLine
|
||||
"""
|
||||
|
||||
point_a: Point
|
||||
point_b: Point
|
||||
|
||||
@property
|
||||
def attack_heading(self) -> float:
|
||||
"""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) -> float:
|
||||
"""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,
|
||||
blue_point: ControlPoint,
|
||||
red_point: ControlPoint,
|
||||
) -> None:
|
||||
self.blue_cp = blue_point
|
||||
self.red_cp = red_point
|
||||
try:
|
||||
route = list(blue_point.convoy_route_to(red_point))
|
||||
except KeyError:
|
||||
# Some campaigns are air only and the mission generator currently relies on
|
||||
# *some* "front line" being drawn between these two. In this case there will
|
||||
# be no supply route to follow. Just create an arbitrary route between the
|
||||
# two points.
|
||||
route = [blue_point.position, red_point.position]
|
||||
# Snap the beginning and end points to the CPs rather than the convoy waypoints,
|
||||
# which are on roads.
|
||||
route[0] = blue_point.position
|
||||
route[-1] = red_point.position
|
||||
self.segments: List[FrontLineSegment] = [
|
||||
FrontLineSegment(a, b) for a, b in pairwise(route)
|
||||
]
|
||||
self.name = f"Front line {blue_point}/{red_point}"
|
||||
|
||||
def control_point_hostile_to(self, player: bool) -> ControlPoint:
|
||||
if player:
|
||||
return self.red_cp
|
||||
return self.blue_cp
|
||||
|
||||
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,
|
||||
FlightType.AEWC,
|
||||
# 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 points(self) -> Iterator[Point]:
|
||||
yield self.segments[0].point_a
|
||||
for segment in self.segments:
|
||||
yield segment.point_b
|
||||
|
||||
@property
|
||||
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
||||
"""Returns a tuple of the two control points."""
|
||||
return self.blue_cp, self.red_cp
|
||||
|
||||
@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: float) -> Point:
|
||||
"""
|
||||
Returns a point {distance} away from control_point_a along the frontline segments.
|
||||
"""
|
||||
if distance < self.segments[0].attack_distance:
|
||||
return self.blue_cp.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.blue_cp.base.strength + self.red_cp.base.strength
|
||||
if self.blue_cp.base.strength == 0:
|
||||
return self._adjust_for_min_dist(0)
|
||||
if self.red_cp.base.strength == 0:
|
||||
return self._adjust_for_min_dist(self.attack_distance)
|
||||
strength_pct = self.blue_cp.base.strength / total_strength
|
||||
return self._adjust_for_min_dist(strength_pct * self.attack_distance)
|
||||
|
||||
def _adjust_for_min_dist(self, distance: float) -> float:
|
||||
"""
|
||||
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
|
||||
48
game/theater/landmap.py
Normal file
48
game/theater/landmap.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from dataclasses import dataclass
|
||||
import pickle
|
||||
from functools import cached_property
|
||||
from typing import Optional, Tuple, Union
|
||||
import logging
|
||||
|
||||
from shapely import geometry
|
||||
from shapely.geometry import MultiPolygon, Polygon
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Landmap:
|
||||
inclusion_zones: MultiPolygon
|
||||
exclusion_zones: MultiPolygon
|
||||
sea_zones: MultiPolygon
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.inclusion_zones.is_valid:
|
||||
raise RuntimeError("Inclusion zones not valid")
|
||||
if not self.exclusion_zones.is_valid:
|
||||
raise RuntimeError("Exclusion zones not valid")
|
||||
if not self.sea_zones.is_valid:
|
||||
raise RuntimeError("Sea zones not valid")
|
||||
|
||||
@cached_property
|
||||
def inclusion_zone_only(self) -> MultiPolygon:
|
||||
return self.inclusion_zones - self.exclusion_zones - self.sea_zones
|
||||
|
||||
|
||||
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: Union[MultiPolygon, 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)
|
||||
34
game/theater/latlon.py
Normal file
34
game/theater/latlon.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LatLon:
|
||||
latitude: float
|
||||
longitude: float
|
||||
|
||||
def as_list(self) -> List[float]:
|
||||
return [self.latitude, self.longitude]
|
||||
|
||||
@staticmethod
|
||||
def _components(dimension: float) -> Tuple[int, int, float]:
|
||||
degrees = int(dimension)
|
||||
minutes = int(dimension * 60 % 60)
|
||||
seconds = dimension * 3600 % 60
|
||||
return degrees, minutes, seconds
|
||||
|
||||
def _format_component(
|
||||
self, dimension: float, hemispheres: Tuple[str, str], seconds_precision: int
|
||||
) -> str:
|
||||
hemisphere = hemispheres[0] if dimension >= 0 else hemispheres[1]
|
||||
degrees, minutes, seconds = self._components(dimension)
|
||||
return f"{degrees}°{minutes:02}'{seconds:02.{seconds_precision}f}\"{hemisphere}"
|
||||
|
||||
def format_dms(self, include_decimal_seconds: bool = False) -> str:
|
||||
precision = 2 if include_decimal_seconds else 0
|
||||
return " ".join(
|
||||
[
|
||||
self._format_component(self.latitude, ("N", "S"), precision),
|
||||
self._format_component(self.longitude, ("E", "W"), precision),
|
||||
]
|
||||
)
|
||||
49
game/theater/missiontarget.py
Normal file
49
game/theater/missiontarget.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterator, TYPE_CHECKING, List, Union
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
|
||||
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_ESCORT,
|
||||
FlightType.SWEEP,
|
||||
# TODO: FlightType.ELINT,
|
||||
# TODO: FlightType.EWAR,
|
||||
# TODO: FlightType.RECON,
|
||||
]
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
||||
return []
|
||||
8
game/theater/nevada.py
Normal file
8
game/theater/nevada.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from game.theater.projections import TransverseMercator
|
||||
|
||||
PARAMETERS = TransverseMercator(
|
||||
central_meridian=-117,
|
||||
false_easting=-193996.80999964548,
|
||||
false_northing=-4410028.063999966,
|
||||
scale_factor=0.9996,
|
||||
)
|
||||
8
game/theater/normandy.py
Normal file
8
game/theater/normandy.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from game.theater.projections import TransverseMercator
|
||||
|
||||
PARAMETERS = TransverseMercator(
|
||||
central_meridian=-3,
|
||||
false_easting=-195526.00000000204,
|
||||
false_northing=-5484812.999999951,
|
||||
scale_factor=0.9996,
|
||||
)
|
||||
8
game/theater/persiangulf.py
Normal file
8
game/theater/persiangulf.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from game.theater.projections import TransverseMercator
|
||||
|
||||
PARAMETERS = TransverseMercator(
|
||||
central_meridian=57,
|
||||
false_easting=75755.99999999645,
|
||||
false_northing=-2894933.0000000377,
|
||||
scale_factor=0.9996,
|
||||
)
|
||||
31
game/theater/projections.py
Normal file
31
game/theater/projections.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pyproj import CRS
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TransverseMercator:
|
||||
central_meridian: int
|
||||
false_easting: float
|
||||
false_northing: float
|
||||
scale_factor: float
|
||||
|
||||
def to_crs(self) -> CRS:
|
||||
return CRS.from_proj4(
|
||||
" ".join(
|
||||
[
|
||||
"+proj=tmerc",
|
||||
"+lat_0=0",
|
||||
f"+lon_0={self.central_meridian}",
|
||||
f"+k_0={self.scale_factor}",
|
||||
f"+x_0={self.false_easting}",
|
||||
f"+y_0={self.false_northing}",
|
||||
"+towgs84=0,0,0,0,0,0,0",
|
||||
"+units=m",
|
||||
"+vunits=m",
|
||||
"+ellps=WGS84",
|
||||
"+no_defs",
|
||||
"+axis=neu",
|
||||
]
|
||||
)
|
||||
)
|
||||
624
game/theater/start_generator.py
Normal file
624
game/theater/start_generator.py
Normal file
@@ -0,0 +1,624 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import pickle
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Iterable, List, 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.scenery_group import SceneryGroup
|
||||
from game.theater import Carrier, Lha, PointWithHeading
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
EwrGroundObject,
|
||||
FactoryGroundObject,
|
||||
LhaGroundObject,
|
||||
MissileSiteGroundObject,
|
||||
SamGroundObject,
|
||||
ShipGroundObject,
|
||||
SceneryGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
CoastalSiteGroundObject,
|
||||
)
|
||||
from game.version import VERSION
|
||||
from gen import namegen
|
||||
from gen.coastal.coastal_group_generator import generate_coastal_group
|
||||
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.missiles.missiles_group_generator import generate_missile_group
|
||||
from gen.sam.airdefensegroupgenerator import AirDefenseRange
|
||||
from gen.sam.ewr_group_generator import generate_ewr_group
|
||||
from gen.sam.sam_group_generator import generate_anti_air_group
|
||||
from . import (
|
||||
ConflictTheater,
|
||||
ControlPoint,
|
||||
ControlPointType,
|
||||
Fob,
|
||||
OffMapSpawn,
|
||||
)
|
||||
from ..profiling import logged_duration
|
||||
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:
|
||||
with logged_duration("TGO population"):
|
||||
# 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
|
||||
game.begin_turn_0()
|
||||
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 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
|
||||
|
||||
@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.
|
||||
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 position in self.control_point.preset_locations.ships:
|
||||
self.generate_ship_at(position)
|
||||
|
||||
def generate_ship_at(self, position: PointWithHeading) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = ShipGroundObject(
|
||||
namegen.random_objective_name(), group_id, position, 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 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
|
||||
|
||||
self.generate_ground_points()
|
||||
return True
|
||||
|
||||
def generate_ground_points(self) -> None:
|
||||
"""Generate ground objects and AA sites for the control point."""
|
||||
self.generate_armor_groups()
|
||||
self.generate_aa()
|
||||
self.generate_ewrs()
|
||||
self.generate_scenery_sites()
|
||||
self.generate_strike_targets()
|
||||
self.generate_offshore_strike_targets()
|
||||
self.generate_factories()
|
||||
self.generate_ammunition_depots()
|
||||
|
||||
if self.faction.missiles:
|
||||
self.generate_missile_sites()
|
||||
|
||||
if self.faction.coastal_defenses:
|
||||
self.generate_coastal_sites()
|
||||
|
||||
def generate_armor_groups(self) -> None:
|
||||
for position in self.control_point.preset_locations.armor_groups:
|
||||
self.generate_armor_at(position)
|
||||
|
||||
def generate_armor_at(self, position: PointWithHeading) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = VehicleGroupGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
)
|
||||
|
||||
group = generate_armor_group(self.faction_name, self.game, g)
|
||||
if group is None:
|
||||
logging.error(
|
||||
"Could not generate armor group for %s at %s",
|
||||
g.name,
|
||||
self.control_point,
|
||||
)
|
||||
return
|
||||
g.groups = [group]
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_aa(self) -> None:
|
||||
presets = self.control_point.preset_locations
|
||||
for position in presets.long_range_sams:
|
||||
self.generate_aa_at(
|
||||
position,
|
||||
ranges=[
|
||||
{AirDefenseRange.Long},
|
||||
{AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
{AirDefenseRange.AAA},
|
||||
],
|
||||
)
|
||||
for position in presets.medium_range_sams:
|
||||
self.generate_aa_at(
|
||||
position,
|
||||
ranges=[
|
||||
{AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
{AirDefenseRange.AAA},
|
||||
],
|
||||
)
|
||||
for position in presets.short_range_sams:
|
||||
self.generate_aa_at(
|
||||
position,
|
||||
ranges=[{AirDefenseRange.Short}, {AirDefenseRange.AAA}],
|
||||
)
|
||||
for position in presets.aaa:
|
||||
self.generate_aa_at(
|
||||
position,
|
||||
ranges=[{AirDefenseRange.AAA}],
|
||||
)
|
||||
|
||||
def generate_ewrs(self) -> None:
|
||||
presets = self.control_point.preset_locations
|
||||
for position in presets.ewrs:
|
||||
self.generate_ewr_at(position)
|
||||
|
||||
def generate_strike_target_at(self, category: str, position: Point) -> None:
|
||||
|
||||
obj_name = namegen.random_objective_name()
|
||||
template = random.choice(list(self.templates[category].values()))
|
||||
|
||||
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,
|
||||
position + template_point,
|
||||
unit["heading"],
|
||||
self.control_point,
|
||||
unit["type"],
|
||||
)
|
||||
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_ammunition_depots(self) -> None:
|
||||
for position in self.control_point.preset_locations.ammunition_depots:
|
||||
self.generate_strike_target_at(category="ammo", position=position)
|
||||
|
||||
def generate_factories(self) -> None:
|
||||
for position in self.control_point.preset_locations.factories:
|
||||
self.generate_factory_at(position)
|
||||
|
||||
def generate_factory_at(self, point: PointWithHeading) -> None:
|
||||
obj_name = namegen.random_objective_name()
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = FactoryGroundObject(
|
||||
obj_name,
|
||||
group_id,
|
||||
point,
|
||||
point.heading,
|
||||
self.control_point,
|
||||
)
|
||||
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
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,
|
||||
)
|
||||
groups = generate_anti_air_group(self.game, g, self.faction, ranges)
|
||||
if not groups:
|
||||
logging.error(
|
||||
"Could not generate air defense group for %s at %s",
|
||||
g.name,
|
||||
self.control_point,
|
||||
)
|
||||
return
|
||||
g.groups = groups
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_ewr_at(self, position: PointWithHeading) -> None:
|
||||
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(
|
||||
"Could not generate ewr group for %s at %s",
|
||||
g.name,
|
||||
self.control_point,
|
||||
)
|
||||
return
|
||||
g.groups = [group]
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_scenery_sites(self) -> None:
|
||||
presets = self.control_point.preset_locations
|
||||
for scenery_group in presets.scenery:
|
||||
self.generate_tgo_for_scenery(scenery_group)
|
||||
|
||||
def generate_tgo_for_scenery(self, scenery: SceneryGroup) -> None:
|
||||
|
||||
obj_name = namegen.random_objective_name()
|
||||
category = scenery.category
|
||||
group_id = self.game.next_group_id()
|
||||
object_id = 0
|
||||
|
||||
# Each nested trigger zone is a target/building/unit for an objective.
|
||||
for zone in scenery.zones:
|
||||
|
||||
object_id += 1
|
||||
local_position = zone.position
|
||||
local_dcs_identifier = zone.name
|
||||
|
||||
g = SceneryGroundObject(
|
||||
obj_name,
|
||||
category,
|
||||
group_id,
|
||||
object_id,
|
||||
local_position,
|
||||
self.control_point,
|
||||
local_dcs_identifier,
|
||||
zone,
|
||||
)
|
||||
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
return
|
||||
|
||||
def generate_missile_sites(self) -> None:
|
||||
for position in self.control_point.preset_locations.missile_sites:
|
||||
self.generate_missile_site_at(position)
|
||||
|
||||
def generate_missile_site_at(self, position: PointWithHeading) -> None:
|
||||
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
|
||||
|
||||
def generate_coastal_sites(self) -> None:
|
||||
for position in self.control_point.preset_locations.coastal_defenses:
|
||||
self.generate_coastal_site_at(position)
|
||||
|
||||
def generate_coastal_site_at(self, position: PointWithHeading) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = CoastalSiteGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
position.heading,
|
||||
)
|
||||
group = generate_coastal_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
|
||||
|
||||
def generate_strike_targets(self) -> None:
|
||||
building_set = list(set(self.faction.building_set) - {"oil"})
|
||||
if not building_set:
|
||||
logging.error("Faction has no buildings defined")
|
||||
return
|
||||
for position in self.control_point.preset_locations.strike_locations:
|
||||
category = random.choice(building_set)
|
||||
self.generate_strike_target_at(category, position)
|
||||
|
||||
def generate_offshore_strike_targets(self) -> None:
|
||||
if "oil" not in self.faction.building_set:
|
||||
logging.error("Faction does not support offshore strike targets")
|
||||
return
|
||||
for position in self.control_point.preset_locations.offshore_strike_locations:
|
||||
self.generate_strike_target_at("oil", position)
|
||||
|
||||
|
||||
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||
def generate(self) -> bool:
|
||||
self.generate_fob()
|
||||
self.generate_armor_groups()
|
||||
self.generate_factories()
|
||||
self.generate_ammunition_depots()
|
||||
self.generate_aa()
|
||||
self.generate_ewrs()
|
||||
self.generate_scenery_sites()
|
||||
self.generate_strike_targets()
|
||||
self.generate_offshore_strike_targets()
|
||||
|
||||
if self.faction.missiles:
|
||||
self.generate_missile_sites()
|
||||
|
||||
if self.faction.coastal_defenses:
|
||||
self.generate_coastal_sites()
|
||||
|
||||
return True
|
||||
|
||||
def generate_fob(self) -> None:
|
||||
category = "fob"
|
||||
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"],
|
||||
is_fob_structure=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()
|
||||
8
game/theater/syria.py
Normal file
8
game/theater/syria.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from game.theater.projections import TransverseMercator
|
||||
|
||||
PARAMETERS = TransverseMercator(
|
||||
central_meridian=39,
|
||||
false_easting=282801.00000003993,
|
||||
false_northing=-3879865.9999999935,
|
||||
scale_factor=0.9996,
|
||||
)
|
||||
642
game/theater/theatergroundobject.py
Normal file
642
game/theater/theatergroundobject.py
Normal file
@@ -0,0 +1,642 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from typing import Iterator, List, TYPE_CHECKING, Union
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.triggers import TriggerZone
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import Group
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from .. import db
|
||||
from ..data.radar_db import (
|
||||
TRACK_RADARS,
|
||||
TELARS,
|
||||
LAUNCHER_TRACKER_PAIRS,
|
||||
)
|
||||
from ..utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .controlpoint import ControlPoint
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
from .missiontarget import MissionTarget
|
||||
|
||||
NAME_BY_CATEGORY = {
|
||||
"ewr": "Early Warning Radar",
|
||||
"aa": "AA Defense Site",
|
||||
"allycamp": "Camp",
|
||||
"ammo": "Ammo depot",
|
||||
"armor": "Armor group",
|
||||
"coastal": "Coastal defense",
|
||||
"comms": "Communications tower",
|
||||
"derrick": "Derrick",
|
||||
"factory": "Factory",
|
||||
"farp": "FARP",
|
||||
"fob": "FOB",
|
||||
"fuel": "Fuel depot",
|
||||
"missile": "Missile site",
|
||||
"oil": "Oil platform",
|
||||
"power": "Power plant",
|
||||
"ship": "Ship",
|
||||
"village": "Village",
|
||||
"ware": "Warehouse",
|
||||
"ww2bunker": "Bunker",
|
||||
}
|
||||
|
||||
|
||||
class TheaterGroundObject(MissionTarget):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
category: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
heading: int,
|
||||
control_point: ControlPoint,
|
||||
dcs_identifier: str,
|
||||
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.sea_object = sea_object
|
||||
self.groups: List[Group] = []
|
||||
|
||||
@property
|
||||
def is_dead(self) -> bool:
|
||||
return self.alive_unit_count == 0
|
||||
|
||||
@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 dead_units(self) -> List[Unit]:
|
||||
"""
|
||||
:return: all the dead units at this location
|
||||
"""
|
||||
return list(
|
||||
itertools.chain.from_iterable(
|
||||
[getattr(g, "units_losts", []) 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
|
||||
|
||||
@property
|
||||
def has_live_radar_sam(self) -> bool:
|
||||
"""Returns True if the ground object contains a unit with working radar SAM."""
|
||||
for group in self.groups:
|
||||
if self.threat_range(group, radar_only=True):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _max_range_of_type(self, group: Group, range_type: str) -> Distance:
|
||||
if not self.might_have_aa:
|
||||
return meters(0)
|
||||
|
||||
max_range = meters(0)
|
||||
for u in group.units:
|
||||
unit = db.unit_type_from_name(u.type)
|
||||
if unit is None:
|
||||
logging.error(f"Unknown unit type {u.type}")
|
||||
continue
|
||||
|
||||
# Some units in pydcs have detection_range/threat_range defined,
|
||||
# but explicitly set to None.
|
||||
unit_range = getattr(unit, range_type, None)
|
||||
if unit_range is not None:
|
||||
max_range = max(max_range, meters(unit_range))
|
||||
return max_range
|
||||
|
||||
def max_detection_range(self) -> Distance:
|
||||
return max(self.detection_range(g) for g in self.groups)
|
||||
|
||||
def detection_range(self, group: Group) -> Distance:
|
||||
return self._max_range_of_type(group, "detection_range")
|
||||
|
||||
def max_threat_range(self) -> Distance:
|
||||
return max(self.threat_range(g) for g in self.groups)
|
||||
|
||||
def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
|
||||
return self._max_range_of_type(group, "threat_range")
|
||||
|
||||
@property
|
||||
def is_factory(self) -> bool:
|
||||
return self.category == "factory"
|
||||
|
||||
@property
|
||||
def is_control_point(self) -> bool:
|
||||
"""True if this TGO is the group for the control point itself (CVs and FOBs)."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
||||
return self.units
|
||||
|
||||
@property
|
||||
def mark_locations(self) -> Iterator[Point]:
|
||||
yield self.position
|
||||
|
||||
def clear(self) -> None:
|
||||
self.groups = []
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
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,
|
||||
is_fob_structure=False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category=category,
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=heading,
|
||||
control_point=control_point,
|
||||
dcs_identifier=dcs_identifier,
|
||||
sea_object=False,
|
||||
)
|
||||
self.is_fob_structure = is_fob_structure
|
||||
self.object_id = object_id
|
||||
# Other TGOs track deadness based on the number of alive units, but
|
||||
# buildings don't have groups assigned to the TGO.
|
||||
self._dead = False
|
||||
|
||||
@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}"
|
||||
|
||||
@property
|
||||
def is_dead(self) -> bool:
|
||||
if not hasattr(self, "_dead"):
|
||||
self._dead = False
|
||||
return self._dead
|
||||
|
||||
def kill(self) -> None:
|
||||
self._dead = True
|
||||
|
||||
def iter_building_group(self) -> Iterator[TheaterGroundObject]:
|
||||
for tgo in self.control_point.ground_objects:
|
||||
if tgo.obj_name == self.obj_name and not tgo.is_dead:
|
||||
yield tgo
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
||||
return list(self.iter_building_group())
|
||||
|
||||
@property
|
||||
def mark_locations(self) -> Iterator[Point]:
|
||||
for building in self.iter_building_group():
|
||||
yield building.position
|
||||
|
||||
@property
|
||||
def is_control_point(self) -> bool:
|
||||
return self.is_fob_structure
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class SceneryGroundObject(BuildingGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
category: str,
|
||||
group_id: int,
|
||||
object_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
dcs_identifier: str,
|
||||
zone: TriggerZone,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category=category,
|
||||
group_id=group_id,
|
||||
object_id=object_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier=dcs_identifier,
|
||||
is_fob_structure=False,
|
||||
)
|
||||
self.zone = zone
|
||||
try:
|
||||
# In the default TriggerZone using "assign as..." in the DCS Mission Editor,
|
||||
# property three has the scenery's object ID as its value.
|
||||
self.map_object_id = self.zone.properties[3]["value"]
|
||||
except (IndexError, KeyError):
|
||||
logging.exception(
|
||||
"Invalid TriggerZone for Scenery definition. The third property must "
|
||||
"be the map object ID."
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
class FactoryGroundObject(BuildingGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
heading: int,
|
||||
control_point: ControlPoint,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="factory",
|
||||
group_id=group_id,
|
||||
object_id=0,
|
||||
position=position,
|
||||
heading=heading,
|
||||
control_point=control_point,
|
||||
dcs_identifier="Workshop A",
|
||||
is_fob_structure=False,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class GenericCarrierGroundObject(NavalGroundObject):
|
||||
@property
|
||||
def is_control_point(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
# 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",
|
||||
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",
|
||||
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="missile",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class CoastalSiteGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
heading,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="coastal",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=heading,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# 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(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",
|
||||
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 FlightType.SEAD
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def might_have_aa(self) -> bool:
|
||||
return True
|
||||
|
||||
def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
|
||||
max_non_radar = meters(0)
|
||||
live_trs = set()
|
||||
max_telar_range = meters(0)
|
||||
launchers = set()
|
||||
for unit in group.units:
|
||||
unit_type = db.unit_type_from_name(unit.type)
|
||||
if unit_type is None or not issubclass(unit_type, VehicleType):
|
||||
continue
|
||||
if unit_type in TRACK_RADARS:
|
||||
live_trs.add(unit_type)
|
||||
elif unit_type in TELARS:
|
||||
max_telar_range = max(
|
||||
max_telar_range, meters(getattr(unit_type, "threat_range", 0))
|
||||
)
|
||||
elif unit_type in LAUNCHER_TRACKER_PAIRS:
|
||||
launchers.add(unit_type)
|
||||
else:
|
||||
max_non_radar = max(
|
||||
max_non_radar, meters(getattr(unit_type, "threat_range", 0))
|
||||
)
|
||||
max_tel_range = meters(0)
|
||||
for launcher in launchers:
|
||||
if LAUNCHER_TRACKER_PAIRS[launcher] in live_trs:
|
||||
max_tel_range = max(
|
||||
max_tel_range, meters(getattr(launcher, "threat_range"))
|
||||
)
|
||||
if radar_only:
|
||||
return max(max_tel_range, max_telar_range)
|
||||
else:
|
||||
return max(max_tel_range, max_telar_range, max_non_radar)
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class VehicleGroupGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="armor",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class EwrGroundObject(TheaterGroundObject):
|
||||
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",
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them.
|
||||
# Use Group Id and uppercase EWR
|
||||
return f"{self.faction_color}|EWR|{self.group_id}"
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def purchasable(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="ship",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
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}"
|
||||
8
game/theater/thechannel.py
Normal file
8
game/theater/thechannel.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from game.theater.projections import TransverseMercator
|
||||
|
||||
PARAMETERS = TransverseMercator(
|
||||
central_meridian=3,
|
||||
false_easting=99376.00000000288,
|
||||
false_northing=-5636889.00000001,
|
||||
scale_factor=0.9996,
|
||||
)
|
||||
196
game/theater/transitnetwork.py
Normal file
196
game/theater/transitnetwork.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, auto
|
||||
from typing import Dict, Iterator, List, Optional, Set, Tuple
|
||||
|
||||
from game.theater import ConflictTheater
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
|
||||
|
||||
class NoPathError(RuntimeError):
|
||||
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
|
||||
super().__init__(f"Could not reconstruct path to {destination} from {origin}")
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class FrontierNode:
|
||||
cost: float
|
||||
point: ControlPoint = field(compare=False)
|
||||
|
||||
|
||||
class Frontier:
|
||||
def __init__(self) -> None:
|
||||
self.nodes: List[FrontierNode] = []
|
||||
|
||||
def push(self, poly: ControlPoint, cost: float) -> None:
|
||||
heapq.heappush(self.nodes, FrontierNode(cost, poly))
|
||||
|
||||
def pop(self) -> Optional[FrontierNode]:
|
||||
try:
|
||||
return heapq.heappop(self.nodes)
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.nodes)
|
||||
|
||||
|
||||
class TransitConnection(Enum):
|
||||
Road = auto()
|
||||
Shipping = auto()
|
||||
Airlift = auto()
|
||||
|
||||
|
||||
class TransitNetwork:
|
||||
def __init__(self) -> None:
|
||||
self.nodes: Dict[
|
||||
ControlPoint, Dict[ControlPoint, TransitConnection]
|
||||
] = defaultdict(dict)
|
||||
|
||||
def has_destinations(self, control_point: ControlPoint) -> bool:
|
||||
return bool(self.nodes[control_point])
|
||||
|
||||
def has_link(self, a: ControlPoint, b: ControlPoint) -> bool:
|
||||
return b in self.nodes[a]
|
||||
|
||||
def link_type(self, a: ControlPoint, b: ControlPoint) -> TransitConnection:
|
||||
return self.nodes[a][b]
|
||||
|
||||
def link_with(
|
||||
self, a: ControlPoint, b: ControlPoint, link_type: TransitConnection
|
||||
) -> None:
|
||||
self.nodes[a][b] = link_type
|
||||
self.nodes[b][a] = link_type
|
||||
|
||||
def link_road(self, a: ControlPoint, b: ControlPoint) -> None:
|
||||
self.link_with(a, b, TransitConnection.Road)
|
||||
|
||||
def link_shipping(self, a: ControlPoint, b: ControlPoint) -> None:
|
||||
self.link_with(a, b, TransitConnection.Shipping)
|
||||
|
||||
def link_airport(self, a: ControlPoint, b: ControlPoint) -> None:
|
||||
self.link_with(a, b, TransitConnection.Airlift)
|
||||
|
||||
def connections_from(self, control_point: ControlPoint) -> Iterator[ControlPoint]:
|
||||
yield from self.nodes[control_point]
|
||||
|
||||
def cost(self, a: ControlPoint, b: ControlPoint) -> float:
|
||||
return {
|
||||
TransitConnection.Road: 1,
|
||||
TransitConnection.Shipping: 3,
|
||||
# Set arbitrarily high so that other methods are preferred, but still scaled
|
||||
# by distance so that when we do need it we still pick the closest airfield.
|
||||
# The units of distance are meters so there's no risk of these
|
||||
TransitConnection.Airlift: a.position.distance_to_point(b.position),
|
||||
}[self.link_type(a, b)]
|
||||
|
||||
def has_path_between(
|
||||
self,
|
||||
origin: ControlPoint,
|
||||
destination: ControlPoint,
|
||||
seen: Optional[set[ControlPoint]] = None,
|
||||
) -> bool:
|
||||
if seen is None:
|
||||
seen = set()
|
||||
seen.add(origin)
|
||||
for connection in self.connections_from(origin):
|
||||
if connection in seen:
|
||||
continue
|
||||
if connection == destination:
|
||||
return True
|
||||
if self.has_path_between(connection, destination, seen):
|
||||
return True
|
||||
return False
|
||||
|
||||
def shortest_path_between(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> list[ControlPoint]:
|
||||
return self.shortest_path_with_cost(origin, destination)[0]
|
||||
|
||||
def shortest_path_with_cost(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> Tuple[List[ControlPoint], float]:
|
||||
if origin not in self.nodes:
|
||||
raise ValueError(f"{origin} is not in the transit network.")
|
||||
if destination not in self.nodes:
|
||||
raise ValueError(f"{destination} is not in the transit network.")
|
||||
|
||||
frontier = Frontier()
|
||||
frontier.push(origin, 0)
|
||||
|
||||
came_from: Dict[ControlPoint, Optional[ControlPoint]] = {origin: None}
|
||||
|
||||
best_known: Dict[ControlPoint, float] = defaultdict(lambda: math.inf)
|
||||
best_known[origin] = 0.0
|
||||
|
||||
while (node := frontier.pop()) is not None:
|
||||
cost = node.cost
|
||||
current = node.point
|
||||
if cost > best_known[current]:
|
||||
continue
|
||||
|
||||
for neighbor in self.connections_from(current):
|
||||
new_cost = cost + self.cost(node.point, neighbor)
|
||||
if new_cost < best_known[neighbor]:
|
||||
best_known[neighbor] = new_cost
|
||||
frontier.push(neighbor, new_cost)
|
||||
came_from[neighbor] = current
|
||||
|
||||
# Reconstruct and reverse the path.
|
||||
current = destination
|
||||
path: List[ControlPoint] = []
|
||||
while current != origin:
|
||||
path.append(current)
|
||||
previous = came_from.get(current)
|
||||
if previous is None:
|
||||
raise NoPathError(origin, destination)
|
||||
current = previous
|
||||
path.reverse()
|
||||
return path, best_known[destination]
|
||||
|
||||
|
||||
class TransitNetworkBuilder:
|
||||
def __init__(self, theater: ConflictTheater, for_player: bool) -> None:
|
||||
self.control_points = list(theater.control_points_for(for_player))
|
||||
self.network = TransitNetwork()
|
||||
self.airports: Set[ControlPoint] = {
|
||||
cp
|
||||
for cp in self.control_points
|
||||
if cp.is_friendly(for_player) and cp.runway_is_operational()
|
||||
}
|
||||
|
||||
def build(self) -> TransitNetwork:
|
||||
seen = set()
|
||||
for control_point in self.control_points:
|
||||
if control_point not in seen:
|
||||
seen.add(control_point)
|
||||
self.add_transit_links(control_point)
|
||||
return self.network
|
||||
|
||||
def add_transit_links(self, control_point: ControlPoint) -> None:
|
||||
# Prefer road connections.
|
||||
for road_connection in control_point.connected_points:
|
||||
if road_connection.is_friendly_to(control_point):
|
||||
self.network.link_road(control_point, road_connection)
|
||||
|
||||
# Use sea connections if there's no road or rail connection.
|
||||
for sea_connection in control_point.shipping_lanes:
|
||||
if self.network.has_link(control_point, sea_connection):
|
||||
continue
|
||||
if sea_connection.is_friendly_to(control_point):
|
||||
self.network.link_shipping(control_point, sea_connection)
|
||||
|
||||
# And use airports as a last resort.
|
||||
if control_point in self.airports:
|
||||
for airport in self.airports:
|
||||
if control_point == airport:
|
||||
continue
|
||||
if self.network.has_link(control_point, airport):
|
||||
continue
|
||||
if not airport.is_friendly_to(control_point):
|
||||
continue
|
||||
self.network.link_airport(control_point, airport)
|
||||
209
game/threatzones.py
Normal file
209
game/threatzones.py
Normal file
@@ -0,0 +1,209 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import singledispatchmethod
|
||||
from typing import Optional, TYPE_CHECKING, Union, Iterable
|
||||
|
||||
from dcs.mapping import Point as DcsPoint
|
||||
from shapely.geometry import (
|
||||
LineString,
|
||||
MultiPolygon,
|
||||
Point as ShapelyPoint,
|
||||
Polygon,
|
||||
)
|
||||
from shapely.geometry.base import BaseGeometry
|
||||
from shapely.ops import nearest_points, unary_union
|
||||
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import Flight, FlightWaypoint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
ThreatPoly = Union[MultiPolygon, Polygon]
|
||||
|
||||
|
||||
class ThreatZones:
|
||||
def __init__(
|
||||
self, airbases: ThreatPoly, air_defenses: ThreatPoly, radar_sam_threats
|
||||
) -> None:
|
||||
self.airbases = airbases
|
||||
self.air_defenses = air_defenses
|
||||
self.radar_sam_threats = radar_sam_threats
|
||||
self.all = unary_union([airbases, air_defenses])
|
||||
|
||||
def closest_boundary(self, point: DcsPoint) -> DcsPoint:
|
||||
boundary, _ = nearest_points(
|
||||
self.all.boundary, self.dcs_to_shapely_point(point)
|
||||
)
|
||||
return DcsPoint(boundary.x, boundary.y)
|
||||
|
||||
def distance_to_threat(self, point: DcsPoint) -> Distance:
|
||||
boundary = self.closest_boundary(point)
|
||||
return meters(boundary.distance_to_point(point))
|
||||
|
||||
@singledispatchmethod
|
||||
def threatened(self, position) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@threatened.register
|
||||
def _threatened_geometry(self, position: BaseGeometry) -> bool:
|
||||
return self.all.intersects(position)
|
||||
|
||||
@threatened.register
|
||||
def _threatened_dcs_point(self, position: DcsPoint) -> bool:
|
||||
return self.all.intersects(self.dcs_to_shapely_point(position))
|
||||
|
||||
def path_threatened(self, a: DcsPoint, b: DcsPoint) -> bool:
|
||||
return self.threatened(
|
||||
LineString([self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])
|
||||
)
|
||||
|
||||
@singledispatchmethod
|
||||
def threatened_by_aircraft(self, target) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@threatened_by_aircraft.register
|
||||
def _threatened_by_aircraft_geom(self, position: BaseGeometry) -> bool:
|
||||
return self.airbases.intersects(position)
|
||||
|
||||
@threatened_by_aircraft.register
|
||||
def _threatened_by_aircraft_flight(self, flight: Flight) -> bool:
|
||||
return self.threatened_by_aircraft(
|
||||
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
|
||||
)
|
||||
|
||||
def waypoints_threatened_by_aircraft(
|
||||
self, waypoints: Iterable[FlightWaypoint]
|
||||
) -> bool:
|
||||
return self.threatened_by_aircraft(
|
||||
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
|
||||
)
|
||||
|
||||
@singledispatchmethod
|
||||
def threatened_by_air_defense(self, target) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@threatened_by_air_defense.register
|
||||
def _threatened_by_air_defense_geom(self, position: BaseGeometry) -> bool:
|
||||
return self.air_defenses.intersects(position)
|
||||
|
||||
@threatened_by_air_defense.register
|
||||
def _threatened_by_air_defense_flight(self, flight: Flight) -> bool:
|
||||
return self.threatened_by_air_defense(
|
||||
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
|
||||
)
|
||||
|
||||
@threatened_by_air_defense.register
|
||||
def _threatened_by_air_defense_mission_target(self, target: MissionTarget) -> bool:
|
||||
return self.threatened_by_air_defense(
|
||||
self.dcs_to_shapely_point(target.position)
|
||||
)
|
||||
|
||||
@singledispatchmethod
|
||||
def threatened_by_radar_sam(self, target) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@threatened_by_radar_sam.register
|
||||
def _threatened_by_radar_sam_geom(self, position: BaseGeometry) -> bool:
|
||||
return self.radar_sam_threats.intersects(position)
|
||||
|
||||
@threatened_by_radar_sam.register
|
||||
def _threatened_by_radar_sam_flight(self, flight: Flight) -> bool:
|
||||
return self.threatened_by_radar_sam(
|
||||
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
|
||||
)
|
||||
|
||||
def waypoints_threatened_by_radar_sam(
|
||||
self, waypoints: Iterable[FlightWaypoint]
|
||||
) -> bool:
|
||||
return self.threatened_by_radar_sam(
|
||||
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def closest_enemy_airbase(
|
||||
cls, location: ControlPoint, max_distance: Distance
|
||||
) -> Optional[ControlPoint]:
|
||||
airfields = ObjectiveDistanceCache.get_closest_airfields(location)
|
||||
for airfield in airfields.all_airfields_within(max_distance):
|
||||
if airfield.captured != location.captured:
|
||||
return airfield
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def barcap_threat_range(cls, game: Game, control_point: ControlPoint) -> Distance:
|
||||
doctrine = game.faction_for(control_point.captured).doctrine
|
||||
cap_threat_range = (
|
||||
doctrine.cap_max_distance_from_cp + doctrine.cap_engagement_range
|
||||
)
|
||||
opposing_airfield = cls.closest_enemy_airbase(
|
||||
control_point, cap_threat_range * 2
|
||||
)
|
||||
if opposing_airfield is None:
|
||||
return cap_threat_range
|
||||
|
||||
airfield_distance = meters(
|
||||
opposing_airfield.position.distance_to_point(control_point.position)
|
||||
)
|
||||
|
||||
# BARCAPs should not commit further than halfway to the closest enemy
|
||||
# airfield (with some breathing room) to avoid those missions becoming
|
||||
# offensive. For dissimilar doctrines we could weight this so that, as
|
||||
# an example, modern US goes no closer than 70% of the way to the WW2
|
||||
# German base, and the Germans go no closer than 30% of the way to the
|
||||
# US base, but for now equal weighting is fine.
|
||||
max_distance = airfield_distance * 0.45
|
||||
return min(cap_threat_range, max_distance)
|
||||
|
||||
@classmethod
|
||||
def for_faction(cls, game: Game, player: bool) -> ThreatZones:
|
||||
"""Generates the threat zones projected by the given coalition.
|
||||
|
||||
Args:
|
||||
game: The game to generate the threat zone for.
|
||||
player: True if the coalition projecting the threat zone belongs to
|
||||
the player.
|
||||
|
||||
Returns:
|
||||
The threat zones projected by the given coalition. If the threat
|
||||
zone belongs to the player, it is the zone that will be avoided by
|
||||
the enemy and vice versa.
|
||||
"""
|
||||
air_threats = []
|
||||
air_defenses = []
|
||||
radar_sam_threats = []
|
||||
for control_point in game.theater.controlpoints:
|
||||
if control_point.captured != player:
|
||||
continue
|
||||
if control_point.runway_is_operational():
|
||||
point = ShapelyPoint(control_point.position.x, control_point.position.y)
|
||||
cap_threat_range = cls.barcap_threat_range(game, control_point)
|
||||
air_threats.append(point.buffer(cap_threat_range.meters))
|
||||
|
||||
for tgo in control_point.ground_objects:
|
||||
for group in tgo.groups:
|
||||
threat_range = tgo.threat_range(group)
|
||||
# Any system with a shorter range than this is not worth
|
||||
# even avoiding.
|
||||
if threat_range > nautical_miles(3):
|
||||
point = ShapelyPoint(tgo.position.x, tgo.position.y)
|
||||
threat_zone = point.buffer(threat_range.meters)
|
||||
air_defenses.append(threat_zone)
|
||||
radar_threat_range = tgo.threat_range(group, radar_only=True)
|
||||
if radar_threat_range > nautical_miles(3):
|
||||
point = ShapelyPoint(tgo.position.x, tgo.position.y)
|
||||
threat_zone = point.buffer(threat_range.meters)
|
||||
radar_sam_threats.append(threat_zone)
|
||||
|
||||
return cls(
|
||||
airbases=unary_union(air_threats),
|
||||
air_defenses=unary_union(air_defenses),
|
||||
radar_sam_threats=unary_union(radar_sam_threats),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def dcs_to_shapely_point(point: DcsPoint) -> ShapelyPoint:
|
||||
return ShapelyPoint(point.x, point.y)
|
||||
619
game/transfers.py
Normal file
619
game/transfers.py
Normal file
@@ -0,0 +1,619 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from functools import singledispatchmethod
|
||||
from typing import (
|
||||
Dict,
|
||||
Generic,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
Type,
|
||||
TypeVar,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unittype import FlyingType, VehicleType
|
||||
|
||||
from game.procurement import AircraftProcurementRequest
|
||||
from game.squadrons import Squadron
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.theater.transitnetwork import (
|
||||
TransitConnection,
|
||||
TransitNetwork,
|
||||
)
|
||||
from game.utils import meters, nautical_miles
|
||||
from gen.ato import Package
|
||||
from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import Flight, FlightType
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from gen.naming import namegen
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.inventory import ControlPointAircraftInventory
|
||||
|
||||
|
||||
class Transport:
|
||||
def __init__(self, destination: ControlPoint):
|
||||
self.destination = destination
|
||||
|
||||
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||
raise NotImplementedError
|
||||
|
||||
def description(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransferOrder:
|
||||
"""The base type of all transfer orders.
|
||||
|
||||
A transfer order can transfer multiple units of multiple types.
|
||||
"""
|
||||
|
||||
#: The location the units are transferring from.
|
||||
origin: ControlPoint
|
||||
|
||||
#: The location the units are transferring to.
|
||||
destination: ControlPoint
|
||||
|
||||
#: The current position of the group being transferred. Groups may make multiple
|
||||
#: stops and can switch transport modes before reaching their destination.
|
||||
position: ControlPoint = field(init=False)
|
||||
|
||||
#: True if the transfer order belongs to the player.
|
||||
player: bool = field(init=False)
|
||||
|
||||
#: The units being transferred.
|
||||
units: Dict[Type[VehicleType], int]
|
||||
|
||||
transport: Optional[Transport] = field(default=None)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.position = self.origin
|
||||
self.player = self.origin.is_friendly(to_player=True)
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
if self.transport is None:
|
||||
return "No transports available"
|
||||
return self.transport.description()
|
||||
|
||||
def kill_all(self) -> None:
|
||||
self.units.clear()
|
||||
|
||||
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
|
||||
if unit_type not in self.units or not self.units[unit_type]:
|
||||
raise KeyError(f"{self.destination} has no {unit_type} remaining")
|
||||
self.units[unit_type] -= 1
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return sum(c for c in self.units.values())
|
||||
|
||||
def iter_units(self) -> Iterator[Type[VehicleType]]:
|
||||
for unit_type, count in self.units.items():
|
||||
for _ in range(count):
|
||||
yield unit_type
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
return self.destination == self.position or not self.units
|
||||
|
||||
def disband_at(self, location: ControlPoint) -> None:
|
||||
logging.info(f"Units halting at {location}.")
|
||||
location.base.commision_units(self.units)
|
||||
self.units.clear()
|
||||
|
||||
@property
|
||||
def next_stop(self) -> ControlPoint:
|
||||
if self.transport is None:
|
||||
raise RuntimeError(
|
||||
"TransferOrder.next_stop called with no transport assigned"
|
||||
)
|
||||
return self.transport.destination
|
||||
|
||||
def proceed(self) -> None:
|
||||
if self.transport is None:
|
||||
return
|
||||
|
||||
if not self.destination.is_friendly(self.player):
|
||||
logging.info(f"Transfer destination {self.destination} was captured.")
|
||||
if self.position.is_friendly(self.player):
|
||||
self.disband_at(self.position)
|
||||
elif (escape_route := self.transport.find_escape_route()) is not None:
|
||||
self.disband_at(escape_route)
|
||||
else:
|
||||
logging.info(
|
||||
f"No escape route available. Units were surrounded and destroyed "
|
||||
"during transfer."
|
||||
)
|
||||
self.kill_all()
|
||||
return
|
||||
|
||||
self.position = self.next_stop
|
||||
self.transport = None
|
||||
|
||||
if self.completed:
|
||||
self.disband_at(self.position)
|
||||
|
||||
|
||||
class Airlift(Transport):
|
||||
"""A transfer order that moves units by cargo planes and helicopters."""
|
||||
|
||||
def __init__(
|
||||
self, transfer: TransferOrder, flight: Flight, next_stop: ControlPoint
|
||||
) -> None:
|
||||
super().__init__(next_stop)
|
||||
self.transfer = transfer
|
||||
self.flight = flight
|
||||
|
||||
@property
|
||||
def units(self) -> Dict[Type[VehicleType], int]:
|
||||
return self.transfer.units
|
||||
|
||||
@property
|
||||
def player_owned(self) -> bool:
|
||||
return self.transfer.player
|
||||
|
||||
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||
# TODO: Move units to closest base.
|
||||
return None
|
||||
|
||||
def description(self) -> str:
|
||||
return (
|
||||
f"Being airlifted from {self.transfer.position} to {self.destination} by "
|
||||
f"{self.flight}"
|
||||
)
|
||||
|
||||
|
||||
class AirliftPlanner:
|
||||
#: Maximum range from for any link in the route of takeoff, pickup, dropoff, and RTB
|
||||
#: for a helicopter to be considered for airlift. Total route length is not
|
||||
#: considered because the helicopter can refuel at each stop. Cargo planes have no
|
||||
#: maximum range.
|
||||
HELO_MAX_RANGE = nautical_miles(100)
|
||||
|
||||
def __init__(
|
||||
self, game: Game, transfer: TransferOrder, next_stop: ControlPoint
|
||||
) -> None:
|
||||
self.game = game
|
||||
self.transfer = transfer
|
||||
self.next_stop = next_stop
|
||||
self.for_player = transfer.destination.captured
|
||||
self.package = Package(target=next_stop, auto_asap=True)
|
||||
|
||||
def compatible_with_mission(
|
||||
self, unit_type: Type[FlyingType], airfield: ControlPoint
|
||||
) -> bool:
|
||||
if not unit_type in TRANSPORT_CAPABLE:
|
||||
return False
|
||||
if not self.transfer.origin.can_operate(unit_type):
|
||||
return False
|
||||
if not self.next_stop.can_operate(unit_type):
|
||||
return False
|
||||
|
||||
# Cargo planes have no maximum range.
|
||||
if not unit_type.helicopter:
|
||||
return True
|
||||
|
||||
# A helicopter that is transport capable and able to operate at both bases. Need
|
||||
# to check that no leg of the journey exceeds the maximum range. This doesn't
|
||||
# account for any routing around threats that might take place, but it's close
|
||||
# enough.
|
||||
|
||||
home = airfield.position
|
||||
pickup = self.transfer.position.position
|
||||
drop_off = self.transfer.position.position
|
||||
if meters(home.distance_to_point(pickup)) > self.HELO_MAX_RANGE:
|
||||
return False
|
||||
|
||||
if meters(pickup.distance_to_point(drop_off)) > self.HELO_MAX_RANGE:
|
||||
return False
|
||||
|
||||
if meters(drop_off.distance_to_point(home)) > self.HELO_MAX_RANGE:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_package_for_airlift(self) -> None:
|
||||
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
|
||||
self.transfer.position
|
||||
)
|
||||
for cp in distance_cache.closest_airfields:
|
||||
if cp.captured != self.for_player:
|
||||
continue
|
||||
|
||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||
for unit_type, available in inventory.all_aircraft:
|
||||
squadrons = [
|
||||
s
|
||||
for s in self.game.air_wing_for(self.for_player).squadrons_for(
|
||||
unit_type
|
||||
)
|
||||
if FlightType.TRANSPORT in s.auto_assignable_mission_types
|
||||
]
|
||||
if not squadrons:
|
||||
continue
|
||||
squadron = squadrons[0]
|
||||
if self.compatible_with_mission(unit_type, cp):
|
||||
while available and self.transfer.transport is None:
|
||||
flight_size = self.create_airlift_flight(squadron, inventory)
|
||||
available -= flight_size
|
||||
if self.package.flights:
|
||||
self.game.ato_for(self.for_player).add_package(self.package)
|
||||
|
||||
def create_airlift_flight(
|
||||
self, squadron: Squadron, inventory: ControlPointAircraftInventory
|
||||
) -> int:
|
||||
available = inventory.available(squadron.aircraft)
|
||||
capacity_each = 1 if squadron.aircraft.helicopter else 2
|
||||
required = math.ceil(self.transfer.size / capacity_each)
|
||||
flight_size = min(required, available, squadron.aircraft.group_size_max)
|
||||
capacity = flight_size * capacity_each
|
||||
|
||||
if capacity < self.transfer.size:
|
||||
transfer = self.game.transfers.split_transfer(self.transfer, capacity)
|
||||
else:
|
||||
transfer = self.transfer
|
||||
|
||||
player = inventory.control_point.captured
|
||||
flight = Flight(
|
||||
self.package,
|
||||
self.game.country_for(player),
|
||||
squadron,
|
||||
flight_size,
|
||||
FlightType.TRANSPORT,
|
||||
self.game.settings.default_start_type,
|
||||
departure=inventory.control_point,
|
||||
arrival=inventory.control_point,
|
||||
divert=None,
|
||||
cargo=transfer,
|
||||
)
|
||||
|
||||
transport = Airlift(transfer, flight, self.next_stop)
|
||||
transfer.transport = transport
|
||||
|
||||
self.package.add_flight(flight)
|
||||
planner = FlightPlanBuilder(self.game, self.package, self.for_player)
|
||||
planner.populate_flight_plan(flight)
|
||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
||||
return flight_size
|
||||
|
||||
|
||||
class MultiGroupTransport(MissionTarget, Transport):
|
||||
def __init__(
|
||||
self, name: str, origin: ControlPoint, destination: ControlPoint
|
||||
) -> None:
|
||||
MissionTarget.__init__(self, name, origin.position)
|
||||
Transport.__init__(self, destination)
|
||||
self.origin = origin
|
||||
self.transfers: List[TransferOrder] = []
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
return self.origin.captured
|
||||
|
||||
def add_units(self, transfer: TransferOrder) -> None:
|
||||
self.transfers.append(transfer)
|
||||
transfer.transport = self
|
||||
|
||||
def remove_units(self, transfer: TransferOrder) -> None:
|
||||
transfer.transport = None
|
||||
self.transfers.remove(transfer)
|
||||
|
||||
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
|
||||
for transfer in self.transfers:
|
||||
try:
|
||||
transfer.kill_unit(unit_type)
|
||||
return
|
||||
except KeyError:
|
||||
pass
|
||||
raise KeyError
|
||||
|
||||
def kill_all(self) -> None:
|
||||
for transfer in self.transfers:
|
||||
transfer.kill_all()
|
||||
|
||||
def disband(self) -> None:
|
||||
for transfer in list(self.transfers):
|
||||
self.remove_units(transfer)
|
||||
self.transfers.clear()
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return sum(sum(t.units.values()) for t in self.transfers)
|
||||
|
||||
@property
|
||||
def units(self) -> Dict[Type[VehicleType], int]:
|
||||
units: Dict[Type[VehicleType], int] = defaultdict(int)
|
||||
for transfer in self.transfers:
|
||||
for unit_type, count in transfer.units.items():
|
||||
units[unit_type] += count
|
||||
return units
|
||||
|
||||
@property
|
||||
def player_owned(self) -> bool:
|
||||
return self.origin.captured
|
||||
|
||||
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||
raise NotImplementedError
|
||||
|
||||
def description(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Convoy(MultiGroupTransport):
|
||||
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
|
||||
super().__init__(namegen.next_convoy_name(), origin, destination)
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
if self.is_friendly(for_player):
|
||||
return
|
||||
|
||||
yield FlightType.BAI
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def route_start(self) -> Point:
|
||||
return self.origin.convoy_origin_for(self.destination)
|
||||
|
||||
@property
|
||||
def route_end(self) -> Point:
|
||||
return self.destination.convoy_origin_for(self.origin)
|
||||
|
||||
def description(self) -> str:
|
||||
return f"In a convoy from {self.origin} to {self.destination}"
|
||||
|
||||
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||
return None
|
||||
|
||||
|
||||
class CargoShip(MultiGroupTransport):
|
||||
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
|
||||
super().__init__(namegen.next_cargo_ship_name(), origin, destination)
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
if self.is_friendly(for_player):
|
||||
return
|
||||
|
||||
yield FlightType.ANTISHIP
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def route(self) -> Sequence[Point]:
|
||||
return self.origin.shipping_lanes[self.destination]
|
||||
|
||||
def description(self) -> str:
|
||||
return f"On a ship from {self.origin} to {self.destination}"
|
||||
|
||||
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||
return None
|
||||
|
||||
|
||||
TransportType = TypeVar("TransportType", bound=MultiGroupTransport)
|
||||
|
||||
|
||||
class TransportMap(Generic[TransportType]):
|
||||
def __init__(self) -> None:
|
||||
# Dict of origin -> destination -> transport.
|
||||
self.transports: Dict[
|
||||
ControlPoint, Dict[ControlPoint, TransportType]
|
||||
] = defaultdict(dict)
|
||||
|
||||
def create_transport(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> TransportType:
|
||||
raise NotImplementedError
|
||||
|
||||
def transport_exists(self, origin: ControlPoint, destination: ControlPoint) -> bool:
|
||||
return destination in self.transports[origin]
|
||||
|
||||
def find_transport(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> Optional[TransportType]:
|
||||
return self.transports[origin].get(destination)
|
||||
|
||||
def find_or_create_transport(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> TransportType:
|
||||
transport = self.find_transport(origin, destination)
|
||||
if transport is None:
|
||||
transport = self.create_transport(origin, destination)
|
||||
self.transports[origin][destination] = transport
|
||||
return transport
|
||||
|
||||
def departing_from(self, origin: ControlPoint) -> Iterator[TransportType]:
|
||||
yield from self.transports[origin].values()
|
||||
|
||||
def travelling_to(self, destination: ControlPoint) -> Iterator[TransportType]:
|
||||
for destination_dict in self.transports.values():
|
||||
if destination in destination_dict:
|
||||
yield destination_dict[destination]
|
||||
|
||||
def disband_transport(self, transport: TransportType) -> None:
|
||||
transport.disband()
|
||||
del self.transports[transport.origin][transport.destination]
|
||||
|
||||
def add(self, transfer: TransferOrder, next_stop: ControlPoint) -> None:
|
||||
self.find_or_create_transport(transfer.position, next_stop).add_units(transfer)
|
||||
|
||||
def remove(self, transport: TransportType, transfer: TransferOrder) -> None:
|
||||
transport.remove_units(transfer)
|
||||
if not transport.transfers:
|
||||
self.disband_transport(transport)
|
||||
|
||||
def disband_all(self) -> None:
|
||||
for transport in list(self):
|
||||
self.disband_transport(transport)
|
||||
|
||||
def __iter__(self) -> Iterator[TransportType]:
|
||||
for destination_dict in self.transports.values():
|
||||
yield from destination_dict.values()
|
||||
|
||||
|
||||
class ConvoyMap(TransportMap):
|
||||
def create_transport(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> Convoy:
|
||||
return Convoy(origin, destination)
|
||||
|
||||
|
||||
class CargoShipMap(TransportMap):
|
||||
def create_transport(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> CargoShip:
|
||||
return CargoShip(origin, destination)
|
||||
|
||||
|
||||
class PendingTransfers:
|
||||
def __init__(self, game: Game) -> None:
|
||||
self.game = game
|
||||
self.convoys = ConvoyMap()
|
||||
self.cargo_ships = CargoShipMap()
|
||||
self.pending_transfers: List[TransferOrder] = []
|
||||
|
||||
def __iter__(self) -> Iterator[TransferOrder]:
|
||||
yield from self.pending_transfers
|
||||
|
||||
@property
|
||||
def pending_transfer_count(self) -> int:
|
||||
return len(self.pending_transfers)
|
||||
|
||||
def transfer_at_index(self, index: int) -> TransferOrder:
|
||||
return self.pending_transfers[index]
|
||||
|
||||
def index_of_transfer(self, transfer: TransferOrder) -> int:
|
||||
return self.pending_transfers.index(transfer)
|
||||
|
||||
def network_for(self, control_point: ControlPoint) -> TransitNetwork:
|
||||
return self.game.transit_network_for(control_point.captured)
|
||||
|
||||
def arrange_transport(self, transfer: TransferOrder) -> None:
|
||||
network = self.network_for(transfer.position)
|
||||
path = network.shortest_path_between(transfer.position, transfer.destination)
|
||||
next_stop = path[0]
|
||||
if network.link_type(transfer.position, next_stop) == TransitConnection.Road:
|
||||
self.convoys.add(transfer, next_stop)
|
||||
elif (
|
||||
network.link_type(transfer.position, next_stop)
|
||||
== TransitConnection.Shipping
|
||||
):
|
||||
self.cargo_ships.add(transfer, next_stop)
|
||||
else:
|
||||
AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift()
|
||||
|
||||
def new_transfer(self, transfer: TransferOrder) -> None:
|
||||
transfer.origin.base.commit_losses(transfer.units)
|
||||
self.pending_transfers.append(transfer)
|
||||
self.arrange_transport(transfer)
|
||||
|
||||
def split_transfer(self, transfer: TransferOrder, size: int) -> TransferOrder:
|
||||
"""Creates a smaller transfer that is a subset of the original."""
|
||||
if transfer.size <= size:
|
||||
raise ValueError
|
||||
|
||||
units = {}
|
||||
for unit_type, remaining in transfer.units.items():
|
||||
take = min(remaining, size)
|
||||
size -= take
|
||||
transfer.units[unit_type] -= take
|
||||
units[unit_type] = take
|
||||
if not size:
|
||||
break
|
||||
new_transfer = TransferOrder(transfer.origin, transfer.destination, units)
|
||||
self.pending_transfers.append(new_transfer)
|
||||
return new_transfer
|
||||
|
||||
@singledispatchmethod
|
||||
def cancel_transport(self, transport, transfer: TransferOrder) -> None:
|
||||
pass
|
||||
|
||||
@cancel_transport.register
|
||||
def _cancel_transport_air(
|
||||
self, transport: Airlift, _transfer: TransferOrder
|
||||
) -> None:
|
||||
flight = transport.flight
|
||||
flight.package.remove_flight(flight)
|
||||
if not flight.package.flights:
|
||||
self.game.ato_for(transport.player_owned).remove_package(flight.package)
|
||||
self.game.aircraft_inventory.return_from_flight(flight)
|
||||
flight.clear_roster()
|
||||
|
||||
@cancel_transport.register
|
||||
def _cancel_transport_convoy(
|
||||
self, transport: Convoy, transfer: TransferOrder
|
||||
) -> None:
|
||||
self.convoys.remove(transport, transfer)
|
||||
|
||||
@cancel_transport.register
|
||||
def _cancel_transport_cargo_ship(
|
||||
self, transport: CargoShip, transfer: TransferOrder
|
||||
) -> None:
|
||||
self.cargo_ships.remove(transport, transfer)
|
||||
|
||||
def cancel_transfer(self, transfer: TransferOrder) -> None:
|
||||
if transfer.transport is not None:
|
||||
self.cancel_transport(transfer.transport, transfer)
|
||||
self.pending_transfers.remove(transfer)
|
||||
transfer.origin.base.commision_units(transfer.units)
|
||||
|
||||
def perform_transfers(self) -> None:
|
||||
incomplete = []
|
||||
for transfer in self.pending_transfers:
|
||||
transfer.proceed()
|
||||
if not transfer.completed:
|
||||
incomplete.append(transfer)
|
||||
self.pending_transfers = incomplete
|
||||
self.convoys.disband_all()
|
||||
self.cargo_ships.disband_all()
|
||||
|
||||
def plan_transports(self) -> None:
|
||||
for transfer in self.pending_transfers:
|
||||
if transfer.transport is None:
|
||||
self.arrange_transport(transfer)
|
||||
|
||||
def order_airlift_assets(self) -> None:
|
||||
for control_point in self.game.theater.controlpoints:
|
||||
self.order_airlift_assets_at(control_point)
|
||||
|
||||
@staticmethod
|
||||
def desired_airlift_capacity(control_point: ControlPoint) -> int:
|
||||
return 4 if control_point.has_factory else 0
|
||||
|
||||
def current_airlift_capacity(self, control_point: ControlPoint) -> int:
|
||||
inventory = self.game.aircraft_inventory.for_control_point(control_point)
|
||||
squadrons = self.game.air_wing_for(control_point.captured).squadrons_for_task(
|
||||
FlightType.TRANSPORT
|
||||
)
|
||||
unit_types = {s.aircraft for s in squadrons}.intersection(TRANSPORT_CAPABLE)
|
||||
return sum(
|
||||
count
|
||||
for unit_type, count in inventory.all_aircraft
|
||||
if unit_type in unit_types
|
||||
)
|
||||
|
||||
def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
|
||||
gap = self.desired_airlift_capacity(
|
||||
control_point
|
||||
) - self.current_airlift_capacity(control_point)
|
||||
|
||||
if gap <= 0:
|
||||
return
|
||||
|
||||
if gap % 2:
|
||||
# Always buy in pairs since we're not trying to fill odd squadrons. Purely
|
||||
# aesthetic.
|
||||
gap += 1
|
||||
|
||||
self.game.procurement_requests_for(player=control_point.captured).append(
|
||||
AircraftProcurementRequest(
|
||||
control_point, nautical_miles(200), FlightType.TRANSPORT, gap
|
||||
)
|
||||
)
|
||||
161
game/unitdelivery.py
Normal file
161
game/unitdelivery.py
Normal file
@@ -0,0 +1,161 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.unittype import UnitType, VehicleType
|
||||
|
||||
from game.theater import ControlPoint
|
||||
from .db import PRICES
|
||||
from .theater.transitnetwork import (
|
||||
NoPathError,
|
||||
TransitNetwork,
|
||||
)
|
||||
from .transfers import TransferOrder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .game import Game
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundUnitSource:
|
||||
control_point: ControlPoint
|
||||
|
||||
|
||||
class PendingUnitDeliveries:
|
||||
def __init__(self, destination: ControlPoint) -> None:
|
||||
self.destination = destination
|
||||
|
||||
# Maps unit type to order quantity.
|
||||
self.units: Dict[Type[UnitType], int] = defaultdict(int)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Pending delivery to {self.destination}"
|
||||
|
||||
def order(self, units: Dict[Type[UnitType], int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] += v
|
||||
|
||||
def sell(self, units: Dict[Type[UnitType], int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] -= v
|
||||
|
||||
def refund_all(self, game: Game) -> None:
|
||||
self.refund(game, self.units)
|
||||
self.units = defaultdict(int)
|
||||
|
||||
def refund(self, game: Game, units: Dict[Type[UnitType], int]) -> None:
|
||||
for unit_type, count in units.items():
|
||||
try:
|
||||
price = PRICES[unit_type]
|
||||
except KeyError:
|
||||
logging.error(f"Could not refund {unit_type.id}, price unknown")
|
||||
continue
|
||||
|
||||
logging.info(f"Refunding {count} {unit_type.id} at {self.destination.name}")
|
||||
game.adjust_budget(price * count, player=self.destination.captured)
|
||||
|
||||
def pending_orders(self, unit_type: Type[UnitType]) -> int:
|
||||
pending_units = self.units.get(unit_type)
|
||||
if pending_units is None:
|
||||
pending_units = 0
|
||||
return pending_units
|
||||
|
||||
def available_next_turn(self, unit_type: Type[UnitType]) -> int:
|
||||
current_units = self.destination.base.total_units_of_type(unit_type)
|
||||
return self.pending_orders(unit_type) + current_units
|
||||
|
||||
def process(self, game: Game) -> None:
|
||||
ground_unit_source = self.find_ground_unit_source(game)
|
||||
if ground_unit_source is None:
|
||||
game.message(
|
||||
f"{self.destination.name} lost its source for ground unit "
|
||||
"reinforcements. Refunding purchase price."
|
||||
)
|
||||
self.refund_all(game)
|
||||
return
|
||||
|
||||
bought_units: Dict[Type[UnitType], int] = {}
|
||||
units_needing_transfer: Dict[Type[VehicleType], int] = {}
|
||||
sold_units: Dict[Type[UnitType], int] = {}
|
||||
for unit_type, count in self.units.items():
|
||||
coalition = "Ally" if self.destination.captured else "Enemy"
|
||||
name = unit_type.id
|
||||
|
||||
if (
|
||||
issubclass(unit_type, VehicleType)
|
||||
and self.destination != ground_unit_source
|
||||
):
|
||||
source = ground_unit_source
|
||||
d = units_needing_transfer
|
||||
else:
|
||||
source = self.destination
|
||||
d = bought_units
|
||||
|
||||
if count >= 0:
|
||||
d[unit_type] = count
|
||||
game.message(
|
||||
f"{coalition} reinforcements: {name} x {count} at {source}"
|
||||
)
|
||||
else:
|
||||
sold_units[unit_type] = -count
|
||||
game.message(f"{coalition} sold: {name} x {-count} at {source}")
|
||||
|
||||
self.units = defaultdict(int)
|
||||
self.destination.base.commision_units(bought_units)
|
||||
self.destination.base.commit_losses(sold_units)
|
||||
|
||||
if units_needing_transfer:
|
||||
ground_unit_source.base.commision_units(units_needing_transfer)
|
||||
self.create_transfer(game, ground_unit_source, units_needing_transfer)
|
||||
|
||||
def create_transfer(
|
||||
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
|
||||
) -> None:
|
||||
game.transfers.new_transfer(TransferOrder(source, self.destination, units))
|
||||
|
||||
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
|
||||
# This is running *after* the turn counter has been incremented, so this is the
|
||||
# reaction to turn 0. On turn zero we allow units to be recruited anywhere for
|
||||
# delivery on turn 1 so that turn 1 always starts with units on the front line.
|
||||
if game.turn == 1:
|
||||
return self.destination
|
||||
|
||||
# Fast path if the destination is a valid source.
|
||||
if self.destination.can_recruit_ground_units(game):
|
||||
return self.destination
|
||||
|
||||
try:
|
||||
return self.find_ground_unit_source_in_network(
|
||||
game.transit_network_for(self.destination.captured), game
|
||||
)
|
||||
except NoPathError:
|
||||
return None
|
||||
|
||||
def find_ground_unit_source_in_network(
|
||||
self, network: TransitNetwork, game: Game
|
||||
) -> Optional[ControlPoint]:
|
||||
sources = []
|
||||
for control_point in game.theater.control_points_for(self.destination.captured):
|
||||
if control_point.can_recruit_ground_units(
|
||||
game
|
||||
) and network.has_path_between(self.destination, control_point):
|
||||
sources.append(control_point)
|
||||
|
||||
if not sources:
|
||||
return None
|
||||
|
||||
# Fast path to skip the distance calculation if we have only one option.
|
||||
if len(sources) == 1:
|
||||
return sources[0]
|
||||
|
||||
closest = sources[0]
|
||||
_, cost = network.shortest_path_with_cost(self.destination, closest)
|
||||
for source in sources:
|
||||
_, new_cost = network.shortest_path_with_cost(self.destination, source)
|
||||
if new_cost < cost:
|
||||
closest = source
|
||||
cost = new_cost
|
||||
return closest
|
||||
238
game/unitmap.py
Normal file
238
game/unitmap.py
Normal file
@@ -0,0 +1,238 @@
|
||||
"""Maps generated units back to their Liberation types."""
|
||||
import itertools
|
||||
import math
|
||||
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.squadrons import Pilot
|
||||
from game.theater import Airfield, ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
|
||||
from game.transfers import CargoShip, Convoy, TransferOrder
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FlyingUnit:
|
||||
flight: Flight
|
||||
pilot: Pilot
|
||||
|
||||
|
||||
@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 ConvoyUnit:
|
||||
unit_type: Type[VehicleType]
|
||||
convoy: Convoy
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirliftUnits:
|
||||
cargo: tuple[Type[VehicleType], ...]
|
||||
transfer: TransferOrder
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Building:
|
||||
ground_object: BuildingGroundObject
|
||||
|
||||
|
||||
class UnitMap:
|
||||
def __init__(self) -> None:
|
||||
self.aircraft: Dict[str, FlyingUnit] = {}
|
||||
self.airfields: Dict[str, Airfield] = {}
|
||||
self.front_line_units: Dict[str, FrontLineUnit] = {}
|
||||
self.ground_object_units: Dict[str, GroundObjectUnit] = {}
|
||||
self.buildings: Dict[str, Building] = {}
|
||||
self.convoys: Dict[str, ConvoyUnit] = {}
|
||||
self.cargo_ships: Dict[str, CargoShip] = {}
|
||||
self.airlifts: Dict[str, AirliftUnits] = {}
|
||||
|
||||
def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None:
|
||||
for pilot, unit in zip(flight.roster.pilots, 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}")
|
||||
if pilot is None:
|
||||
raise ValueError(f"{name} has no pilot assigned")
|
||||
self.aircraft[name] = FlyingUnit(flight, pilot)
|
||||
if flight.cargo is not None:
|
||||
self.add_airlift_units(group, flight.cargo)
|
||||
|
||||
def flight(self, unit_name: str) -> Optional[FlyingUnit]:
|
||||
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_convoy_units(self, group: Group, convoy: Convoy) -> 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.convoys:
|
||||
raise RuntimeError(f"Duplicate convoy 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.convoys[name] = ConvoyUnit(unit_type, convoy)
|
||||
|
||||
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
|
||||
return self.convoys.get(name, None)
|
||||
|
||||
def add_cargo_ship(self, group: Group, ship: CargoShip) -> None:
|
||||
if len(group.units) > 1:
|
||||
# Cargo ship "groups" are single units. Killing the one ship kills the whole
|
||||
# transfer. If we ever want to add escorts or create multiple cargo ships in
|
||||
# a convoy of ships that logic needs to change.
|
||||
raise ValueError("Expected cargo ship to be a single unit group.")
|
||||
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.cargo_ships:
|
||||
raise RuntimeError(f"Duplicate cargo ship: {name}")
|
||||
self.cargo_ships[name] = ship
|
||||
|
||||
def cargo_ship(self, name: str) -> Optional[CargoShip]:
|
||||
return self.cargo_ships.get(name, None)
|
||||
|
||||
def add_airlift_units(self, group: FlyingGroup, transfer: TransferOrder) -> None:
|
||||
capacity_each = math.ceil(transfer.size / len(group.units))
|
||||
for idx, transport in enumerate(group.units):
|
||||
# Slice the units in groups based on the capacity of each unit. Cargo is
|
||||
# assigned arbitrarily to units in the order of the group. The last unit in
|
||||
# the group will receive a partial load if there is not enough cargo to fill
|
||||
# every transport.
|
||||
base_idx = idx * capacity_each
|
||||
cargo = tuple(
|
||||
itertools.islice(
|
||||
transfer.iter_units(), base_idx, base_idx + capacity_each
|
||||
)
|
||||
)
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(transport.name)
|
||||
if name in self.airlifts:
|
||||
raise RuntimeError(f"Duplicate airlift unit: {name}")
|
||||
self.airlifts[name] = AirliftUnits(cargo, transfer)
|
||||
|
||||
def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
|
||||
return self.airlifts.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__.
|
||||
# The name of the initiator in the DCS dead event will have " object"
|
||||
# appended for statics.
|
||||
name = f"{group.name} object"
|
||||
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 add_scenery(self, ground_object: SceneryGroundObject) -> None:
|
||||
name = str(ground_object.map_object_id)
|
||||
if name in self.buildings:
|
||||
raise RuntimeError(
|
||||
f"Duplicate TGO unit: {name}. TriggerZone name: "
|
||||
f"{ground_object.dcs_identifier}"
|
||||
)
|
||||
|
||||
self.buildings[name] = Building(ground_object)
|
||||
|
||||
def building_or_fortification(self, name: str) -> Optional[Building]:
|
||||
return self.buildings.get(name, None)
|
||||
193
game/utils.py
193
game/utils.py
@@ -1,14 +1,191 @@
|
||||
def meter_to_feet(value_in_meter: float) -> int:
|
||||
return int(3.28084 * value_in_meter)
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
METERS_TO_FEET = 3.28084
|
||||
FEET_TO_METERS = 1 / METERS_TO_FEET
|
||||
NM_TO_METERS = 1852
|
||||
METERS_TO_NM = 1 / NM_TO_METERS
|
||||
|
||||
KNOTS_TO_KPH = 1.852
|
||||
KPH_TO_KNOTS = 1 / KNOTS_TO_KPH
|
||||
MS_TO_KPH = 3.6
|
||||
KPH_TO_MS = 1 / MS_TO_KPH
|
||||
|
||||
|
||||
def feet_to_meter(value_in_feet: float) -> int:
|
||||
return int(value_in_feet / 3.28084)
|
||||
def heading_sum(h, a) -> int:
|
||||
h += a
|
||||
if h > 360:
|
||||
return h - 360
|
||||
elif h < 0:
|
||||
return 360 + h
|
||||
else:
|
||||
return h
|
||||
|
||||
|
||||
def meter_to_nm(value_in_meter: float) -> int:
|
||||
return int(value_in_meter / 1852)
|
||||
def opposite_heading(h):
|
||||
return heading_sum(h, 180)
|
||||
|
||||
|
||||
def nm_to_meter(value_in_nm: float) -> int:
|
||||
return int(value_in_nm * 1852)
|
||||
@dataclass(frozen=True, order=True)
|
||||
class Distance:
|
||||
distance_in_meters: float
|
||||
|
||||
@property
|
||||
def feet(self) -> float:
|
||||
return self.distance_in_meters * METERS_TO_FEET
|
||||
|
||||
@property
|
||||
def meters(self) -> float:
|
||||
return self.distance_in_meters
|
||||
|
||||
@property
|
||||
def nautical_miles(self) -> float:
|
||||
return self.distance_in_meters * METERS_TO_NM
|
||||
|
||||
@classmethod
|
||||
def from_feet(cls, value: float) -> Distance:
|
||||
return cls(value * FEET_TO_METERS)
|
||||
|
||||
@classmethod
|
||||
def from_meters(cls, value: float) -> Distance:
|
||||
return cls(value)
|
||||
|
||||
@classmethod
|
||||
def from_nautical_miles(cls, value: float) -> Distance:
|
||||
return cls(value * NM_TO_METERS)
|
||||
|
||||
def __add__(self, other: Distance) -> Distance:
|
||||
return meters(self.meters + other.meters)
|
||||
|
||||
def __sub__(self, other: Distance) -> Distance:
|
||||
return meters(self.meters - other.meters)
|
||||
|
||||
def __mul__(self, other: Union[float, int]) -> Distance:
|
||||
return meters(self.meters * other)
|
||||
|
||||
def __truediv__(self, other: Union[float, int]) -> Distance:
|
||||
return meters(self.meters / other)
|
||||
|
||||
def __floordiv__(self, other: Union[float, int]) -> Distance:
|
||||
return meters(self.meters // other)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return not math.isclose(self.meters, 0.0)
|
||||
|
||||
|
||||
def feet(value: float) -> Distance:
|
||||
return Distance.from_feet(value)
|
||||
|
||||
|
||||
def meters(value: float) -> Distance:
|
||||
return Distance.from_meters(value)
|
||||
|
||||
|
||||
def nautical_miles(value: float) -> Distance:
|
||||
return Distance.from_nautical_miles(value)
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class Speed:
|
||||
speed_in_kph: float
|
||||
|
||||
@property
|
||||
def knots(self) -> float:
|
||||
return self.speed_in_kph * KPH_TO_KNOTS
|
||||
|
||||
@property
|
||||
def kph(self) -> float:
|
||||
return self.speed_in_kph
|
||||
|
||||
@property
|
||||
def meters_per_second(self) -> float:
|
||||
return self.speed_in_kph * KPH_TO_MS
|
||||
|
||||
def mach(self, altitude: Distance = meters(0)) -> float:
|
||||
c_sound = mach(1, altitude)
|
||||
return self.speed_in_kph / c_sound.kph
|
||||
|
||||
@classmethod
|
||||
def from_knots(cls, value: float) -> Speed:
|
||||
return cls(value * KNOTS_TO_KPH)
|
||||
|
||||
@classmethod
|
||||
def from_kph(cls, value: float) -> Speed:
|
||||
return cls(value)
|
||||
|
||||
@classmethod
|
||||
def from_meters_per_second(cls, value: float) -> Speed:
|
||||
return cls(value * MS_TO_KPH)
|
||||
|
||||
@classmethod
|
||||
def from_mach(cls, value: float, altitude: Distance) -> Speed:
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
|
||||
if altitude <= feet(36152):
|
||||
temperature_f = 59 - 0.00356 * altitude.feet
|
||||
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)
|
||||
return mps(c_sound) * value
|
||||
|
||||
def __add__(self, other: Speed) -> Speed:
|
||||
return kph(self.kph + other.kph)
|
||||
|
||||
def __sub__(self, other: Speed) -> Speed:
|
||||
return kph(self.kph - other.kph)
|
||||
|
||||
def __mul__(self, other: Union[float, int]) -> Speed:
|
||||
return kph(self.kph * other)
|
||||
|
||||
def __truediv__(self, other: Union[float, int]) -> Speed:
|
||||
return kph(self.kph / other)
|
||||
|
||||
def __floordiv__(self, other: Union[float, int]) -> Speed:
|
||||
return kph(self.kph // other)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return not math.isclose(self.kph, 0.0)
|
||||
|
||||
|
||||
def knots(value: float) -> Speed:
|
||||
return Speed.from_knots(value)
|
||||
|
||||
|
||||
def kph(value: float) -> Speed:
|
||||
return Speed.from_kph(value)
|
||||
|
||||
|
||||
def mps(value: float) -> Speed:
|
||||
return Speed.from_meters_per_second(value)
|
||||
|
||||
|
||||
def mach(value: float, altitude: Distance) -> Speed:
|
||||
return Speed.from_mach(value, altitude)
|
||||
|
||||
|
||||
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
|
||||
|
||||
|
||||
def pairwise(iterable):
|
||||
"""
|
||||
itertools recipe
|
||||
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
||||
"""
|
||||
a, b = itertools.tee(iterable)
|
||||
next(b, None)
|
||||
return zip(a, b)
|
||||
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
def _build_version_string() -> str:
|
||||
components = ["2.2.0"]
|
||||
components = ["3.1.0"]
|
||||
build_number_path = Path("resources/buildnumber")
|
||||
if build_number_path.exists():
|
||||
with build_number_path.open("r") as build_number_file:
|
||||
@@ -16,3 +16,75 @@ def _build_version_string() -> str:
|
||||
|
||||
#: Current version of Liberation.
|
||||
VERSION = _build_version_string()
|
||||
|
||||
#: The latest version of the campaign format. Increment this version whenever all
|
||||
#: existing campaigns should be flagged as incompatible in the UI. We will still attempt
|
||||
#: to load old campaigns, but this provides a warning to the user that the campaign may
|
||||
#: not work correctly.
|
||||
#:
|
||||
#: There is no verification that the campaign author updated their campaign correctly
|
||||
#: this is just a UI hint.
|
||||
#:
|
||||
#: Version history:
|
||||
#:
|
||||
#: Version 0
|
||||
#: * Unknown compatibility.
|
||||
#:
|
||||
#: Version 1
|
||||
#: * Compatible with Liberation 2.5.
|
||||
#:
|
||||
#: Version 2
|
||||
#: * Front line endpoints now define convoy origin/destination waypoints. They should be
|
||||
#: placed on or near roads.
|
||||
#: * Factories (Workshop_A) define factory objectives. Only control points with
|
||||
#: factories will be able to recruit ground units, so they should exist in sufficient
|
||||
#: number and be protected by IADS.
|
||||
#:
|
||||
#: Version 3
|
||||
#: * Bulker Handy Winds define shipping lanes. They should be placed in port areas that
|
||||
#: are navigable by ships and have a route to another port area. DCS ships *will not*
|
||||
#: avoid driving into islands, so ensure that their waypoints plot a navigable route.
|
||||
#:
|
||||
#: Version 4
|
||||
#: * TriggerZones define map based building targets. White TriggerZones created by right
|
||||
#: clicking an object and using "assign as..." define the buildings within an objective.
|
||||
#: Blue circular TriggerZones created normally must surround groups of one or more
|
||||
#: white TriggerZones to define an objective. If a white TriggerZone is not surrounded
|
||||
#: by a blue circular TriggerZone, campaign creation will fail. Blue circular
|
||||
#: TriggerZones must also have their first property's value field define the type of
|
||||
#: objective (a valid value for a building TGO category, from `game.db.PRICES`).
|
||||
#:
|
||||
#: Version 4.1
|
||||
#: * All objective types may now be set as required generation (similar to the required
|
||||
#: IADS generation). This includes:
|
||||
#: * SHORADS
|
||||
#: * Armor groups
|
||||
#: * Strike targets
|
||||
#: * Offshore strike targets
|
||||
#: * Ships
|
||||
#: * Missile sites
|
||||
#: * Coastal defenses
|
||||
#:
|
||||
#: See the unit lists in MizCampaignLoader in conflicttheater.py for unit types.
|
||||
#:
|
||||
#: Version 4.2
|
||||
#: * Adds support for AAA objectives. Place with any of the following units (either red
|
||||
#: or blue):
|
||||
#: * AAA_8_8cm_Flak_18,
|
||||
#: * SPAAA_Vulcan_M163,
|
||||
#: * SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
#:
|
||||
#: Version 5.0
|
||||
#: * Ammunition Depots objective locations are now predetermined using the "Ammunition
|
||||
# Depot" Warehouse object, and through trigger zone based scenery objects.
|
||||
#: * The number of alive Ammunition Depot objective buildings connected to a control
|
||||
#: point directly influences how many ground units can be supported on the front
|
||||
#: line.
|
||||
#: * The number of supported ground units at any control point is artificially
|
||||
#: capped at 50, even if the number of alive Ammunition Depot objectives can
|
||||
#: support more.
|
||||
#:
|
||||
#: Version 6.0
|
||||
#: * Random objective generation no is longer supported. Fixed objective locations were
|
||||
#: added in 4.1.
|
||||
CAMPAIGN_FORMAT_VERSION = (6, 0)
|
||||
|
||||
@@ -3,14 +3,18 @@ from __future__ import annotations
|
||||
import datetime
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.weather import Weather as PydcsWeather, Wind
|
||||
from dcs.cloud_presets import Clouds as PydcsClouds
|
||||
from dcs.weather import CloudPreset, Weather as PydcsWeather, Wind
|
||||
|
||||
from game.settings import Settings
|
||||
from theater import ConflictTheater
|
||||
from game.utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater
|
||||
|
||||
|
||||
class TimeOfDay(Enum):
|
||||
@@ -33,11 +37,28 @@ class Clouds:
|
||||
density: int
|
||||
thickness: int
|
||||
precipitation: PydcsWeather.Preceptions
|
||||
preset: Optional[CloudPreset] = field(default=None)
|
||||
|
||||
@classmethod
|
||||
def random_preset(cls, rain: bool) -> Clouds:
|
||||
clouds = (p.value for p in PydcsClouds)
|
||||
if rain:
|
||||
presets = [p for p in clouds if "Rain" in p.name]
|
||||
else:
|
||||
presets = [p for p in clouds if "Rain" not in p.name]
|
||||
preset = random.choice(presets)
|
||||
return Clouds(
|
||||
base=random.randint(preset.min_base, preset.max_base),
|
||||
density=0,
|
||||
thickness=0,
|
||||
precipitation=PydcsWeather.Preceptions.None_,
|
||||
preset=preset,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Fog:
|
||||
visibility: int
|
||||
visibility: Distance
|
||||
thickness: int
|
||||
|
||||
|
||||
@@ -54,8 +75,8 @@ class Weather:
|
||||
if random.randrange(5) != 0:
|
||||
return None
|
||||
return Fog(
|
||||
visibility=random.randint(2500, 5000),
|
||||
thickness=random.randint(100, 500)
|
||||
visibility=meters(random.randint(2500, 5000)),
|
||||
thickness=random.randint(100, 500),
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
@@ -73,7 +94,7 @@ class Weather:
|
||||
# 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)
|
||||
at_8000m=Wind(wind_direction, base_wind * at_8000m_factor),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -98,12 +119,11 @@ class ClearSkies(Weather):
|
||||
|
||||
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_
|
||||
)
|
||||
return Clouds.random_preset(rain=False)
|
||||
|
||||
def generate_fog(self) -> Optional[Fog]:
|
||||
# DCS 2.7 says to not use fog with the cloud presets.
|
||||
return None
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 4)
|
||||
@@ -111,12 +131,11 @@ class Cloudy(Weather):
|
||||
|
||||
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
|
||||
)
|
||||
return Clouds.random_preset(rain=True)
|
||||
|
||||
def generate_fog(self) -> Optional[Fog]:
|
||||
# DCS 2.7 says to not use fog with the cloud presets.
|
||||
return None
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 6)
|
||||
@@ -128,7 +147,7 @@ class Thunderstorm(Weather):
|
||||
base=self.random_cloud_base(),
|
||||
density=random.randint(9, 10),
|
||||
thickness=self.random_cloud_thickness(),
|
||||
precipitation=PydcsWeather.Preceptions.Thunderstorm
|
||||
precipitation=PydcsWeather.Preceptions.Thunderstorm,
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
@@ -142,20 +161,29 @@ class Conditions:
|
||||
weather: Weather
|
||||
|
||||
@classmethod
|
||||
def generate(cls, theater: ConflictTheater, day: datetime.date,
|
||||
time_of_day: TimeOfDay, settings: Settings) -> Conditions:
|
||||
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()
|
||||
weather=cls.generate_weather(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate_start_time(cls, theater: ConflictTheater, day: datetime.date,
|
||||
time_of_day: TimeOfDay,
|
||||
night_disabled: bool) -> datetime.datetime:
|
||||
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 = {
|
||||
@@ -178,6 +206,7 @@ class Conditions:
|
||||
Cloudy: 60,
|
||||
ClearSkies: 20,
|
||||
}
|
||||
weather_type = random.choices(list(chances.keys()),
|
||||
weights=list(chances.values()))[0]
|
||||
weather_type = random.choices(
|
||||
list(chances.keys()), weights=list(chances.values())
|
||||
)[0]
|
||||
return weather_type()
|
||||
|
||||
1521
gen/aircraft.py
1521
gen/aircraft.py
File diff suppressed because it is too large
Load Diff
438
gen/airfields.py
438
gen/airfields.py
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,11 @@
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Type
|
||||
from datetime import timedelta
|
||||
from typing import List, Type, Tuple, Optional
|
||||
|
||||
from dcs.mission import Mission, StartType
|
||||
from dcs.planes import IL_78M
|
||||
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.task import (
|
||||
AWACS,
|
||||
ActivateBeaconCommand,
|
||||
@@ -19,6 +22,7 @@ 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
|
||||
@@ -30,19 +34,26 @@ AWACS_ALT = 13000
|
||||
@dataclass
|
||||
class AwacsInfo:
|
||||
"""AWACS information for the kneeboard."""
|
||||
dcsGroupName: str
|
||||
|
||||
group_name: str
|
||||
callsign: str
|
||||
freq: RadioFrequency
|
||||
depature_location: Optional[str]
|
||||
start_time: Optional[timedelta]
|
||||
end_time: Optional[timedelta]
|
||||
blue: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class TankerInfo:
|
||||
"""Tanker information for the kneeboard."""
|
||||
dcsGroupName: str
|
||||
|
||||
group_name: str
|
||||
callsign: str
|
||||
variant: str
|
||||
freq: RadioFrequency
|
||||
tacan: TacanChannel
|
||||
blue: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -52,10 +63,14 @@ class AirSupport:
|
||||
|
||||
|
||||
class AirSupportConflictGenerator:
|
||||
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game,
|
||||
radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
mission: Mission,
|
||||
conflict: Conflict,
|
||||
game,
|
||||
radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry,
|
||||
) -> None:
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
@@ -67,28 +82,54 @@ class AirSupportConflictGenerator:
|
||||
def support_tasks(cls) -> List[Type[MainTask]]:
|
||||
return [Refueling, AWACS]
|
||||
|
||||
def generate(self, is_awacs_enabled):
|
||||
player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp
|
||||
@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.blue_cp
|
||||
if self.conflict.blue_cp.captured
|
||||
else self.conflict.red_cp
|
||||
)
|
||||
|
||||
fallback_tanker_number = 0
|
||||
|
||||
for i, tanker_unit_type in enumerate(db.find_unittype(Refueling, self.conflict.attackers_side)):
|
||||
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_heading = (
|
||||
self.conflict.red_cp.position.heading_between_point(
|
||||
self.conflict.blue_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),
|
||||
name=namegen.next_tanker_name(
|
||||
self.mission.country(self.game.player_country), tanker_unit_type
|
||||
),
|
||||
airport=None,
|
||||
plane_type=tanker_unit_type,
|
||||
position=tanker_position,
|
||||
altitude=TANKER_ALT,
|
||||
altitude=alt,
|
||||
race_distance=58000,
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
speed=574,
|
||||
speed=airspeed,
|
||||
tacanchannel=str(tacan),
|
||||
)
|
||||
tanker_group.set_frequency(freq.mhz)
|
||||
@@ -111,26 +152,49 @@ class AirSupportConflictGenerator:
|
||||
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(
|
||||
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))
|
||||
self.air_support.tankers.append(
|
||||
TankerInfo(
|
||||
str(tanker_group.name),
|
||||
callsign,
|
||||
variant,
|
||||
freq,
|
||||
tacan,
|
||||
blue=True,
|
||||
)
|
||||
)
|
||||
|
||||
if is_awacs_enabled:
|
||||
try:
|
||||
if not self.game.settings.disable_legacy_aewc:
|
||||
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_unit = db.find_unittype(AWACS, self.conflict.attackers_side)[0]
|
||||
|
||||
awacs_flight = self.mission.awacs_flight(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
name=namegen.next_awacs_name(self.mission.country(self.game.player_country)),
|
||||
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),
|
||||
position=self.conflict.position.random_point_within(
|
||||
AWACS_DISTANCE, AWACS_DISTANCE
|
||||
),
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
)
|
||||
@@ -139,7 +203,16 @@ class AirSupportConflictGenerator:
|
||||
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.air_support.awacs.append(AwacsInfo(
|
||||
str(awacs_flight.name), callsign_for_support_unit(awacs_flight), freq))
|
||||
except:
|
||||
print("No AWACS for faction")
|
||||
self.air_support.awacs.append(
|
||||
AwacsInfo(
|
||||
group_name=str(awacs_flight.name),
|
||||
callsign=callsign_for_support_unit(awacs_flight),
|
||||
freq=freq,
|
||||
depature_location=None,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
blue=True,
|
||||
)
|
||||
)
|
||||
else:
|
||||
logging.warning("No AWACS for faction")
|
||||
|
||||
826
gen/armor.py
826
gen/armor.py
File diff suppressed because it is too large
Load Diff
53
gen/ato.py
53
gen/ato.py
@@ -8,6 +8,8 @@ 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.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
@@ -16,9 +18,11 @@ from typing import Dict, List, Optional
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
from theater.missiontarget import MissionTarget
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.utils import Speed
|
||||
from .flights.flight import Flight, FlightType
|
||||
from .flights.flightplan import FormationFlightPlan
|
||||
from .flights.traveltime import TotEstimator
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -53,13 +57,22 @@ class Package:
|
||||
|
||||
delay: int = field(default=0)
|
||||
|
||||
#: True if the package ToT should be reset to ASAP whenever the player makes
|
||||
#: a change. This is really a UI property rather than a game property, but
|
||||
#: we want it to persist in the save.
|
||||
auto_asap: bool = field(default=False)
|
||||
|
||||
#: 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]:
|
||||
def has_players(self) -> bool:
|
||||
return any(flight.client_count for flight in self.flights)
|
||||
|
||||
@property
|
||||
def formation_speed(self) -> Optional[Speed]:
|
||||
"""The speed of the package when in formation.
|
||||
|
||||
If none of the flights in the package will join a formation, this
|
||||
@@ -89,7 +102,8 @@ class Package:
|
||||
if tot is None:
|
||||
logging.error(
|
||||
f"{flight} requested escort at {waypoint} but that "
|
||||
"waypoint has no TOT. It may not be escorted.")
|
||||
"waypoint has no TOT. It may not be escorted."
|
||||
)
|
||||
continue
|
||||
times.append(tot)
|
||||
if times:
|
||||
@@ -110,13 +124,26 @@ class Package:
|
||||
logging.error(
|
||||
f"{flight} dismissed escort at {waypoint} but that "
|
||||
"waypoint has no TOT or departure time. It may not be "
|
||||
"escorted.")
|
||||
"escorted."
|
||||
)
|
||||
continue
|
||||
times.append(tot)
|
||||
if times:
|
||||
return max(times)
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> Optional[timedelta]:
|
||||
times = []
|
||||
for flight in self.flights:
|
||||
times.append(flight.flight_plan.mission_departure_time)
|
||||
if times:
|
||||
return max(times)
|
||||
return None
|
||||
|
||||
def set_tot_asap(self) -> None:
|
||||
self.time_over_target = TotEstimator(self).earliest_tot()
|
||||
|
||||
def add_flight(self, flight: Flight) -> None:
|
||||
"""Adds a flight to the package."""
|
||||
self.flights.append(flight)
|
||||
@@ -147,19 +174,16 @@ class Package:
|
||||
FlightType.CAS,
|
||||
FlightType.STRIKE,
|
||||
FlightType.ANTISHIP,
|
||||
FlightType.OCA_AIRCRAFT,
|
||||
FlightType.OCA_RUNWAY,
|
||||
FlightType.BAI,
|
||||
FlightType.EVAC,
|
||||
FlightType.TROOP_TRANSPORT,
|
||||
FlightType.RECON,
|
||||
FlightType.ELINT,
|
||||
FlightType.DEAD,
|
||||
FlightType.TRANSPORT,
|
||||
FlightType.SEAD,
|
||||
FlightType.LOGISTICS,
|
||||
FlightType.INTERCEPTION,
|
||||
FlightType.TARCAP,
|
||||
FlightType.CAP,
|
||||
FlightType.BARCAP,
|
||||
FlightType.EWAR,
|
||||
FlightType.AEWC,
|
||||
FlightType.SWEEP,
|
||||
FlightType.ESCORT,
|
||||
]
|
||||
for task in task_priorities:
|
||||
@@ -178,7 +202,10 @@ class Package:
|
||||
task = self.primary_task
|
||||
if task is None:
|
||||
return "No mission"
|
||||
return task.name
|
||||
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?
|
||||
|
||||
@@ -2,29 +2,33 @@
|
||||
Briefing generation logic
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from theater.frontline import FrontLine
|
||||
from typing import List, Dict, TYPE_CHECKING
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
from 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 theater import ControlPoint
|
||||
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
|
||||
|
||||
@@ -32,14 +36,17 @@ class CommInfo:
|
||||
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_base: ControlPoint = front_line.blue_cp
|
||||
self.enemy_base: ControlPoint = front_line.red_cp
|
||||
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.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.
|
||||
|
||||
@@ -119,8 +126,17 @@ class MissionInfoGenerator:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BriefingGenerator(MissionInfoGenerator):
|
||||
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]] = {}
|
||||
@@ -130,35 +146,36 @@ class BriefingGenerator(MissionInfoGenerator):
|
||||
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
|
||||
"""
|
||||
"""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"))
|
||||
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):
|
||||
"""Build FrontLineInfo objects from FrontLine type and append to briefing."""
|
||||
for front_line in self.game.theater.conflicts():
|
||||
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.
|
||||
"""
|
||||
"""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?
|
||||
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]
|
||||
|
||||
47
gen/cargoshipgen.py
Normal file
47
gen/cargoshipgen.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.ships import Bulker_Handy_Wind
|
||||
from dcs.unitgroup import ShipGroup
|
||||
|
||||
from game.transfers import CargoShip
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import knots
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
class CargoShipGenerator:
|
||||
def __init__(self, mission: Mission, game: Game, unit_map: UnitMap) -> None:
|
||||
self.mission = mission
|
||||
self.game = game
|
||||
self.unit_map = unit_map
|
||||
self.count = itertools.count()
|
||||
|
||||
def generate(self) -> None:
|
||||
# Reset the count to make generation deterministic.
|
||||
for ship in self.game.transfers.cargo_ships:
|
||||
self.generate_cargo_ship(ship)
|
||||
|
||||
def generate_cargo_ship(self, ship: CargoShip) -> ShipGroup:
|
||||
country = self.mission.country(
|
||||
self.game.player_country if ship.player_owned else self.game.enemy_country
|
||||
)
|
||||
waypoints = ship.route
|
||||
group = self.mission.ship_group(
|
||||
country,
|
||||
ship.name,
|
||||
Bulker_Handy_Wind,
|
||||
position=waypoints[0],
|
||||
group_size=1,
|
||||
)
|
||||
for waypoint in waypoints[1:]:
|
||||
# 12 knots is very slow but it's also nearly the max allowed by DCS for this
|
||||
# type of ship.
|
||||
group.add_waypoint(waypoint, speed=knots(12).kph)
|
||||
self.unit_map.add_cargo_ship(group, ship)
|
||||
return group
|
||||
31
gen/coastal/coastal_group_generator.py
Normal file
31
gen/coastal/coastal_group_generator.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import logging
|
||||
import random
|
||||
from game import db
|
||||
from gen.coastal.silkworm import SilkwormGenerator
|
||||
|
||||
COASTAL_MAP = {
|
||||
"SilkwormGenerator": SilkwormGenerator,
|
||||
}
|
||||
|
||||
|
||||
def generate_coastal_group(game, ground_object, faction_name: str):
|
||||
"""
|
||||
This generate a coastal defenses group
|
||||
:return: Nothing, but put the group reference inside the ground object
|
||||
"""
|
||||
faction = db.FACTIONS[faction_name]
|
||||
if len(faction.coastal_defenses) > 0:
|
||||
generators = faction.coastal_defenses
|
||||
if len(generators) > 0:
|
||||
gen = random.choice(generators)
|
||||
if gen in COASTAL_MAP.keys():
|
||||
generator = COASTAL_MAP[gen](game, ground_object, faction)
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
else:
|
||||
logging.info(
|
||||
"Unable to generate missile group, generator : "
|
||||
+ str(gen)
|
||||
+ "does not exists"
|
||||
)
|
||||
return None
|
||||
58
gen/coastal/silkworm.py
Normal file
58
gen/coastal/silkworm.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from dcs.vehicles import MissilesSS, Unarmed, AirDefence
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class SilkwormGenerator(GroupGenerator):
|
||||
def __init__(self, game, ground_object, faction):
|
||||
super(SilkwormGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
|
||||
def generate(self):
|
||||
|
||||
positions = self.get_circular_position(5, launcher_distance=120, coverage=180)
|
||||
|
||||
self.add_unit(
|
||||
MissilesSS.AShM_Silkworm_SR,
|
||||
"SR#0",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Launchers
|
||||
for i, p in enumerate(positions):
|
||||
self.add_unit(
|
||||
MissilesSS.AShM_SS_N_2_Silkworm,
|
||||
"Missile#" + str(i),
|
||||
p[0],
|
||||
p[1],
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Commander
|
||||
self.add_unit(
|
||||
Unarmed.Truck_KAMAZ_43101,
|
||||
"KAMAZ#0",
|
||||
self.position.x - 35,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Shorad
|
||||
self.add_unit(
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
"SHILKA#0",
|
||||
self.position.x - 55,
|
||||
self.position.y - 38,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Shorad 2
|
||||
self.add_unit(
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
|
||||
"STRELA#0",
|
||||
self.position.x + 200,
|
||||
self.position.y + 15,
|
||||
90,
|
||||
)
|
||||
@@ -1,502 +1,160 @@
|
||||
import logging
|
||||
import random
|
||||
from typing import Tuple
|
||||
from typing import Tuple, Optional
|
||||
|
||||
from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
from shapely.geometry import LineString, Point as ShapelyPoint
|
||||
|
||||
from theater import ConflictTheater, ControlPoint
|
||||
from game.theater.conflicttheater import ConflictTheater, FrontLine
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
from game.utils import heading_sum, opposite_heading
|
||||
|
||||
AIR_DISTANCE = 40000
|
||||
|
||||
CAPTURE_AIR_ATTACKERS_DISTANCE = 25000
|
||||
CAPTURE_AIR_DEFENDERS_DISTANCE = 60000
|
||||
STRIKE_AIR_ATTACKERS_DISTANCE = 45000
|
||||
STRIKE_AIR_DEFENDERS_DISTANCE = 25000
|
||||
|
||||
CAP_CAS_DISTANCE = 10000, 120000
|
||||
|
||||
GROUND_INTERCEPT_SPREAD = 5000
|
||||
GROUND_DISTANCE_FACTOR = 1.4
|
||||
GROUND_DISTANCE = 2000
|
||||
|
||||
GROUND_ATTACK_DISTANCE = 25000, 13000
|
||||
|
||||
TRANSPORT_FRONTLINE_DIST = 1800
|
||||
|
||||
INTERCEPT_ATTACKERS_HEADING = -45, 45
|
||||
INTERCEPT_DEFENDERS_HEADING = -10, 10
|
||||
INTERCEPT_CONFLICT_DISTANCE = 50000
|
||||
INTERCEPT_ATTACKERS_DISTANCE = 100000
|
||||
INTERCEPT_MAX_DISTANCE = 160000
|
||||
INTERCEPT_MIN_DISTANCE = 100000
|
||||
|
||||
NAVAL_INTERCEPT_DISTANCE_FACTOR = 1
|
||||
NAVAL_INTERCEPT_DISTANCE_MAX = 40000
|
||||
NAVAL_INTERCEPT_STEP = 5000
|
||||
|
||||
FRONTLINE_LENGTH = 80000
|
||||
FRONTLINE_MIN_CP_DISTANCE = 5000
|
||||
FRONTLINE_DISTANCE_STRENGTH_FACTOR = 0.7
|
||||
|
||||
|
||||
def _opposite_heading(h):
|
||||
return h+180
|
||||
|
||||
|
||||
def _heading_sum(h, a) -> int:
|
||||
h += a
|
||||
if h > 360:
|
||||
return h - 360
|
||||
elif h < 0:
|
||||
return 360 + h
|
||||
else:
|
||||
return h
|
||||
|
||||
|
||||
class Conflict:
|
||||
def __init__(self,
|
||||
theater: ConflictTheater,
|
||||
from_cp: ControlPoint,
|
||||
to_cp: ControlPoint,
|
||||
attackers_side: str,
|
||||
defenders_side: str,
|
||||
attackers_country: Country,
|
||||
defenders_country: Country,
|
||||
position: Point,
|
||||
heading=None,
|
||||
distance=None,
|
||||
ground_attackers_location: Point = None,
|
||||
ground_defenders_location: Point = None,
|
||||
air_attackers_location: Point = None,
|
||||
air_defenders_location: Point = None):
|
||||
def __init__(
|
||||
self,
|
||||
theater: ConflictTheater,
|
||||
front_line: FrontLine,
|
||||
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.front_line = front_line
|
||||
self.theater = theater
|
||||
self.position = position
|
||||
self.heading = heading
|
||||
self.distance = distance
|
||||
self.size = to_cp.size
|
||||
self.radials = to_cp.radials
|
||||
self.ground_attackers_location = ground_attackers_location
|
||||
self.ground_defenders_location = ground_defenders_location
|
||||
self.air_attackers_location = air_attackers_location
|
||||
self.air_defenders_location = air_defenders_location
|
||||
self.size = size
|
||||
|
||||
@property
|
||||
def center(self) -> Point:
|
||||
return self.position.point_from_heading(self.heading, self.distance / 2)
|
||||
def blue_cp(self) -> ControlPoint:
|
||||
return self.front_line.blue_cp
|
||||
|
||||
@property
|
||||
def tail(self) -> Point:
|
||||
return self.position.point_from_heading(self.heading, self.distance)
|
||||
|
||||
@property
|
||||
def is_vector(self) -> bool:
|
||||
return self.heading is not None
|
||||
|
||||
@property
|
||||
def opposite_heading(self) -> int:
|
||||
return _heading_sum(self.heading, 180)
|
||||
|
||||
@property
|
||||
def to_size(self):
|
||||
return self.to_cp.size * GROUND_DISTANCE_FACTOR
|
||||
|
||||
def find_insertion_point(self, other_point: Point) -> Point:
|
||||
if self.is_vector:
|
||||
dx = self.position.x - self.tail.x
|
||||
dy = self.position.y - self.tail.y
|
||||
dr2 = float(dx ** 2 + dy ** 2)
|
||||
|
||||
lerp = ((other_point.x - self.tail.x) * dx + (other_point.y - self.tail.y) * dy) / dr2
|
||||
if lerp < 0:
|
||||
lerp = 0
|
||||
elif lerp > 1:
|
||||
lerp = 1
|
||||
|
||||
x = lerp * dx + self.tail.x
|
||||
y = lerp * dy + self.tail.y
|
||||
return Point(x, y)
|
||||
else:
|
||||
return self.position
|
||||
|
||||
def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> Point:
|
||||
return Conflict._find_ground_position(at, max_distance, heading, self.theater)
|
||||
def red_cp(self) -> ControlPoint:
|
||||
return self.front_line.red_cp
|
||||
|
||||
@classmethod
|
||||
def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool:
|
||||
return from_cp.has_frontline and to_cp.has_frontline
|
||||
|
||||
@classmethod
|
||||
def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]:
|
||||
attack_heading = from_cp.position.heading_between_point(to_cp.position)
|
||||
attack_distance = from_cp.position.distance_to_point(to_cp.position)
|
||||
middle_point = from_cp.position.point_from_heading(attack_heading, attack_distance / 2)
|
||||
|
||||
strength_delta = (from_cp.base.strength - to_cp.base.strength) / 1.0
|
||||
position = middle_point.point_from_heading(attack_heading, strength_delta * attack_distance / 2 - FRONTLINE_MIN_CP_DISTANCE)
|
||||
return position, _opposite_heading(attack_heading)
|
||||
|
||||
def frontline_position(
|
||||
cls, frontline: FrontLine, theater: ConflictTheater
|
||||
) -> Tuple[Point, int]:
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:
|
||||
def frontline_vector(
|
||||
cls, front_line: FrontLine, theater: ConflictTheater
|
||||
) -> Tuple[Point, int, int]:
|
||||
"""
|
||||
probe_end_point = initial.point_from_heading(heading, FRONTLINE_LENGTH)
|
||||
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ])
|
||||
intersection = probe.intersection(theater.land_poly)
|
||||
|
||||
if isinstance(intersection, geometry.LineString):
|
||||
intersection = intersection
|
||||
elif isinstance(intersection, geometry.MultiLineString):
|
||||
intersection = intersection.geoms[0]
|
||||
else:
|
||||
print(intersection)
|
||||
return None
|
||||
|
||||
return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length
|
||||
Returns a vector for a valid frontline location avoiding exclusion zones.
|
||||
"""
|
||||
frontline = cls.frontline_position(theater, from_cp, to_cp)
|
||||
center_position, heading = frontline
|
||||
left_position, right_position = None, None
|
||||
|
||||
if not theater.is_on_land(center_position):
|
||||
pos = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, -90), theater)
|
||||
if pos:
|
||||
right_position = pos
|
||||
center_position = pos
|
||||
else:
|
||||
pos = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, +90), theater)
|
||||
if pos:
|
||||
left_position = pos
|
||||
center_position = pos
|
||||
|
||||
if left_position is None:
|
||||
left_position = cls._extend_ground_position(center_position, int(FRONTLINE_LENGTH/2), _heading_sum(heading, -90), theater)
|
||||
|
||||
if right_position is None:
|
||||
right_position = cls._extend_ground_position(center_position, int(FRONTLINE_LENGTH/2), _heading_sum(heading, 90), theater)
|
||||
|
||||
return left_position, _heading_sum(heading, 90), int(right_position.distance_to_point(left_position))
|
||||
center_position, heading = cls.frontline_position(front_line, 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
|
||||
|
||||
@classmethod
|
||||
def _extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
|
||||
def frontline_cas_conflict(
|
||||
cls,
|
||||
attacker_name: str,
|
||||
defender_name: str,
|
||||
attacker: Country,
|
||||
defender: Country,
|
||||
front_line: FrontLine,
|
||||
theater: ConflictTheater,
|
||||
):
|
||||
assert cls.has_frontline_between(front_line.blue_cp, front_line.red_cp)
|
||||
position, heading, distance = cls.frontline_vector(front_line, theater)
|
||||
conflict = cls(
|
||||
position=position,
|
||||
heading=heading,
|
||||
theater=theater,
|
||||
front_line=front_line,
|
||||
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"""
|
||||
extended = initial.point_from_heading(heading, max_distance)
|
||||
if theater.landmap is None:
|
||||
# TODO: Why is this possible?
|
||||
return extended
|
||||
|
||||
p0 = ShapelyPoint(initial.x, initial.y)
|
||||
p1 = ShapelyPoint(extended.x, extended.y)
|
||||
line = LineString([p0, p1])
|
||||
|
||||
intersection = line.intersection(theater.landmap.inclusion_zone_only.boundary)
|
||||
if intersection.is_empty:
|
||||
# Max extent does not intersect with the boundary of the inclusion
|
||||
# zone, so the full front line is usable. This does assume that the
|
||||
# front line was centered on a valid location.
|
||||
return extended
|
||||
|
||||
# Otherwise extend the front line only up to the intersection.
|
||||
return initial.point_from_heading(heading, p0.distance(intersection))
|
||||
|
||||
@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
|
||||
for offset in range(0, int(max_distance), 500):
|
||||
new_pos = initial.point_from_heading(heading, offset)
|
||||
if theater.is_on_land(new_pos):
|
||||
pos = new_pos
|
||||
else:
|
||||
return pos
|
||||
return pos
|
||||
|
||||
"""
|
||||
probe_end_point = initial.point_from_heading(heading, max_distance)
|
||||
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y)])
|
||||
|
||||
intersection = probe.intersection(theater.land_poly)
|
||||
if intersection is geometry.LineString:
|
||||
return Point(*intersection.xy[1])
|
||||
elif intersection is geometry.MultiLineString:
|
||||
return Point(*intersection.geoms[0].xy[1])
|
||||
|
||||
return None
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
|
||||
pos = initial
|
||||
for _ in range(0, int(max_distance), 500):
|
||||
if theater.is_on_land(pos):
|
||||
return pos
|
||||
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 = pos.point_from_heading(heading, 500)
|
||||
"""
|
||||
probe_end_point = initial.point_from_heading(heading, max_distance)
|
||||
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ])
|
||||
|
||||
intersection = probe.intersection(theater.land_poly)
|
||||
if isinstance(intersection, geometry.LineString):
|
||||
return Point(*intersection.xy[1])
|
||||
elif isinstance(intersection, geometry.MultiLineString):
|
||||
return Point(*intersection.geoms[0].xy[1])
|
||||
"""
|
||||
|
||||
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 initial
|
||||
|
||||
@classmethod
|
||||
def capture_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
position = to_cp.position
|
||||
attack_raw_heading = to_cp.position.heading_between_point(from_cp.position)
|
||||
attack_heading = to_cp.find_radial(attack_raw_heading)
|
||||
defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading)
|
||||
|
||||
distance = GROUND_DISTANCE
|
||||
attackers_location = position.point_from_heading(attack_heading, distance)
|
||||
attackers_location = Conflict._find_ground_position(attackers_location, distance * 2, attack_heading, theater)
|
||||
|
||||
defenders_location = position.point_from_heading(defense_heading, 0)
|
||||
defenders_location = Conflict._find_ground_position(defenders_location, distance * 2, defense_heading, theater)
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=attackers_location,
|
||||
ground_defenders_location=defenders_location,
|
||||
air_attackers_location=position.point_from_heading(attack_raw_heading, CAPTURE_AIR_ATTACKERS_DISTANCE),
|
||||
air_defenders_location=position.point_from_heading(_opposite_heading(attack_raw_heading), CAPTURE_AIR_DEFENDERS_DISTANCE)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def strike_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
position = to_cp.position
|
||||
attack_raw_heading = to_cp.position.heading_between_point(from_cp.position)
|
||||
attack_heading = to_cp.find_radial(attack_raw_heading)
|
||||
defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading)
|
||||
|
||||
distance = to_cp.size * GROUND_DISTANCE_FACTOR
|
||||
attackers_location = position.point_from_heading(attack_heading, distance)
|
||||
attackers_location = Conflict._find_ground_position(
|
||||
attackers_location, int(distance * 2),
|
||||
_heading_sum(attack_heading, 180), theater)
|
||||
|
||||
defenders_location = position.point_from_heading(defense_heading, distance)
|
||||
defenders_location = Conflict._find_ground_position(
|
||||
defenders_location, int(distance * 2),
|
||||
_heading_sum(defense_heading, 180), theater)
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=attackers_location,
|
||||
ground_defenders_location=defenders_location,
|
||||
air_attackers_location=position.point_from_heading(attack_raw_heading, STRIKE_AIR_ATTACKERS_DISTANCE),
|
||||
air_defenders_location=position.point_from_heading(_opposite_heading(attack_raw_heading), STRIKE_AIR_DEFENDERS_DISTANCE)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def intercept_position(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> Point:
|
||||
raw_distance = from_cp.position.distance_to_point(to_cp.position) * 1.5
|
||||
distance = max(min(raw_distance, INTERCEPT_MAX_DISTANCE), INTERCEPT_MIN_DISTANCE)
|
||||
heading = _heading_sum(from_cp.position.heading_between_point(to_cp.position), random.choice([-1, 1]) * random.randint(60, 100))
|
||||
return from_cp.position.point_from_heading(heading, distance)
|
||||
|
||||
@classmethod
|
||||
def intercept_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, position: Point, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
heading = from_cp.position.heading_between_point(position)
|
||||
return cls(
|
||||
position=position.point_from_heading(position.heading_between_point(to_cp.position), INTERCEPT_CONFLICT_DISTANCE),
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=None,
|
||||
ground_defenders_location=None,
|
||||
air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, INTERCEPT_ATTACKERS_DISTANCE),
|
||||
air_defenders_location=position
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def ground_attack_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
heading = random.choice(to_cp.radials)
|
||||
initial_location = to_cp.position.random_point_within(*GROUND_ATTACK_DISTANCE)
|
||||
position = Conflict._find_ground_position(initial_location, GROUND_INTERCEPT_SPREAD, _heading_sum(heading, 180), theater)
|
||||
if not position:
|
||||
heading = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
|
||||
position = to_cp.position.point_from_heading(heading, to_cp.size * GROUND_DISTANCE_FACTOR)
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=position,
|
||||
ground_defenders_location=None,
|
||||
air_attackers_location=None,
|
||||
air_defenders_location=position.point_from_heading(heading, AIR_DISTANCE),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def convoy_strike_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
frontline_position, frontline_heading, frontline_length = Conflict.frontline_vector(from_cp, to_cp, theater)
|
||||
if not frontline_position:
|
||||
assert False
|
||||
|
||||
heading = frontline_heading
|
||||
starting_position = Conflict._find_ground_position(frontline_position.point_from_heading(heading, 7000),
|
||||
GROUND_INTERCEPT_SPREAD,
|
||||
_opposite_heading(heading), theater)
|
||||
if not starting_position:
|
||||
starting_position = frontline_position
|
||||
destination_position = frontline_position
|
||||
else:
|
||||
destination_position = frontline_position
|
||||
|
||||
return cls(
|
||||
position=destination_position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=None,
|
||||
ground_defenders_location=starting_position,
|
||||
air_attackers_location=starting_position.point_from_heading(_opposite_heading(heading), AIR_DISTANCE),
|
||||
air_defenders_location=starting_position.point_from_heading(heading, AIR_DISTANCE),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def frontline_cas_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
assert cls.has_frontline_between(from_cp, to_cp)
|
||||
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
heading=heading,
|
||||
distance=distance,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=None,
|
||||
ground_defenders_location=None,
|
||||
air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, AIR_DISTANCE),
|
||||
air_defenders_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + _opposite_heading(heading), AIR_DISTANCE),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def frontline_cap_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
assert cls.has_frontline_between(from_cp, to_cp)
|
||||
|
||||
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
|
||||
attack_position = position.point_from_heading(heading, random.randint(0, int(distance)))
|
||||
attackers_position = attack_position.point_from_heading(heading - 90, AIR_DISTANCE)
|
||||
defenders_position = attack_position.point_from_heading(heading + 90, random.randint(*CAP_CAS_DISTANCE))
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
heading=heading,
|
||||
distance=distance,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
air_attackers_location=attackers_position,
|
||||
air_defenders_location=defenders_position,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def ground_base_attack(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
position = to_cp.position
|
||||
attack_heading = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
|
||||
defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading)
|
||||
|
||||
distance = to_cp.size * GROUND_DISTANCE_FACTOR
|
||||
defenders_location = position.point_from_heading(defense_heading, distance)
|
||||
defenders_location = Conflict._find_ground_position(
|
||||
defenders_location, int(distance * 2),
|
||||
_heading_sum(defense_heading, 180), theater)
|
||||
|
||||
return cls(
|
||||
position=position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=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_position(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
radial = random.choice(to_cp.sea_radials)
|
||||
|
||||
initial_distance = min(int(from_cp.position.distance_to_point(to_cp.position) * NAVAL_INTERCEPT_DISTANCE_FACTOR), NAVAL_INTERCEPT_DISTANCE_MAX)
|
||||
initial_position = to_cp.position.point_from_heading(radial, initial_distance)
|
||||
for offset in range(0, initial_distance, NAVAL_INTERCEPT_STEP):
|
||||
position = initial_position.point_from_heading(_opposite_heading(radial), offset)
|
||||
|
||||
if not theater.is_on_land(position):
|
||||
break
|
||||
return position
|
||||
|
||||
@classmethod
|
||||
def naval_intercept_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, position: Point, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
attacker_heading = from_cp.position.heading_between_point(to_cp.position)
|
||||
return cls(
|
||||
position=position,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=None,
|
||||
ground_defenders_location=position,
|
||||
air_attackers_location=position.point_from_heading(attacker_heading, AIR_DISTANCE),
|
||||
air_defenders_location=position.point_from_heading(_opposite_heading(attacker_heading), AIR_DISTANCE)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def transport_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
frontline_position, heading = cls.frontline_position(theater, from_cp, to_cp)
|
||||
initial_dest = frontline_position.point_from_heading(heading, TRANSPORT_FRONTLINE_DIST)
|
||||
dest = cls._find_ground_position(initial_dest, from_cp.position.distance_to_point(to_cp.position) / 3, heading, theater)
|
||||
if not dest:
|
||||
radial = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
|
||||
dest = to_cp.position.point_from_heading(radial, to_cp.size * GROUND_DISTANCE_FACTOR)
|
||||
|
||||
return cls(
|
||||
position=dest,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
ground_attackers_location=from_cp.position,
|
||||
ground_defenders_location=frontline_position,
|
||||
air_attackers_location=from_cp.position.point_from_heading(0, 100),
|
||||
air_defenders_location=frontline_position
|
||||
)
|
||||
return None
|
||||
|
||||
92
gen/convoygen.py
Normal file
92
gen/convoygen.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from typing import Dict, TYPE_CHECKING, Type
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.mapping import Point
|
||||
from dcs.point import PointAction
|
||||
from dcs.unit import Vehicle
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game.transfers import Convoy
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import kph
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
class ConvoyGenerator:
|
||||
def __init__(self, mission: Mission, game: Game, unit_map: UnitMap) -> None:
|
||||
self.mission = mission
|
||||
self.game = game
|
||||
self.unit_map = unit_map
|
||||
self.count = itertools.count()
|
||||
|
||||
def generate(self) -> None:
|
||||
# Reset the count to make generation deterministic.
|
||||
for convoy in self.game.transfers.convoys:
|
||||
self.generate_convoy(convoy)
|
||||
|
||||
def generate_convoy(self, convoy: Convoy) -> VehicleGroup:
|
||||
group = self._create_mixed_unit_group(
|
||||
convoy.name,
|
||||
convoy.route_start,
|
||||
convoy.units,
|
||||
convoy.player_owned,
|
||||
)
|
||||
group.add_waypoint(
|
||||
convoy.route_end,
|
||||
speed=kph(40).kph,
|
||||
move_formation=PointAction.OnRoad,
|
||||
)
|
||||
self.make_drivable(group)
|
||||
self.unit_map.add_convoy_units(group, convoy)
|
||||
return group
|
||||
|
||||
def _create_mixed_unit_group(
|
||||
self,
|
||||
name: str,
|
||||
position: Point,
|
||||
units: Dict[Type[VehicleType], int],
|
||||
for_player: bool,
|
||||
) -> VehicleGroup:
|
||||
country = self.mission.country(
|
||||
self.game.player_country if for_player else self.game.enemy_country
|
||||
)
|
||||
|
||||
unit_types = list(units.items())
|
||||
main_unit_type, main_unit_count = unit_types[0]
|
||||
|
||||
group = self.mission.vehicle_group(
|
||||
country,
|
||||
name,
|
||||
main_unit_type,
|
||||
position=position,
|
||||
group_size=main_unit_count,
|
||||
move_formation=PointAction.OnRoad,
|
||||
)
|
||||
|
||||
unit_name_counter = itertools.count(main_unit_count + 1)
|
||||
# pydcs spreads units out by 20 in the Y axis by default. Pick up where it left
|
||||
# off.
|
||||
y = itertools.count(position.y + main_unit_count * 20, 20)
|
||||
for unit_type, count in unit_types[1:]:
|
||||
for i in range(count):
|
||||
v = self.mission.vehicle(
|
||||
f"{name} Unit #{next(unit_name_counter)}", unit_type
|
||||
)
|
||||
v.position.x = position.x
|
||||
v.position.y = next(y)
|
||||
v.heading = 0
|
||||
group.add_unit(v)
|
||||
|
||||
return group
|
||||
|
||||
@staticmethod
|
||||
def make_drivable(group: VehicleGroup) -> None:
|
||||
for v in group.units:
|
||||
if isinstance(v, Vehicle):
|
||||
v.player_can_drive = True
|
||||
@@ -3,15 +3,20 @@ import random
|
||||
from dcs.vehicles import Armor
|
||||
|
||||
from game import db
|
||||
from gen.defenses.armored_group_generator import ArmoredGroupGenerator, FixedSizeArmorGroupGenerator
|
||||
from gen.defenses.armored_group_generator import (
|
||||
ArmoredGroupGenerator,
|
||||
FixedSizeArmorGroupGenerator,
|
||||
)
|
||||
|
||||
|
||||
def generate_armor_group(faction:str, game, ground_object):
|
||||
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()]
|
||||
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)
|
||||
@@ -36,4 +41,3 @@ def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size:
|
||||
generator = FixedSizeArmorGroupGenerator(game, ground_object, unit_type, size)
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ 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
|
||||
@@ -20,13 +19,16 @@ class ArmoredGroupGenerator(GroupGenerator):
|
||||
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)
|
||||
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
|
||||
@@ -38,7 +40,10 @@ class FixedSizeArmorGroupGenerator(GroupGenerator):
|
||||
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)
|
||||
|
||||
self.add_unit(
|
||||
self.unit_type,
|
||||
"Armor#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
@@ -17,11 +17,12 @@ class EnvironmentGenerator:
|
||||
self.mission.weather.clouds_thickness = clouds.thickness
|
||||
self.mission.weather.clouds_density = clouds.density
|
||||
self.mission.weather.clouds_iprecptns = clouds.precipitation
|
||||
self.mission.weather.clouds_preset = clouds.preset
|
||||
|
||||
def set_fog(self, fog: Optional[Fog]) -> None:
|
||||
if fog is None:
|
||||
return
|
||||
self.mission.weather.fog_visibility = fog.visibility
|
||||
self.mission.weather.fog_visibility = fog.visibility.meters
|
||||
self.mission.weather.fog_thickness = fog.thickness
|
||||
|
||||
def set_wind(self, wind: WindConditions) -> None:
|
||||
|
||||
@@ -2,25 +2,122 @@ import random
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
from dcs.ships import DDG_Arleigh_Burke_IIa, CG_Ticonderoga
|
||||
|
||||
|
||||
class CarrierGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
# Add carrier
|
||||
if len(self.faction.aircraft_carrier) > 0:
|
||||
# Carrier Strike Group 8
|
||||
if self.faction.carrier_names[0] == "Carrier Strike Group 8":
|
||||
carrier_type = random.choice(self.faction.aircraft_carrier)
|
||||
self.add_unit(carrier_type, "Carrier", self.position.x, self.position.y, self.heading)
|
||||
|
||||
self.add_unit(
|
||||
carrier_type,
|
||||
"CVN-75 Harry S. Truman",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Add Arleigh Burke escort
|
||||
self.add_unit(
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
"USS Ramage",
|
||||
self.position.x + 6482,
|
||||
self.position.y + 6667,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
self.add_unit(
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
"USS Mitscher",
|
||||
self.position.x - 7963,
|
||||
self.position.y + 7037,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
self.add_unit(
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
"USS Forrest Sherman",
|
||||
self.position.x - 7408,
|
||||
self.position.y - 7408,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
self.add_unit(
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
"USS Lassen",
|
||||
self.position.x + 8704,
|
||||
self.position.y - 6296,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Add Ticonderoga escort
|
||||
if self.heading >= 180:
|
||||
self.add_unit(
|
||||
CG_Ticonderoga,
|
||||
"USS Hué City",
|
||||
self.position.x + 2222,
|
||||
self.position.y - 3333,
|
||||
self.heading,
|
||||
)
|
||||
else:
|
||||
self.add_unit(
|
||||
CG_Ticonderoga,
|
||||
"USS Hué City",
|
||||
self.position.x - 3333,
|
||||
self.position.y + 2222,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
##################################################################################################
|
||||
# Add carrier for normal generation
|
||||
else:
|
||||
return
|
||||
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)
|
||||
# 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.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
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
|
||||
@@ -8,46 +8,66 @@ from dcs.ships import (
|
||||
Type_052C_Destroyer,
|
||||
Type_052B_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
)
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from gen.fleet.dd_group import DDGroupGenerator
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from theater.theatergroundobject import TheaterGroundObject
|
||||
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 not any([include_frigate, include_dd]):
|
||||
include_frigate = True
|
||||
|
||||
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)
|
||||
self.add_unit(
|
||||
Type_054A_Frigate,
|
||||
"FF1",
|
||||
self.position.x + 1200,
|
||||
self.position.y + 900,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Type_054A_Frigate,
|
||||
"FF2",
|
||||
self.position.x + 1200,
|
||||
self.position.y - 900,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
if include_dd:
|
||||
dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer])
|
||||
self.add_unit(dd_type, "FF1", self.position.x + 2400, self.position.y + 900, self.heading)
|
||||
self.add_unit(dd_type, "FF2", self.position.x + 2400, self.position.y - 900, self.heading)
|
||||
|
||||
if include_cc:
|
||||
cc_type = random.choice([CGN_1144_2_Pyotr_Velikiy])
|
||||
self.add_unit(cc_type, "CC1", self.position.x, self.position.y, self.heading)
|
||||
self.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,
|
||||
)
|
||||
|
||||
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)
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
super(Type54GroupGenerator, self).__init__(
|
||||
game, ground_object, faction, Type_054A_Frigate
|
||||
)
|
||||
|
||||
@@ -2,33 +2,58 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from theater.theatergroundobject import TheaterGroundObject
|
||||
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
|
||||
from dcs.ships import FFG_Oliver_Hazzard_Perry, DDG_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):
|
||||
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.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)
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
super(OliverHazardPerryGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, FFG_Oliver_Hazzard_Perry
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
super(ArleighBurkeGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, DDG_Arleigh_Burke_IIa
|
||||
)
|
||||
|
||||
12
gen/fleet/lacombattanteII.py
Normal file
12
gen/fleet/lacombattanteII.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from dcs.ships import FAC_La_Combattante_IIa
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import TheaterGroundObject
|
||||
from gen.fleet.dd_group import DDGroupGenerator
|
||||
|
||||
|
||||
class LaCombattanteIIGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(LaCombattanteIIGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, FAC_La_Combattante_IIa
|
||||
)
|
||||
@@ -4,18 +4,31 @@ 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)
|
||||
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.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
|
||||
|
||||
@@ -3,20 +3,19 @@ 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
|
||||
Corvette_1124_4_Grisha,
|
||||
Corvette_1241_1_Molniya,
|
||||
Frigate_11540_Neustrashimy,
|
||||
Frigate_1135M_Rezky,
|
||||
Cruiser_1164_Moskva,
|
||||
SSK_877V_Kilo,
|
||||
SSK_641B_Tango,
|
||||
)
|
||||
|
||||
from gen.fleet.dd_group import DDGroupGenerator
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from game.factions.faction import Faction
|
||||
from theater.theatergroundobject import TheaterGroundObject
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -24,7 +23,6 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
include_frigate = random.choice([True, True, False])
|
||||
@@ -35,38 +33,90 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||
else:
|
||||
include_cc = False
|
||||
|
||||
if not any([include_frigate, include_dd, include_cc]):
|
||||
include_frigate = True
|
||||
|
||||
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)
|
||||
frigate_type = random.choice(
|
||||
[Corvette_1124_4_Grisha, Corvette_1241_1_Molniya]
|
||||
)
|
||||
self.add_unit(
|
||||
frigate_type,
|
||||
"FF1",
|
||||
self.position.x + 1200,
|
||||
self.position.y + 900,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
frigate_type,
|
||||
"FF2",
|
||||
self.position.x + 1200,
|
||||
self.position.y - 900,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
if include_dd:
|
||||
dd_type = random.choice([FFG_11540_Neustrashimy, FF_1135M_Rezky])
|
||||
self.add_unit(dd_type, "FF1", self.position.x + 2400, self.position.y + 900, self.heading)
|
||||
self.add_unit(dd_type, "FF2", self.position.x + 2400, self.position.y - 900, self.heading)
|
||||
dd_type = random.choice([Frigate_11540_Neustrashimy, Frigate_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)
|
||||
# Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster.
|
||||
# See https://github.com/dcs-liberation/dcs_liberation/issues/567
|
||||
self.add_unit(
|
||||
Cruiser_1164_Moskva,
|
||||
"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)
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
super(GrishaGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, Corvette_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)
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
super(MolniyaGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, Corvette_1241_1_Molniya
|
||||
)
|
||||
|
||||
|
||||
class KiloSubGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_877)
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
super(KiloSubGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, SSK_877V_Kilo
|
||||
)
|
||||
|
||||
|
||||
class TangoSubGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_641B)
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
super(TangoSubGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, SSK_641B_Tango
|
||||
)
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import random
|
||||
|
||||
from dcs.ships import Schnellboot_type_S130
|
||||
from dcs.ships import Boat_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.add_unit(
|
||||
Boat_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
|
||||
|
||||
@@ -4,10 +4,19 @@ 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.dd_group import (
|
||||
ArleighBurkeGroupGenerator,
|
||||
OliverHazardPerryGroupGenerator,
|
||||
)
|
||||
from gen.fleet.lacombattanteII import LaCombattanteIIGroupGenerator
|
||||
from gen.fleet.lha_group import LHAGroupGenerator
|
||||
from gen.fleet.ru_dd_group import RussianNavyGroupGenerator, GrishaGroupGenerator, MolniyaGroupGenerator, \
|
||||
KiloSubGroupGenerator, TangoSubGroupGenerator
|
||||
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
|
||||
@@ -25,7 +34,8 @@ SHIP_MAP = {
|
||||
"MolniyaGroupGenerator": MolniyaGroupGenerator,
|
||||
"KiloSubGroupGenerator": KiloSubGroupGenerator,
|
||||
"TangoSubGroupGenerator": TangoSubGroupGenerator,
|
||||
"Type54GroupGenerator": Type54GroupGenerator
|
||||
"Type54GroupGenerator": Type54GroupGenerator,
|
||||
"LaCombattanteIIGroupGenerator": LaCombattanteIIGroupGenerator,
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +49,15 @@ def generate_ship_group(game, ground_object, faction_name: str):
|
||||
gen = random.choice(faction.navy_generators)
|
||||
if gen in SHIP_MAP.keys():
|
||||
generator = SHIP_MAP[gen](game, ground_object, faction)
|
||||
print(generator.position)
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
else:
|
||||
logging.info("Unable to generate ship group, generator : " + str(gen) + "does not exists")
|
||||
logging.info(
|
||||
"Unable to generate ship group, generator : "
|
||||
+ str(gen)
|
||||
+ "does not exists"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import random
|
||||
|
||||
from dcs.ships import Uboat_VIIC_U_flak
|
||||
from dcs.ships import U_boat_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.add_unit(
|
||||
U_boat_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
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
|
||||
@@ -6,13 +6,24 @@ 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)
|
||||
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.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
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user