mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
1275 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e6e1469d7 | ||
|
|
6cc967742a | ||
|
|
17f2bcc9c9 | ||
|
|
9d499a1430 | ||
|
|
3b55dfad40 | ||
|
|
9d3c7a86b6 | ||
|
|
7f68846023 | ||
|
|
c1534cba9e | ||
|
|
eea31168c1 | ||
|
|
f2de1fdac6 | ||
|
|
8dd29d2319 | ||
|
|
b402dad801 | ||
|
|
7199fead00 | ||
|
|
4ff0f29fe0 | ||
|
|
1b8992eb04 | ||
|
|
ccbcf4f69a | ||
|
|
0747007f58 | ||
|
|
723588666f | ||
|
|
94861ca477 | ||
|
|
e8992c5bed | ||
|
|
e841358f74 | ||
|
|
3c135720a0 | ||
|
|
d7db290892 | ||
|
|
b7626c10da | ||
|
|
d79e8f46f3 | ||
|
|
278b9730cd | ||
|
|
d187c571ea | ||
|
|
b3705531d4 | ||
|
|
666b389821 | ||
|
|
ddc076b141 | ||
|
|
eee1791a79 | ||
|
|
fb5a6d3243 | ||
|
|
113c00ac05 | ||
|
|
85ca85ac6d | ||
|
|
da917a7dde | ||
|
|
b03d1599e1 | ||
|
|
2b3c56ad38 | ||
|
|
8dc35bec5a | ||
|
|
3f4f27612b | ||
|
|
17f9487fe0 | ||
|
|
e15b10ae7e | ||
|
|
17d56beeaa | ||
|
|
53c7912592 | ||
|
|
1f318aff3c | ||
|
|
2bb1c0b3f2 | ||
|
|
b057f027d5 | ||
|
|
cc079ad44e | ||
|
|
974c0069e6 | ||
|
|
9028109fe3 | ||
|
|
db27f3b0d9 | ||
|
|
cb542b6af4 | ||
|
|
fcea37c340 | ||
|
|
cf3d13f9d3 | ||
|
|
6789beb4b5 | ||
|
|
8f1ec4a519 | ||
|
|
b8bc9d87ec | ||
|
|
52aff8bc30 | ||
|
|
5c81ac06ac | ||
|
|
8364148305 | ||
|
|
2bcff5a5c2 | ||
|
|
c227923bdf | ||
|
|
4569b1b45a | ||
|
|
3a193d1dd4 | ||
|
|
9334cba564 | ||
|
|
4dc1daa100 | ||
|
|
0d99fc3d36 | ||
|
|
eee78288c9 | ||
|
|
c2f112e3a6 | ||
|
|
ef3f7125b3 | ||
|
|
4558088412 | ||
|
|
d2cc3f673e | ||
|
|
dc85644d71 | ||
|
|
0b5bdf8151 | ||
|
|
b27238a69a | ||
|
|
bb2bf78e8a | ||
|
|
7e17533cc6 | ||
|
|
7808da118a | ||
|
|
4259cf8764 | ||
|
|
994c55945e | ||
|
|
f20c145ece | ||
|
|
5b31026e1c | ||
|
|
39fe5951f7 | ||
|
|
9d767c3dd8 | ||
|
|
2a3f9bf81c | ||
|
|
3fd4359cb1 | ||
|
|
ca1be580df | ||
|
|
28820f2e64 | ||
|
|
6c3987ec86 | ||
|
|
089eb9e86b | ||
|
|
0793e9afc5 | ||
|
|
1e2522375b | ||
|
|
e09f53da8f | ||
|
|
29b4b62a44 | ||
|
|
b1a63db1fc | ||
|
|
9940dc8451 | ||
|
|
703c68eb66 | ||
|
|
3338df9836 | ||
|
|
dc4794b246 | ||
|
|
b130c9882a | ||
|
|
5f8b838652 | ||
|
|
4efd1b5d3e | ||
|
|
ad6ed21b6b | ||
|
|
2ffaa71bb5 | ||
|
|
1763f59320 | ||
|
|
08d32ffc77 | ||
|
|
7e3cebb96d | ||
|
|
930fb404af | ||
|
|
6cd711a1e2 | ||
|
|
1bcc332885 | ||
|
|
9bb986cff9 | ||
|
|
1247942bf1 | ||
|
|
95d3ff4cbe | ||
|
|
0a874a28ef | ||
|
|
2dee702060 | ||
|
|
4ea66477fe | ||
|
|
d3be732566 | ||
|
|
933517055e | ||
|
|
040a3d9b36 | ||
|
|
2c859bf280 | ||
|
|
fe227e02b8 | ||
|
|
c68e583c20 | ||
|
|
6620d56859 | ||
|
|
1ec72d3e94 | ||
|
|
5c3bb75786 | ||
|
|
a90cb0dad9 | ||
|
|
8854a491ab | ||
|
|
74e8073328 | ||
|
|
a6d62a7596 | ||
|
|
980a224d02 | ||
|
|
0c6b83fc35 | ||
|
|
6d2310f59d | ||
|
|
05107fab1c | ||
|
|
285bed65c6 | ||
|
|
b523c23e7a | ||
|
|
4c9a028a4e | ||
|
|
cea970f065 | ||
|
|
d8511fab1d | ||
|
|
b2d10e92e9 | ||
|
|
0582d5e2b6 | ||
|
|
da2b56b5b1 | ||
|
|
46c15f37c5 | ||
|
|
4ddc02d7fe | ||
|
|
4c3ac0af91 | ||
|
|
edd0b90576 | ||
|
|
11dca41945 | ||
|
|
75c4724200 | ||
|
|
09704b6f37 | ||
|
|
8a0824880e | ||
|
|
d84abf021e | ||
|
|
e7223da19f | ||
|
|
499d143199 | ||
|
|
fefeb3c006 | ||
|
|
6aeee933d2 | ||
|
|
34c0698c48 | ||
|
|
1cc1a00820 | ||
|
|
62f6b57948 | ||
|
|
5387acf533 | ||
|
|
077b3ef04d | ||
|
|
9c654254d3 | ||
|
|
4bb8bbbad8 | ||
|
|
39adafb1be | ||
|
|
e19bfcdd04 | ||
|
|
6fde92f5ac | ||
|
|
7170a7b302 | ||
|
|
24884e4a77 | ||
|
|
384be8ceae | ||
|
|
ee9a5e8482 | ||
|
|
34453fa3be | ||
|
|
f727712bfa | ||
|
|
3bb974b9e0 | ||
|
|
021445216e | ||
|
|
c13bf3ccd1 | ||
|
|
8d53f42421 | ||
|
|
ace42019fb | ||
|
|
54aa161da0 | ||
|
|
25c289deaa | ||
|
|
3c802e7d55 | ||
|
|
ba3cf4d2bd | ||
|
|
4aa905716b | ||
|
|
0fc1e8ec10 | ||
|
|
0875d35129 | ||
|
|
8c62a081fe | ||
|
|
f811ae6c61 | ||
|
|
4a3ef42e67 | ||
|
|
88abaef7f9 | ||
|
|
21fe746f2f | ||
|
|
c3c6915fa0 | ||
|
|
b2705c1a13 | ||
|
|
00ca7d0b4d | ||
|
|
64c426653c | ||
|
|
a8960c9bbe | ||
|
|
75e3b4cc84 | ||
|
|
72282845e8 | ||
|
|
78f5235eca | ||
|
|
e64aff4e91 | ||
|
|
78cd17e279 | ||
|
|
c51c8aae5c | ||
|
|
e192e54c90 | ||
|
|
40aa7734e1 | ||
|
|
39b0599b7b | ||
|
|
45b40e4aa3 | ||
|
|
9887a8ff83 | ||
|
|
0594e1148e | ||
|
|
9eacd1563f | ||
|
|
a53a648a63 | ||
|
|
a9dacf4a29 | ||
|
|
8d3556aa4b | ||
|
|
66f82b6ff9 | ||
|
|
a59c01bcfe | ||
|
|
fb72962f74 | ||
|
|
ed1dacfe7c | ||
|
|
0e68884493 | ||
|
|
f8d885fc9a | ||
|
|
794de0fcbb | ||
|
|
366190ee99 | ||
|
|
9d71b2e727 | ||
|
|
5b8f626651 | ||
|
|
42d56a324f | ||
|
|
7d1f1ea2f7 | ||
|
|
461f4b82a9 | ||
|
|
15653d0628 | ||
|
|
dffc631b87 | ||
|
|
30cab8e3a7 | ||
|
|
e0e2162c6d | ||
|
|
f1582fcc10 | ||
|
|
eb6206ea57 | ||
|
|
3ad51cafa8 | ||
|
|
17efb48b2e | ||
|
|
b8c14d69c3 | ||
|
|
7e85825d2b | ||
|
|
725b5083c7 | ||
|
|
798591b980 | ||
|
|
87dd6b19bf | ||
|
|
3188994261 | ||
|
|
4a52af298c | ||
|
|
fe886a754e | ||
|
|
e4c9d8799e | ||
|
|
0220fa4ff6 | ||
|
|
bc938db7f9 | ||
|
|
0a9dc49e7f | ||
|
|
07cdfc16d0 | ||
|
|
622a171ac4 | ||
|
|
fd85efbf55 | ||
|
|
ae2a818d8c | ||
|
|
6966c16dd2 | ||
|
|
27b5f24a0f | ||
|
|
ea15421308 | ||
|
|
ef35ad90b8 | ||
|
|
914691eaa7 | ||
|
|
37bb83dfa6 | ||
|
|
e7336d8608 | ||
|
|
d8881e2734 | ||
|
|
d77a174ac1 | ||
|
|
45869c428e | ||
|
|
40832bd3a1 | ||
|
|
126a8e8efb | ||
|
|
6348317893 | ||
|
|
a516cd2f80 | ||
|
|
1796c21f48 | ||
|
|
363d4af639 | ||
|
|
f1c881378c | ||
|
|
e1aa3e9d0e | ||
|
|
d316e13fa6 | ||
|
|
d1d1acf6e0 | ||
|
|
1ea98a6ed1 | ||
|
|
3d4415d5d2 | ||
|
|
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 | ||
|
|
97ea67d01d | ||
|
|
d6376c3a91 | ||
|
|
793b356c01 | ||
|
|
25efdd3d4f | ||
|
|
0b2483ea15 | ||
|
|
4d26ec0789 | ||
|
|
7d907aac0f | ||
|
|
d6981550a8 | ||
|
|
8b0636367b | ||
|
|
a6c9d0f9bc | ||
|
|
61ebe9780e | ||
|
|
e887082501 | ||
|
|
80778aa267 | ||
|
|
445cb4f146 | ||
|
|
95db2aa14f | ||
|
|
4b0fc637eb | ||
|
|
48d6b4cfa1 | ||
|
|
fc11182bbe | ||
|
|
1848338ef7 | ||
|
|
08ceb57c31 | ||
|
|
affb332eb9 | ||
|
|
d5276c9d4a | ||
|
|
07e5c568c4 | ||
|
|
31fdd24c3e | ||
|
|
bfa0e4ba49 | ||
|
|
c89ff2c3d6 | ||
|
|
4ec88d524a | ||
|
|
48c218b430 | ||
|
|
d316836e90 | ||
|
|
8443f61f0a | ||
|
|
67806f3d76 | ||
|
|
7744f84e85 |
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"
|
||||
4
.github/workflows/build.yml
vendored
4
.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: |
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.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: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,11 +5,14 @@ resources/payloads/*.lua
|
||||
venv
|
||||
logs.txt
|
||||
.DS_Store
|
||||
.vscode/settings.json
|
||||
dist/**
|
||||
a.py
|
||||
resources/tools/a.miz
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
.env
|
||||
env/
|
||||
|
||||
/kneeboards
|
||||
/liberation_preferences.json
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,4 +0,0 @@
|
||||
[submodule "pydcs"]
|
||||
path = pydcs
|
||||
url = https://github.com/pydcs/dcs
|
||||
branch = master
|
||||
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.
|
||||
33
README.md
33
README.md
@@ -1,29 +1,46 @@
|
||||

|
||||
|
||||
[](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.
|
||||
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
|
||||
|
||||

|
||||

|
||||
|
||||
## Downloads
|
||||
|
||||
Latest release is available here : https://github.com/Khopa/dcs_liberation/releases
|
||||
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
|
||||
|
||||
|
||||
337
changelog.md
337
changelog.md
@@ -1,16 +1,346 @@
|
||||
# 4.0.0
|
||||
|
||||
Saves from 3.x are not compatible with 4.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.7.2.7910.1 and newer, including Cyprus, F-16 JDAMs, and the Hind.
|
||||
* **[Campaign]** Squadrons now (optionally, off by default) have a maximum size and killed pilots replenish at a limited rate.
|
||||
* **[Campaign]** Added an option to disable levelling up of AI pilots.
|
||||
* **[Campaign]** Added Russian Intervention 2015 campaign on Syria, for a small and somewhat realistic Russian COIN scenario.
|
||||
* **[Campaign]** Added Operation Atilla campaign on Syria, for a reasonably large invasion of Cyprus scenario.
|
||||
* **[Campaign AI]** AI will plan Tanker flights.
|
||||
* **[Campaign AI]** Removed max distance for AEW&C auto planning.
|
||||
* **[Economy]** Adjusted prices for aircraft to balance out some price inconsistencies.
|
||||
* **[Factions]** Added more tankers to factions.
|
||||
* **[Flight Planner]** Added ability to plan Tankers.
|
||||
* **[Modding]** Campaign format version is now 7.0 to account for DCS map changes that made scenery strike targets incompatible with existing campaigns.
|
||||
* **[Mods]** Added support for the Gripen mod.
|
||||
* **[Mods]** Removes MB-339PAN support, as the mod is now deprecated and no longer works with DCS 2.7+.
|
||||
* **[Mission Generation]** Added support for "Neutral Dot" label options.
|
||||
* **[New Game Wizard]** Mods are now selected via checkboxes in the new game wizard, not as separate factions.
|
||||
* **[UI]** Ctrl click and shift click now buy or sell 5 or 10 units respectively.
|
||||
* **[UI]** Multiple waypoints can now be deleted simultaneously if multiple waypoints are selected.
|
||||
* **[UI]** Carriers and LHAs now match the colour of airfields, and their destination icons are translucent.
|
||||
* **[UI]** Updated intel box text for first turn.
|
||||
* **[UI]** Base Capture Cheat is now usable at all bases and can also be used to transfer player-owned bases to OPFOR.
|
||||
* **[UI]** Pass Turn button is relabled as "Begin Campaign" on Turn 0.
|
||||
* **[UI]** Added a ruler to the map.
|
||||
* **[UI]** Liberation now saves games to `<DCS user directory>/Liberation/Saves` by default to declutter the main directory.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign AI]** Fix procurement for factions that lack some unit types.
|
||||
* **[Campaign AI]** Fix auto purchase of aircraft for factions that have no transport aircraft.
|
||||
* **[Campaign AI]** Fix refunding of pending aircraft purchases when a side has no factory available.
|
||||
* **[Mission Generation]** Fixed problem with mission load when control point name contained an apostrophe.
|
||||
* **[Mission Generation]** Fixed EWR group names so they contribute to Skynet again.
|
||||
* **[Mission Generation]** Fixed duplicate name error when generating convoys and cargo ships when creating manual transfers after loading a game.
|
||||
* **[Mission Generation]** Fixed empty convoys not being disbanded when all units are killed/removed.
|
||||
* **[Mission Generation]** Fixed player losing frontline progress when skipping from turn 0 to turn 1.
|
||||
* **[Mission Generation]** Fixed issue where frontline would only search to the right for valid locations.
|
||||
* **[UI]** Made non-interactive map elements less obstructive.
|
||||
* **[UI]** Added support for Neutral Dot difficulty label
|
||||
* **[UI]** Clear skies at night no longer described as "Sunny" by the weather widget.
|
||||
* **[UI]** Removed ability to buy (useless) ground units at carriers and LHAs.
|
||||
* **[UI]** Fixed enable/disable of buy/sell buttons.
|
||||
* **[UI]** EWRs now appear in the custom waypoint list.
|
||||
|
||||
# 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
|
||||
## Features/Improvements
|
||||
* **[Campaign Map]** Overhauled the campaign model
|
||||
* **[Campaign Map]** Possible to add FOB as control points
|
||||
* **[Campaign Map]** Added off-map spawn locations
|
||||
* **[Campaign AI]** Overhauled AI recruiting behaviour
|
||||
* **[Campaign AI]** Added AI proucurement for Blue
|
||||
* **[Campaign 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.
|
||||
@@ -21,6 +351,7 @@
|
||||
* **[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 :
|
||||
@@ -34,7 +365,7 @@
|
||||
|
||||
# 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.
|
||||
|
||||
0
game/data/__init__.py
Normal file
0
game/data/__init__.py
Normal file
@@ -1,21 +0,0 @@
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
AAA_UNITS = [
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
AirDefence.AAA_ZU_23_Closed,
|
||||
AirDefence.AAA_ZU_23_Emplacement,
|
||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Insurgent_Closed,
|
||||
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Insurgent,
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
AirDefence.AAA_Flak_38,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
AirDefence.AAA_8_8cm_Flak_37,
|
||||
AirDefence.AAA_Flak_Vierling_38,
|
||||
AirDefence.AAA_Kdo_G_40,
|
||||
AirDefence.AAA_8_8cm_Flak_41,
|
||||
AirDefence.AAA_Bofors_40mm
|
||||
]
|
||||
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._1L13_EWR.id: 101,
|
||||
AirDefence._55G6_EWR.id: 102,
|
||||
AirDefence.S_300PS_40B6MD_sr.id: 103,
|
||||
AirDefence.S_300PS_64H6E_sr.id: 104,
|
||||
AirDefence.SA_11_Buk_SR_9S18M1.id: 107,
|
||||
AirDefence.Kub_1S91_str.id: 108,
|
||||
AirDefence.Dog_Ear_radar.id: 109,
|
||||
AirDefence.S_300PS_40B6M_tr.id: 110,
|
||||
AirDefence.SA_11_Buk_LN_9A310M1.id: 115,
|
||||
AirDefence.Osa_9A33_ln.id: 117,
|
||||
AirDefence.Strela_10M3.id: 118,
|
||||
AirDefence.Tor_9A331.id: 119,
|
||||
AirDefence._2S6_Tunguska.id: 120,
|
||||
AirDefence.ZSU_23_4_Shilka.id: 121,
|
||||
AirDefence.P_19_s_125_sr.id: 122,
|
||||
AirDefence.Snr_s_125_tr.id: 123,
|
||||
AirDefence.Rapier_fsa_blindfire_radar.id: 124,
|
||||
AirDefence.Rapier_fsa_launcher.id: 125,
|
||||
AirDefence.SNR_75V.id: 126,
|
||||
AirDefence.HQ_7_LN_SP.id: 127,
|
||||
AirDefence.HQ_7_STR_SP.id: 128,
|
||||
AirDefence.Roland_ADS.id: 201,
|
||||
AirDefence.Patriot_str.id: 202,
|
||||
AirDefence.Hawk_sr.id: 203,
|
||||
AirDefence.Hawk_tr.id: 204,
|
||||
AirDefence.Roland_Radar.id: 205,
|
||||
AirDefence.Hawk_cwar.id: 206,
|
||||
AirDefence.Gepard.id: 207,
|
||||
AirDefence.Vulcan.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', 'fob']
|
||||
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'fob']
|
||||
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'fob']
|
||||
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)
|
||||
]
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
from dcs.planes import (
|
||||
Bf_109K_4,
|
||||
C_101CC,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
I_16,
|
||||
L_39ZA,
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW
|
||||
)
|
||||
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
|
||||
"""
|
||||
This list contains the aircraft that do not use the guns as the last resort weapons, but as a main weapon
|
||||
They'll RTB when they don't have gun ammo left
|
||||
"""
|
||||
GUNFIGHTERS = [
|
||||
|
||||
# Cold War
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
F_86F_Sabre,
|
||||
A_4E_C,
|
||||
F_5E_3,
|
||||
|
||||
# Trainers
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
|
||||
# WW2
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
I_16,
|
||||
|
||||
]
|
||||
@@ -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,31 +25,45 @@ 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: int
|
||||
sweep_distance: Distance
|
||||
|
||||
ground_unit_procurement_ratios: GroundUnitProcurementRatios
|
||||
|
||||
|
||||
MODERN_DOCTRINE = Doctrine(
|
||||
@@ -45,26 +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=nm_to_meter(60),
|
||||
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(
|
||||
@@ -73,26 +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=nm_to_meter(40),
|
||||
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(
|
||||
@@ -101,24 +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=nm_to_meter(10),
|
||||
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,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
17
game/data/groundunitclass.py
Normal file
17
game/data/groundunitclass.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import unique, Enum
|
||||
|
||||
|
||||
@unique
|
||||
class GroundUnitClass(Enum):
|
||||
Tank = "Tank"
|
||||
Atgm = "ATGM"
|
||||
Ifv = "IFV"
|
||||
Apc = "APC"
|
||||
Artillery = "Artillery"
|
||||
Logistics = "Logistics"
|
||||
Recon = "Recon"
|
||||
Infantry = "Infantry"
|
||||
Shorads = "SHORADS"
|
||||
Manpads = "MANPADS"
|
||||
@@ -1,74 +1,108 @@
|
||||
from dcs.ships import (
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
CG_1164_Moskva,
|
||||
CVN_70_Carl_Vinson,
|
||||
CVN_71_Theodore_Roosevelt,
|
||||
CVN_72_Abraham_Lincoln,
|
||||
CVN_73_George_Washington,
|
||||
CVN_74_John_C__Stennis,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
CV_1143_5_Admiral_Kuznetsov_2017,
|
||||
FFG_11540_Neustrashimy,
|
||||
FFL_1124_4_Grisha,
|
||||
FF_1135M_Rezky,
|
||||
FSG_1241_1MP_Molniya,
|
||||
LHA_1_Tarawa,
|
||||
Oliver_Hazzard_Perry_class,
|
||||
Ticonderoga_class,
|
||||
Type_052B_Destroyer,
|
||||
Type_052C_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
PIOTR,
|
||||
MOSCOW,
|
||||
VINSON,
|
||||
CVN_71,
|
||||
CVN_72,
|
||||
CVN_73,
|
||||
Stennis,
|
||||
KUZNECOW,
|
||||
CV_1143_5,
|
||||
NEUSTRASH,
|
||||
ALBATROS,
|
||||
REZKY,
|
||||
MOLNIYA,
|
||||
LHA_Tarawa,
|
||||
PERRY,
|
||||
TICONDEROG,
|
||||
Type_052B,
|
||||
Type_052C,
|
||||
Type_054A,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
)
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
UNITS_WITH_RADAR = [
|
||||
TELARS = {
|
||||
AirDefence._2S6_Tunguska,
|
||||
AirDefence.SA_11_Buk_SR_9S18M1,
|
||||
AirDefence.Osa_9A33_ln,
|
||||
AirDefence.Tor_9A331,
|
||||
AirDefence.Roland_ADS,
|
||||
}
|
||||
|
||||
TRACK_RADARS = {
|
||||
AirDefence.Kub_1S91_str,
|
||||
AirDefence.Snr_s_125_tr,
|
||||
AirDefence.S_300PS_40B6M_tr,
|
||||
AirDefence.Hawk_tr,
|
||||
AirDefence.Patriot_str,
|
||||
AirDefence.SNR_75V,
|
||||
AirDefence.Rapier_fsa_blindfire_radar,
|
||||
AirDefence.HQ_7_STR_SP,
|
||||
}
|
||||
|
||||
LAUNCHER_TRACKER_PAIRS = {
|
||||
AirDefence.Kub_2P25_ln: AirDefence.Kub_1S91_str,
|
||||
AirDefence._5p73_s_125_ln: AirDefence.Snr_s_125_tr,
|
||||
AirDefence.S_300PS_5P85C_ln: AirDefence.S_300PS_40B6M_tr,
|
||||
AirDefence.S_300PS_5P85D_ln: AirDefence.S_300PS_40B6M_tr,
|
||||
AirDefence.Hawk_ln: AirDefence.Hawk_tr,
|
||||
AirDefence.Patriot_ln: AirDefence.Patriot_str,
|
||||
AirDefence.S_75M_Volhov: AirDefence.SNR_75V,
|
||||
AirDefence.Rapier_fsa_launcher: AirDefence.Rapier_fsa_blindfire_radar,
|
||||
AirDefence.HQ_7_LN_SP: AirDefence.HQ_7_STR_SP,
|
||||
}
|
||||
|
||||
UNITS_WITH_RADAR = {
|
||||
# Radars
|
||||
AirDefence.SAM_SA_15_Tor_9A331,
|
||||
AirDefence.SAM_SA_11_Buk_CC_9S470M1,
|
||||
AirDefence.SAM_Patriot_AMG_AN_MRC_137,
|
||||
AirDefence.SAM_Patriot_ECS_AN_MSQ_104,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
AirDefence.EWR_1L13,
|
||||
AirDefence.SAM_SA_6_Kub_STR_9S91,
|
||||
AirDefence.SAM_SA_10_S_300PS_TR_30N6,
|
||||
AirDefence.SAM_SA_10_S_300PS_SR_5N66M,
|
||||
AirDefence.EWR_55G6,
|
||||
AirDefence.SAM_SA_10_S_300PS_SR_64H6E,
|
||||
AirDefence.SAM_SA_11_Buk_SR_9S18M1,
|
||||
AirDefence.CP_9S80M1_Sborka,
|
||||
AirDefence.SAM_Hawk_TR_AN_MPQ_46,
|
||||
AirDefence.SAM_Hawk_SR_AN_MPQ_50,
|
||||
AirDefence.SAM_Patriot_STR_AN_MPQ_53,
|
||||
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
|
||||
AirDefence.SAM_SR_P_19,
|
||||
AirDefence.SAM_Roland_EWR,
|
||||
AirDefence.SAM_SA_3_S_125_TR_SNR,
|
||||
AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song,
|
||||
AirDefence.HQ_7_Self_Propelled_STR,
|
||||
|
||||
AirDefence._2S6_Tunguska,
|
||||
AirDefence.SA_11_Buk_LN_9A310M1,
|
||||
AirDefence.Osa_9A33_ln,
|
||||
AirDefence.Tor_9A331,
|
||||
AirDefence.Gepard,
|
||||
AirDefence.Vulcan,
|
||||
AirDefence.Roland_ADS,
|
||||
AirDefence.ZSU_23_4_Shilka,
|
||||
AirDefence._1L13_EWR,
|
||||
AirDefence.Kub_1S91_str,
|
||||
AirDefence.S_300PS_40B6M_tr,
|
||||
AirDefence.S_300PS_40B6MD_sr,
|
||||
AirDefence._55G6_EWR,
|
||||
AirDefence.S_300PS_64H6E_sr,
|
||||
AirDefence.SA_11_Buk_SR_9S18M1,
|
||||
AirDefence.Dog_Ear_radar,
|
||||
AirDefence.Hawk_tr,
|
||||
AirDefence.Hawk_sr,
|
||||
AirDefence.Patriot_str,
|
||||
AirDefence.Hawk_cwar,
|
||||
AirDefence.P_19_s_125_sr,
|
||||
AirDefence.Roland_Radar,
|
||||
AirDefence.Snr_s_125_tr,
|
||||
AirDefence.SNR_75V,
|
||||
AirDefence.Rapier_fsa_blindfire_radar,
|
||||
AirDefence.HQ_7_LN_SP,
|
||||
AirDefence.HQ_7_STR_SP,
|
||||
AirDefence.FuMG_401,
|
||||
AirDefence.FuSe_65,
|
||||
# Ships
|
||||
CVN_70_Carl_Vinson,
|
||||
Oliver_Hazzard_Perry_class,
|
||||
Ticonderoga_class,
|
||||
FFL_1124_4_Grisha,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
FSG_1241_1MP_Molniya,
|
||||
CG_1164_Moskva,
|
||||
FFG_11540_Neustrashimy,
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
FF_1135M_Rezky,
|
||||
CV_1143_5_Admiral_Kuznetsov_2017,
|
||||
CVN_74_John_C__Stennis,
|
||||
CVN_71_Theodore_Roosevelt,
|
||||
CVN_72_Abraham_Lincoln,
|
||||
CVN_73_George_Washington,
|
||||
VINSON,
|
||||
PERRY,
|
||||
TICONDEROG,
|
||||
ALBATROS,
|
||||
KUZNECOW,
|
||||
MOLNIYA,
|
||||
MOSCOW,
|
||||
NEUSTRASH,
|
||||
PIOTR,
|
||||
REZKY,
|
||||
CV_1143_5,
|
||||
Stennis,
|
||||
CVN_71,
|
||||
CVN_72,
|
||||
CVN_73,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
LHA_1_Tarawa,
|
||||
Type_052B_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
Type_052C_Destroyer
|
||||
]
|
||||
LHA_Tarawa,
|
||||
Type_052B,
|
||||
Type_054A,
|
||||
Type_052C,
|
||||
}
|
||||
|
||||
1176
game/data/weapons.py
Normal file
1176
game/data/weapons.py
Normal file
File diff suppressed because it is too large
Load Diff
1403
game/db.py
1403
game/db.py
File diff suppressed because it is too large
Load Diff
248
game/dcs/aircrafttype.py
Normal file
248
game/dcs/aircrafttype.py
Normal file
@@ -0,0 +1,248 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, Type, Iterator, TYPE_CHECKING, Optional, Any
|
||||
|
||||
import yaml
|
||||
from dcs.helicopters import helicopter_map
|
||||
from dcs.planes import plane_map
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.dcs.unittype import UnitType
|
||||
from game.radio.channels import (
|
||||
ChannelNamer,
|
||||
RadioChannelAllocator,
|
||||
CommonRadioChannelAllocator,
|
||||
HueyChannelNamer,
|
||||
SCR522ChannelNamer,
|
||||
ViggenChannelNamer,
|
||||
ViperChannelNamer,
|
||||
TomcatChannelNamer,
|
||||
MirageChannelNamer,
|
||||
SingleRadioChannelNamer,
|
||||
FarmerRadioChannelAllocator,
|
||||
SCR522RadioChannelAllocator,
|
||||
ViggenRadioChannelAllocator,
|
||||
NoOpChannelAllocator,
|
||||
)
|
||||
from game.utils import Distance, Speed, feet, kph, knots
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.aircraft import FlightData
|
||||
from gen import AirSupport, RadioFrequency, RadioRegistry
|
||||
from gen.radios import Radio
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RadioConfig:
|
||||
inter_flight: Optional[Radio]
|
||||
intra_flight: Optional[Radio]
|
||||
channel_allocator: Optional[RadioChannelAllocator]
|
||||
channel_namer: Type[ChannelNamer]
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: dict[str, Any]) -> RadioConfig:
|
||||
return RadioConfig(
|
||||
cls.make_radio(data.get("inter_flight", None)),
|
||||
cls.make_radio(data.get("intra_flight", None)),
|
||||
cls.make_allocator(data.get("channels", {})),
|
||||
cls.make_namer(data.get("channels", {})),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def make_radio(cls, name: Optional[str]) -> Optional[Radio]:
|
||||
from gen.radios import get_radio
|
||||
|
||||
if name is None:
|
||||
return None
|
||||
return get_radio(name)
|
||||
|
||||
@classmethod
|
||||
def make_allocator(cls, data: dict[str, Any]) -> Optional[RadioChannelAllocator]:
|
||||
try:
|
||||
alloc_type = data["type"]
|
||||
except KeyError:
|
||||
return None
|
||||
allocator_type: Type[RadioChannelAllocator] = {
|
||||
"SCR-522": SCR522RadioChannelAllocator,
|
||||
"common": CommonRadioChannelAllocator,
|
||||
"farmer": FarmerRadioChannelAllocator,
|
||||
"noop": NoOpChannelAllocator,
|
||||
"viggen": ViggenRadioChannelAllocator,
|
||||
}[alloc_type]
|
||||
return allocator_type.from_cfg(data)
|
||||
|
||||
@classmethod
|
||||
def make_namer(cls, config: dict[str, Any]) -> Type[ChannelNamer]:
|
||||
return {
|
||||
"SCR-522": SCR522ChannelNamer,
|
||||
"default": ChannelNamer,
|
||||
"huey": HueyChannelNamer,
|
||||
"mirage": MirageChannelNamer,
|
||||
"single": SingleRadioChannelNamer,
|
||||
"tomcat": TomcatChannelNamer,
|
||||
"viggen": ViggenChannelNamer,
|
||||
"viper": ViperChannelNamer,
|
||||
}[config.get("namer", "default")]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PatrolConfig:
|
||||
altitude: Optional[Distance]
|
||||
speed: Optional[Speed]
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: dict[str, Any]) -> PatrolConfig:
|
||||
altitude = data.get("altitude", None)
|
||||
speed = data.get("altitude", None)
|
||||
return PatrolConfig(
|
||||
feet(altitude) if altitude is not None else None,
|
||||
knots(speed) if speed is not None else None,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AircraftType(UnitType[FlyingType]):
|
||||
carrier_capable: bool
|
||||
lha_capable: bool
|
||||
always_keeps_gun: bool
|
||||
|
||||
# If true, the aircraft does not use the guns as the last resort weapons, but as a main weapon.
|
||||
# It'll RTB when it doesn't have gun ammo left.
|
||||
gunfighter: bool
|
||||
|
||||
max_group_size: int
|
||||
patrol_altitude: Optional[Distance]
|
||||
patrol_speed: Optional[Speed]
|
||||
intra_flight_radio: Optional[Radio]
|
||||
channel_allocator: Optional[RadioChannelAllocator]
|
||||
channel_namer: Type[ChannelNamer]
|
||||
|
||||
_by_name: ClassVar[dict[str, AircraftType]] = {}
|
||||
_by_unit_type: ClassVar[dict[Type[FlyingType], list[AircraftType]]] = defaultdict(
|
||||
list
|
||||
)
|
||||
_loaded: ClassVar[bool] = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def dcs_id(self) -> str:
|
||||
return self.dcs_unit_type.id
|
||||
|
||||
@property
|
||||
def flyable(self) -> bool:
|
||||
return self.dcs_unit_type.flyable
|
||||
|
||||
@cached_property
|
||||
def max_speed(self) -> Speed:
|
||||
return kph(self.dcs_unit_type.max_speed)
|
||||
|
||||
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
|
||||
from gen.radios import ChannelInUseError, MHz
|
||||
|
||||
if self.intra_flight_radio is not None:
|
||||
return radio_registry.alloc_for_radio(self.intra_flight_radio)
|
||||
|
||||
freq = MHz(self.dcs_unit_type.radio_frequency)
|
||||
try:
|
||||
radio_registry.reserve(freq)
|
||||
except ChannelInUseError:
|
||||
pass
|
||||
return freq
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
if self.channel_allocator is not None:
|
||||
self.channel_allocator.assign_channels_for_flight(flight, air_support)
|
||||
|
||||
def channel_name(self, radio_id: int, channel_id: int) -> str:
|
||||
return self.channel_namer.channel_name(radio_id, channel_id)
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# Update any existing models with new data on load.
|
||||
updated = AircraftType.named(state["name"])
|
||||
state.update(updated.__dict__)
|
||||
self.__dict__.update(state)
|
||||
|
||||
@classmethod
|
||||
def register(cls, aircraft_type: AircraftType) -> None:
|
||||
cls._by_name[aircraft_type.name] = aircraft_type
|
||||
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
|
||||
|
||||
@classmethod
|
||||
def named(cls, name: str) -> AircraftType:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
return cls._by_name[name]
|
||||
|
||||
@classmethod
|
||||
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
yield from cls._by_unit_type[dcs_unit_type]
|
||||
|
||||
@staticmethod
|
||||
def _each_unit_type() -> Iterator[Type[FlyingType]]:
|
||||
yield from helicopter_map.values()
|
||||
yield from plane_map.values()
|
||||
|
||||
@classmethod
|
||||
def _load_all(cls) -> None:
|
||||
for unit_type in cls._each_unit_type():
|
||||
for data in cls._each_variant_of(unit_type):
|
||||
cls.register(data)
|
||||
cls._loaded = True
|
||||
|
||||
@classmethod
|
||||
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
|
||||
data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
|
||||
if not data_path.exists():
|
||||
logging.warning(f"No data for {aircraft.id}; it will not be available")
|
||||
return
|
||||
|
||||
with data_path.open() as data_file:
|
||||
data = yaml.safe_load(data_file)
|
||||
|
||||
try:
|
||||
price = data["price"]
|
||||
except KeyError as ex:
|
||||
raise KeyError(f"Missing required price field: {data_path}") from ex
|
||||
|
||||
radio_config = RadioConfig.from_data(data.get("radios", {}))
|
||||
patrol_config = PatrolConfig.from_data(data.get("patrol", {}))
|
||||
|
||||
try:
|
||||
introduction = data["introduced"]
|
||||
if introduction is None:
|
||||
introduction = "N/A"
|
||||
except KeyError:
|
||||
introduction = "No data."
|
||||
|
||||
for variant in data.get("variants", [aircraft.id]):
|
||||
yield AircraftType(
|
||||
dcs_unit_type=aircraft,
|
||||
name=variant,
|
||||
description=data.get("description", "No data."),
|
||||
year_introduced=introduction,
|
||||
country_of_origin=data.get("origin", "No data."),
|
||||
manufacturer=data.get("manufacturer", "No data."),
|
||||
role=data.get("role", "No data."),
|
||||
price=price,
|
||||
carrier_capable=data.get("carrier_capable", False),
|
||||
lha_capable=data.get("lha_capable", False),
|
||||
always_keeps_gun=data.get("always_keeps_gun", False),
|
||||
gunfighter=data.get("gunfighter", False),
|
||||
max_group_size=data.get("max_group_size", aircraft.group_size_max),
|
||||
patrol_altitude=patrol_config.altitude,
|
||||
patrol_speed=patrol_config.speed,
|
||||
intra_flight_radio=radio_config.intra_flight,
|
||||
channel_allocator=radio_config.channel_allocator,
|
||||
channel_namer=radio_config.channel_namer,
|
||||
)
|
||||
97
game/dcs/groundunittype.py
Normal file
97
game/dcs/groundunittype.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Type, Optional, ClassVar, Iterator
|
||||
|
||||
import yaml
|
||||
from dcs.unittype import VehicleType
|
||||
from dcs.vehicles import vehicle_map
|
||||
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.unittype import UnitType
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundUnitType(UnitType[VehicleType]):
|
||||
unit_class: Optional[GroundUnitClass]
|
||||
spawn_weight: int
|
||||
|
||||
_by_name: ClassVar[dict[str, GroundUnitType]] = {}
|
||||
_by_unit_type: ClassVar[
|
||||
dict[Type[VehicleType], list[GroundUnitType]]
|
||||
] = defaultdict(list)
|
||||
_loaded: ClassVar[bool] = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def dcs_id(self) -> str:
|
||||
return self.dcs_unit_type.id
|
||||
|
||||
@classmethod
|
||||
def register(cls, aircraft_type: GroundUnitType) -> None:
|
||||
cls._by_name[aircraft_type.name] = aircraft_type
|
||||
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
|
||||
|
||||
@classmethod
|
||||
def named(cls, name: str) -> GroundUnitType:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
return cls._by_name[name]
|
||||
|
||||
@classmethod
|
||||
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
yield from cls._by_unit_type[dcs_unit_type]
|
||||
|
||||
@staticmethod
|
||||
def _each_unit_type() -> Iterator[Type[VehicleType]]:
|
||||
yield from vehicle_map.values()
|
||||
|
||||
@classmethod
|
||||
def _load_all(cls) -> None:
|
||||
for unit_type in cls._each_unit_type():
|
||||
for data in cls._each_variant_of(unit_type):
|
||||
cls.register(data)
|
||||
cls._loaded = True
|
||||
|
||||
@classmethod
|
||||
def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]:
|
||||
data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml"
|
||||
if not data_path.exists():
|
||||
logging.warning(f"No data for {vehicle.id}; it will not be available")
|
||||
return
|
||||
|
||||
with data_path.open() as data_file:
|
||||
data = yaml.safe_load(data_file)
|
||||
|
||||
try:
|
||||
introduction = data["introduced"]
|
||||
if introduction is None:
|
||||
introduction = "N/A"
|
||||
except KeyError:
|
||||
introduction = "No data."
|
||||
|
||||
class_name = data.get("class")
|
||||
unit_class: Optional[GroundUnitClass] = None
|
||||
if class_name is not None:
|
||||
unit_class = GroundUnitClass(class_name)
|
||||
|
||||
for variant in data.get("variants", [vehicle.id]):
|
||||
yield GroundUnitType(
|
||||
dcs_unit_type=vehicle,
|
||||
unit_class=unit_class,
|
||||
spawn_weight=data.get("spawn_weight", 0),
|
||||
name=variant,
|
||||
description=data.get("description", "No data."),
|
||||
year_introduced=introduction,
|
||||
country_of_origin=data.get("origin", "No data."),
|
||||
manufacturer=data.get("manufacturer", "No data."),
|
||||
role=data.get("role", "No data."),
|
||||
price=data.get("price", 1),
|
||||
)
|
||||
26
game/dcs/unittype.py
Normal file
26
game/dcs/unittype.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from typing import TypeVar, Generic, Type
|
||||
|
||||
from dcs.unittype import UnitType as DcsUnitType
|
||||
|
||||
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=DcsUnitType)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UnitType(Generic[DcsUnitTypeT]):
|
||||
dcs_unit_type: Type[DcsUnitTypeT]
|
||||
name: str
|
||||
description: str
|
||||
year_introduced: str
|
||||
country_of_origin: str
|
||||
manufacturer: str
|
||||
role: str
|
||||
price: int
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@cached_property
|
||||
def eplrs_capable(self) -> bool:
|
||||
return getattr(self.dcs_unit_type, "eplrs", False)
|
||||
@@ -14,15 +14,23 @@ from typing import (
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Type,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater import Airfield, ControlPoint
|
||||
from game.unitmap import Building, FrontLineUnit, GroundObjectUnit, UnitMap
|
||||
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:
|
||||
@@ -33,24 +41,24 @@ DEBRIEFING_LOG_EXTENSION = "log"
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirLosses:
|
||||
player: List[Flight]
|
||||
enemy: List[Flight]
|
||||
player: List[FlyingUnit]
|
||||
enemy: List[FlyingUnit]
|
||||
|
||||
@property
|
||||
def losses(self) -> Iterator[Flight]:
|
||||
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)
|
||||
def by_type(self, player: bool) -> Dict[AircraftType, int]:
|
||||
losses_by_type: Dict[AircraftType, int] = defaultdict(int)
|
||||
losses = self.player if player else self.enemy
|
||||
for loss in losses:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
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:
|
||||
if loss.flight == flight:
|
||||
losses += 1
|
||||
return flight.count - losses
|
||||
|
||||
@@ -60,6 +68,15 @@ 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)
|
||||
|
||||
@@ -70,6 +87,12 @@ class GroundLosses:
|
||||
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.
|
||||
@@ -94,16 +117,21 @@ class StateData:
|
||||
killed_aircraft=data["killed_aircrafts"],
|
||||
# Airfields emit a new "dead" event every time a bomb is dropped on
|
||||
# them when they've already dead. Dedup.
|
||||
killed_ground_units=list(set(data["killed_ground_units"])),
|
||||
#
|
||||
# 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"]
|
||||
base_capture_events=data["base_capture_events"],
|
||||
)
|
||||
|
||||
|
||||
class Debriefing:
|
||||
def __init__(self, state_data: Dict[str, Any], game: Game,
|
||||
unit_map: UnitMap) -> None:
|
||||
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
|
||||
@@ -113,12 +141,28 @@ class Debriefing:
|
||||
|
||||
self.air_losses = self.dead_aircraft()
|
||||
self.ground_losses = self.dead_ground_units()
|
||||
self.base_captures = self.base_capture_events()
|
||||
|
||||
@property
|
||||
def front_line_losses(self) -> Iterator[FrontLineUnit]:
|
||||
yield from self.ground_losses.player_front_line
|
||||
yield from self.ground_losses.enemy_front_line
|
||||
|
||||
@property
|
||||
def 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
|
||||
@@ -135,13 +179,10 @@ class Debriefing:
|
||||
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]
|
||||
)
|
||||
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)
|
||||
def front_line_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
|
||||
if player:
|
||||
losses = self.ground_losses.player_front_line
|
||||
else:
|
||||
@@ -150,6 +191,38 @@ class Debriefing:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def convoy_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, 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[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, 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[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, 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:
|
||||
@@ -167,14 +240,14 @@ class Debriefing:
|
||||
player_losses = []
|
||||
enemy_losses = []
|
||||
for unit_name in self.state_data.killed_aircraft:
|
||||
flight = self.unit_map.flight(unit_name)
|
||||
if flight is None:
|
||||
aircraft = self.unit_map.flight(unit_name)
|
||||
if aircraft is None:
|
||||
logging.error(f"Could not find Flight matching {unit_name}")
|
||||
continue
|
||||
if flight.departure.captured:
|
||||
player_losses.append(flight)
|
||||
if aircraft.flight.departure.captured:
|
||||
player_losses.append(aircraft)
|
||||
else:
|
||||
enemy_losses.append(flight)
|
||||
enemy_losses.append(aircraft)
|
||||
return AirLosses(player_losses, enemy_losses)
|
||||
|
||||
def dead_ground_units(self) -> GroundLosses:
|
||||
@@ -188,6 +261,22 @@ class Debriefing:
|
||||
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:
|
||||
@@ -197,6 +286,11 @@ class Debriefing:
|
||||
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)
|
||||
@@ -216,28 +310,60 @@ class Debriefing:
|
||||
# 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.")
|
||||
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
|
||||
|
||||
@property
|
||||
def base_capture_events(self):
|
||||
def base_capture_events(self) -> List[BaseCaptureEvent]:
|
||||
"""Keeps only the last instance of a base capture event for each base ID."""
|
||||
reversed_captures = list(reversed(self.state_data.base_capture_events))
|
||||
last_base_cap_indexes = []
|
||||
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
|
||||
if base not in [x[1] for x in last_base_cap_indexes]:
|
||||
last_base_cap_indexes.append((idx, base))
|
||||
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
|
||||
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: Callable[[Debriefing], None],
|
||||
game: Game, unit_map: UnitMap) -> None:
|
||||
def __init__(
|
||||
self, callback: Callable[[Debriefing], None], game: Game, unit_map: UnitMap
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._stop_event = threading.Event()
|
||||
self.callback = callback
|
||||
@@ -256,17 +382,27 @@ 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:
|
||||
with open("state.json", "r") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
debriefing = Debriefing(json_data, self.game, self.unit_map)
|
||||
self.callback(debriefing)
|
||||
break
|
||||
try:
|
||||
if (
|
||||
os.path.isfile("state.json")
|
||||
and os.path.getmtime("state.json") > last_modified
|
||||
):
|
||||
with open("state.json", "r") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
debriefing = Debriefing(json_data, self.game, self.unit_map)
|
||||
self.callback(debriefing)
|
||||
break
|
||||
except json.JSONDecodeError:
|
||||
logging.exception(
|
||||
"Failed to decode state.json. Probably attempted read while DCS "
|
||||
"was still writing the file. Will retry in 5 seconds."
|
||||
)
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def wait_for_debriefing(callback: Callable[[Debriefing], None],
|
||||
game: Game, unit_map) -> PollDebriefingFileThread:
|
||||
def wait_for_debriefing(
|
||||
callback: Callable[[Debriefing], None], game: Game, unit_map
|
||||
) -> PollDebriefingFileThread:
|
||||
thread = PollDebriefingFileThread(callback, game, unit_map)
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, List, TYPE_CHECKING, Type
|
||||
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 persistency
|
||||
from game.debriefing import AirLosses, Debriefing
|
||||
@@ -15,16 +14,13 @@ from game.operation.operation import Operation
|
||||
from game.theater import ControlPoint
|
||||
from gen import AirTaskingOrder
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from ..dcs.groundunittype import GroundUnitType
|
||||
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
|
||||
STRONG_DEFEAT_INFLUENCE = 0.5
|
||||
@@ -39,9 +35,16 @@ class Event:
|
||||
from_cp = None # type: ControlPoint
|
||||
to_cp = None # type: ControlPoint
|
||||
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.from_cp = from_cp
|
||||
self.to_cp = target_cp
|
||||
@@ -51,25 +54,24 @@ class Event:
|
||||
|
||||
@property
|
||||
def is_player_attacking(self) -> bool:
|
||||
return self.attacker_name == self.game.player_name
|
||||
return self.attacker_name == self.game.player_faction.name
|
||||
|
||||
@property
|
||||
def tasks(self) -> List[Type[Task]]:
|
||||
return []
|
||||
|
||||
def bonus(self) -> int:
|
||||
return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE)
|
||||
|
||||
def generate(self) -> UnitMap:
|
||||
Operation.prepare(self.game)
|
||||
unit_map = Operation.generate()
|
||||
Operation.current_mission.save(
|
||||
persistency.mission_path_for("liberation_nextturn.miz"))
|
||||
persistency.mission_path_for("liberation_nextturn.miz")
|
||||
)
|
||||
return unit_map
|
||||
|
||||
@staticmethod
|
||||
def _transfer_aircraft(ato: AirTaskingOrder, losses: AirLosses,
|
||||
for_player: bool) -> None:
|
||||
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.
|
||||
@@ -84,13 +86,16 @@ class Event:
|
||||
if flight.arrival.captured != for_player:
|
||||
logging.info(
|
||||
f"Not transferring {flight} because {flight.arrival} "
|
||||
"was captured")
|
||||
"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.")
|
||||
logging.error(
|
||||
f"{flight} had {flight.count} aircraft but "
|
||||
f"{transfer_count} losses were recorded."
|
||||
)
|
||||
continue
|
||||
|
||||
aircraft = flight.unit_type
|
||||
@@ -98,7 +103,8 @@ class Event:
|
||||
if available < transfer_count:
|
||||
logging.error(
|
||||
f"Found killed {aircraft} from {flight.departure} but "
|
||||
f"that airbase has only {available} available.")
|
||||
f"that airbase has only {available} available."
|
||||
)
|
||||
continue
|
||||
|
||||
flight.departure.base.aircraft[aircraft] -= transfer_count
|
||||
@@ -108,26 +114,50 @@ class Event:
|
||||
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)
|
||||
self._transfer_aircraft(
|
||||
self.game.blue_ato, debriefing.air_losses, for_player=True
|
||||
)
|
||||
self._transfer_aircraft(
|
||||
self.game.red_ato, debriefing.air_losses, for_player=False
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def commit_air_losses(debriefing: Debriefing) -> None:
|
||||
def commit_air_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.air_losses.losses:
|
||||
aircraft = loss.unit_type
|
||||
cp = loss.departure
|
||||
if loss.pilot is not None and (
|
||||
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.")
|
||||
"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
|
||||
pilot.record.missions_flown += 1
|
||||
|
||||
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:
|
||||
@@ -137,12 +167,54 @@ class Event:
|
||||
if available <= 0:
|
||||
logging.error(
|
||||
f"Found killed {unit_type} from {control_point} but that "
|
||||
"airbase has none available.")
|
||||
"airbase has none available."
|
||||
)
|
||||
continue
|
||||
|
||||
logging.info(f"{unit_type} destroyed from {control_point}")
|
||||
control_point.base.armor[unit_type] -= 1
|
||||
|
||||
@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
|
||||
|
||||
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:
|
||||
@@ -152,69 +224,59 @@ class Event:
|
||||
|
||||
loss.group.units.remove(loss.unit)
|
||||
loss.group.units_losts.append(loss.unit)
|
||||
if not loss.ground_object.alive_unit_count:
|
||||
loss.ground_object.is_dead = True
|
||||
|
||||
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.building_losses:
|
||||
loss.ground_object.is_dead = True
|
||||
self.game.informations.append(Information(
|
||||
"Building destroyed",
|
||||
f"{loss.ground_object.dcs_identifier} has been destroyed at "
|
||||
f"location {loss.ground_object.obj_name}", self.game.turn
|
||||
))
|
||||
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:
|
||||
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,
|
||||
)
|
||||
|
||||
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}")
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
logging.info("Committing mission results")
|
||||
|
||||
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)
|
||||
|
||||
# ------------------------------
|
||||
# Captured bases
|
||||
#if self.game.player_country in db.BLUEFOR_FACTIONS:
|
||||
coalition = 2 # Value in DCS mission event for BLUE
|
||||
#else:
|
||||
# coalition = 1 # Value in DCS mission event for RED
|
||||
|
||||
for captured in debriefing.base_capture_events:
|
||||
try:
|
||||
id = int(captured.split("||")[0])
|
||||
new_owner_coalition = int(captured.split("||")[1])
|
||||
|
||||
captured_cps = []
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.id == id:
|
||||
|
||||
if cp.captured and new_owner_coalition != coalition:
|
||||
for_player = False
|
||||
info = Information(cp.name + " lost !", "The ennemy took control of " + cp.name + "\nShame on us !", self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
captured_cps.append(cp)
|
||||
elif not(cp.captured) and new_owner_coalition == coalition:
|
||||
for_player = True
|
||||
info = Information(cp.name + " captured !", "We took control of " + cp.name + "! Great job !", self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
captured_cps.append(cp)
|
||||
else:
|
||||
continue
|
||||
|
||||
cp.capture(self.game, for_player)
|
||||
|
||||
for cp in captured_cps:
|
||||
logging.info("Will run redeploy for " + cp.name)
|
||||
self.redeploy_units(cp)
|
||||
except Exception:
|
||||
logging.exception(f"Could not process base capture {captured}")
|
||||
|
||||
self.commit_captures(debriefing)
|
||||
self.complete_aircraft_transfers(debriefing)
|
||||
|
||||
# Destroyed units carcass
|
||||
@@ -227,7 +289,12 @@ 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
|
||||
@@ -243,7 +310,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
|
||||
@@ -268,11 +339,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:
|
||||
@@ -284,7 +361,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
|
||||
|
||||
@@ -292,93 +372,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.commission_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 isinstance(unit_type, GroundUnitType):
|
||||
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: Game) -> None:
|
||||
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[Type[UnitType], int] = {}
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "Pending delivery to {}".format(self.to_cp)
|
||||
|
||||
def deliver(self, units: Dict[Type[UnitType], int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] = self.units.get(k, 0) + v
|
||||
|
||||
def skip(self) -> None:
|
||||
for k, v in self.units.items():
|
||||
if self.to_cp.captured:
|
||||
name = "Ally "
|
||||
else:
|
||||
name = "Enemy "
|
||||
self.game.message(
|
||||
f"{name} reinforcements: {k.id} x {v} at {self.to_cp.name}")
|
||||
|
||||
self.to_cp.base.commision_units(self.units)
|
||||
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)
|
||||
logging.info(text)
|
||||
|
||||
@@ -7,5 +7,6 @@ class FrontlineAttackEvent(Event):
|
||||
Currently the same as its parent, but here for legacy compatibility as well as to allow for
|
||||
future unique Event handling
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return "Frontline attack"
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict, Type, List, Any, cast
|
||||
from typing import Optional, Dict, Type, List, Any, Iterator
|
||||
|
||||
import dcs
|
||||
from dcs.countries import country_dict
|
||||
from dcs.planes import plane_map
|
||||
from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
|
||||
from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
|
||||
from dcs.unittype import ShipType, UnitType
|
||||
|
||||
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
|
||||
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.groundunitclass import GroundUnitClass
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
|
||||
|
||||
@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,25 +45,25 @@ class Faction:
|
||||
description: str = field(default="")
|
||||
|
||||
# Available aircraft
|
||||
aircrafts: List[Type[FlyingType]] = field(default_factory=list)
|
||||
aircrafts: List[AircraftType] = field(default_factory=list)
|
||||
|
||||
# Available awacs aircraft
|
||||
awacs: List[Type[FlyingType]] = field(default_factory=list)
|
||||
awacs: List[AircraftType] = field(default_factory=list)
|
||||
|
||||
# Available tanker aircraft
|
||||
tankers: List[Type[FlyingType]] = field(default_factory=list)
|
||||
tankers: List[AircraftType] = field(default_factory=list)
|
||||
|
||||
# Available frontline units
|
||||
frontline_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
frontline_units: List[GroundUnitType] = field(default_factory=list)
|
||||
|
||||
# Available artillery units
|
||||
artillery_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
artillery_units: List[GroundUnitType] = field(default_factory=list)
|
||||
|
||||
# Infantry units used
|
||||
infantry_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
infantry_units: List[GroundUnitType] = field(default_factory=list)
|
||||
|
||||
# Logistics units used
|
||||
logistics_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
logistics_units: List[GroundUnitType] = field(default_factory=list)
|
||||
|
||||
# Possible SAMS site generators for this faction
|
||||
air_defenses: List[str] = field(default_factory=list)
|
||||
@@ -60,6 +74,9 @@ 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)
|
||||
|
||||
@@ -90,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[Type[FlyingType]] = field(default=None)
|
||||
jtac_unit: Optional[AircraftType] = field(default=None)
|
||||
|
||||
# doctrine
|
||||
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
|
||||
@@ -103,8 +123,7 @@ class Faction:
|
||||
building_set: List[str] = field(default_factory=list)
|
||||
|
||||
# List of default livery overrides
|
||||
liveries_overrides: Dict[Type[UnitType], List[str]] = field(
|
||||
default_factory=dict)
|
||||
liveries_overrides: Dict[AircraftType, 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
|
||||
@@ -115,14 +134,23 @@ class Faction:
|
||||
#: both will use it.
|
||||
unrestricted_satnav: bool = False
|
||||
|
||||
def has_access_to_unittype(self, unit_class: GroundUnitClass) -> bool:
|
||||
for vehicle in itertools.chain(self.frontline_units, self.artillery_units):
|
||||
if vehicle.unit_class is unit_class:
|
||||
return True
|
||||
return False
|
||||
|
||||
@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:
|
||||
@@ -131,18 +159,26 @@ class Faction:
|
||||
faction.authors = json.get("authors", "")
|
||||
faction.description = json.get("description", "")
|
||||
|
||||
faction.aircrafts = load_all_aircraft(json.get("aircrafts", []))
|
||||
faction.awacs = load_all_aircraft(json.get("awacs", []))
|
||||
faction.tankers = load_all_aircraft(json.get("tankers", []))
|
||||
faction.aircrafts = [AircraftType.named(n) for n in json.get("aircrafts", [])]
|
||||
faction.awacs = [AircraftType.named(n) for n in json.get("awacs", [])]
|
||||
faction.tankers = [AircraftType.named(n) for n in 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.tankers)
|
||||
)
|
||||
|
||||
faction.frontline_units = [
|
||||
GroundUnitType.named(n) for n in json.get("frontline_units", [])
|
||||
]
|
||||
faction.artillery_units = [
|
||||
GroundUnitType.named(n) for n in json.get("artillery_units", [])
|
||||
]
|
||||
faction.infantry_units = [
|
||||
GroundUnitType.named(n) for n in json.get("infantry_units", [])
|
||||
]
|
||||
faction.logistics_units = [
|
||||
GroundUnitType.named(n) for n in json.get("logistics_units", [])
|
||||
]
|
||||
|
||||
faction.ewrs = json.get("ewrs", [])
|
||||
|
||||
@@ -153,26 +189,25 @@ class Faction:
|
||||
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)
|
||||
jtac_name = json.get("jtac_unit", None)
|
||||
if jtac_name is not None:
|
||||
faction.jtac_unit = load_aircraft(jtac_name)
|
||||
faction.jtac_unit = AircraftType.named(jtac_name)
|
||||
else:
|
||||
faction.jtac_unit = None
|
||||
faction.navy_group_count = int(json.get("navy_group_count", 1))
|
||||
faction.missiles_group_count = int(json.get("missiles_group_count", 0))
|
||||
faction.coastal_group_count = int(json.get("coastal_group_count", 0))
|
||||
|
||||
# Load doctrine
|
||||
doctrine = json.get("doctrine", "modern")
|
||||
@@ -201,78 +236,110 @@ class Faction:
|
||||
# Load liveries override
|
||||
faction.liveries_overrides = {}
|
||||
liveries_overrides = json.get("liveries_overrides", {})
|
||||
for k, v in liveries_overrides.items():
|
||||
k = load_aircraft(k)
|
||||
if k is not None:
|
||||
faction.liveries_overrides[k] = [s.lower() for s in v]
|
||||
for name, livery in liveries_overrides.items():
|
||||
aircraft = AircraftType.named(name)
|
||||
faction.liveries_overrides[aircraft] = [s.lower() for s in livery]
|
||||
|
||||
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
|
||||
|
||||
return faction
|
||||
|
||||
@property
|
||||
def units(self) -> List[Type[UnitType]]:
|
||||
return (self.infantry_units + self.aircrafts + self.awacs +
|
||||
self.artillery_units + self.frontline_units +
|
||||
self.tankers + self.logistics_units)
|
||||
def ground_units(self) -> Iterator[GroundUnitType]:
|
||||
yield from self.artillery_units
|
||||
yield from self.frontline_units
|
||||
yield from self.logistics_units
|
||||
|
||||
def infantry_with_class(
|
||||
self, unit_class: GroundUnitClass
|
||||
) -> Iterator[GroundUnitType]:
|
||||
for unit in self.infantry_units:
|
||||
if unit.unit_class is unit_class:
|
||||
yield unit
|
||||
|
||||
def unit_loader(
|
||||
unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
|
||||
"""
|
||||
Find unit by name
|
||||
:param unit: Unit name as string
|
||||
:param class_repository: Repository of classes (Either a module, a class, or a list of classes)
|
||||
:return: The unit as a PyDCS type
|
||||
"""
|
||||
if unit is None:
|
||||
return None
|
||||
elif unit in plane_map.keys():
|
||||
return plane_map[unit]
|
||||
else:
|
||||
for mother_class in class_repository:
|
||||
if getattr(mother_class, unit, None) is not None:
|
||||
return getattr(mother_class, unit)
|
||||
if type(mother_class) is list:
|
||||
for m in mother_class:
|
||||
if m.__name__ == unit:
|
||||
return m
|
||||
logging.error(f"FACTION ERROR : Unable to find {unit} in pydcs")
|
||||
return None
|
||||
def apply_mod_settings(self, mod_settings) -> Faction:
|
||||
# aircraft
|
||||
if not mod_settings.a4_skyhawk:
|
||||
self.remove_aircraft("A-4E-C")
|
||||
if not mod_settings.hercules:
|
||||
self.remove_aircraft("Hercules")
|
||||
if not mod_settings.f22_raptor:
|
||||
self.remove_aircraft("F-22A")
|
||||
if not mod_settings.jas39_gripen:
|
||||
self.remove_aircraft("JAS39Gripen")
|
||||
self.remove_aircraft("JAS39Gripen_AG")
|
||||
if not mod_settings.su57_felon:
|
||||
self.remove_aircraft("Su-57")
|
||||
# frenchpack
|
||||
if not mod_settings.frenchpack:
|
||||
self.remove_vehicle("AMX10RCR")
|
||||
self.remove_vehicle("SEPAR")
|
||||
self.remove_vehicle("ERC")
|
||||
self.remove_vehicle("M120")
|
||||
self.remove_vehicle("AA20")
|
||||
self.remove_vehicle("TRM2000")
|
||||
self.remove_vehicle("TRM2000_Citerne")
|
||||
self.remove_vehicle("TRM2000_AA20")
|
||||
self.remove_vehicle("TRMMISTRAL")
|
||||
self.remove_vehicle("VABH")
|
||||
self.remove_vehicle("VAB_RADIO")
|
||||
self.remove_vehicle("VAB_50")
|
||||
self.remove_vehicle("VIB_VBR")
|
||||
self.remove_vehicle("VAB_HOT")
|
||||
self.remove_vehicle("VAB_MORTIER")
|
||||
self.remove_vehicle("VBL50")
|
||||
self.remove_vehicle("VBLANF1")
|
||||
self.remove_vehicle("VBL-radio")
|
||||
self.remove_vehicle("VBAE")
|
||||
self.remove_vehicle("VBAE_MMP")
|
||||
self.remove_vehicle("AMX-30B2")
|
||||
self.remove_vehicle("Tracma")
|
||||
self.remove_vehicle("JTACFP")
|
||||
self.remove_vehicle("SHERIDAN")
|
||||
self.remove_vehicle("Leclerc_XXI")
|
||||
self.remove_vehicle("Toyota_bleu")
|
||||
self.remove_vehicle("Toyota_vert")
|
||||
self.remove_vehicle("Toyota_desert")
|
||||
self.remove_vehicle("Kamikaze")
|
||||
self.remove_vehicle("AMX1375")
|
||||
self.remove_vehicle("AMX1390")
|
||||
self.remove_vehicle("VBCI")
|
||||
self.remove_vehicle("T62")
|
||||
self.remove_vehicle("T64BV")
|
||||
self.remove_vehicle("T72M")
|
||||
self.remove_vehicle("KORNET")
|
||||
# high digit sams
|
||||
if not mod_settings.high_digit_sams:
|
||||
self.remove_air_defenses("SA10BGenerator")
|
||||
self.remove_air_defenses("SA12Generator")
|
||||
self.remove_air_defenses("SA20Generator")
|
||||
self.remove_air_defenses("SA20BGenerator")
|
||||
self.remove_air_defenses("SA23Generator")
|
||||
self.remove_air_defenses("SA17Generator")
|
||||
self.remove_air_defenses("KS19Generator")
|
||||
return self
|
||||
|
||||
def remove_aircraft(self, name):
|
||||
for i in self.aircrafts:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.aircrafts.remove(i)
|
||||
|
||||
def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(
|
||||
name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]
|
||||
))
|
||||
def remove_air_defenses(self, name):
|
||||
for i in self.air_defenses:
|
||||
if i == name:
|
||||
self.air_defenses.remove(i)
|
||||
|
||||
|
||||
def load_all_aircraft(data) -> List[Type[FlyingType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_aircraft(name)
|
||||
if item is not None:
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(
|
||||
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
|
||||
))
|
||||
|
||||
|
||||
def load_all_vehicles(data) -> List[Type[VehicleType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_vehicle(name)
|
||||
if item is not None:
|
||||
items.append(item)
|
||||
return items
|
||||
def remove_vehicle(self, name):
|
||||
for i in self.frontline_units:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.frontline_units.remove(i)
|
||||
|
||||
|
||||
def load_ship(name: str) -> Optional[Type[ShipType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(name, [dcs.ships]))
|
||||
if (ship := getattr(dcs.ships, name, None)) is not None:
|
||||
return ship
|
||||
logging.error(f"FACTION ERROR : Unable to find {name} in dcs.ships")
|
||||
return None
|
||||
|
||||
|
||||
def load_all_ships(data) -> List[Type[ShipType]]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
461
game/game.py
461
game/game.py
@@ -1,34 +1,47 @@
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Dict, List
|
||||
from typing import Any, List
|
||||
|
||||
from dcs.action import Coalition
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike
|
||||
from dcs.vehicles import AirDefence
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
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 gen import aircraft, naming
|
||||
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 . 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 .procurement import ProcurementAi
|
||||
from .settings import Settings
|
||||
from .theater import ConflictTheater, ControlPoint
|
||||
from .navmesh import NavMesh
|
||||
from .procurement import AircraftProcurementRequest, ProcurementAi
|
||||
from .profiling import logged_duration
|
||||
from .settings import Settings, AutoAtoBehavior
|
||||
from .squadrons import 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
|
||||
|
||||
@@ -65,68 +78,117 @@ AWACS_BUDGET_COST = 4
|
||||
# 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, player_budget: int,
|
||||
enemy_budget: int) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
player_faction: Faction,
|
||||
enemy_faction: Faction,
|
||||
theater: ConflictTheater,
|
||||
start_date: datetime,
|
||||
settings: Settings,
|
||||
player_budget: float,
|
||||
enemy_budget: float,
|
||||
) -> None:
|
||||
self.settings = settings
|
||||
self.events: List[Event] = []
|
||||
self.theater = theater
|
||||
self.player_name = player_name
|
||||
self.player_country = db.FACTIONS[player_name].country
|
||||
self.enemy_name = enemy_name
|
||||
self.enemy_country = db.FACTIONS[enemy_name].country
|
||||
self.turn = 0
|
||||
self.player_faction = player_faction
|
||||
self.player_country = player_faction.country
|
||||
self.enemy_faction = enemy_faction
|
||||
self.enemy_country = enemy_faction.country
|
||||
# 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.ground_planners: dict[int, GroundPlanner] = {}
|
||||
self.informations = []
|
||||
self.informations.append(Information("Game Start", "-" * 40, 0))
|
||||
self.__culling_points: List[Point] = []
|
||||
# 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
|
||||
self.enemy_budget = enemy_budget
|
||||
self.current_unit_id = 0
|
||||
self.current_group_id = 0
|
||||
self.name_generator = naming.namegen
|
||||
|
||||
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))
|
||||
|
||||
for cp in self.theater.controlpoints:
|
||||
cp.pending_unit_deliveries = self.units_delivery_event(cp)
|
||||
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()
|
||||
|
||||
# Turn 0 procurement. We don't actually have any missions to plan, but
|
||||
# the planner will tell us what it would like to plan so we can use that
|
||||
# to drive purchase decisions.
|
||||
blue_planner = CoalitionMissionPlanner(self, is_player=True)
|
||||
blue_planner.plan_missions()
|
||||
def ato_for(self, player: bool) -> AirTaskingOrder:
|
||||
if player:
|
||||
return self.blue_ato
|
||||
return self.red_ato
|
||||
|
||||
red_planner = CoalitionMissionPlanner(self, is_player=False)
|
||||
red_planner.plan_missions()
|
||||
def procurement_requests_for(
|
||||
self, player: bool
|
||||
) -> List[AircraftProcurementRequest]:
|
||||
if player:
|
||||
return self.blue_procurement_requests
|
||||
return self.red_procurement_requests
|
||||
|
||||
self.plan_procurement(blue_planner, red_planner)
|
||||
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):
|
||||
"""
|
||||
@@ -141,13 +203,30 @@ class Game:
|
||||
else:
|
||||
self.enemy_country = "Russia"
|
||||
|
||||
@property
|
||||
def player_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.player_name]
|
||||
def faction_for(self, player: bool) -> Faction:
|
||||
if player:
|
||||
return self.player_faction
|
||||
return self.enemy_faction
|
||||
|
||||
@property
|
||||
def enemy_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.enemy_name]
|
||||
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":
|
||||
@@ -157,58 +236,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_faction.name,
|
||||
self.enemy_faction.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) -> int:
|
||||
reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
|
||||
for cp in self.theater.player_points():
|
||||
for g in cp.ground_objects:
|
||||
if g.category in REWARDS.keys() and not g.is_dead:
|
||||
reward += REWARDS[g.category]
|
||||
return int(reward * self.settings.player_income_multiplier)
|
||||
def adjust_budget(self, amount: float, player: bool) -> None:
|
||||
if player:
|
||||
self.budget += amount
|
||||
else:
|
||||
self.enemy_budget += amount
|
||||
|
||||
def process_player_income(self):
|
||||
self.budget += self.budget_reward_amount
|
||||
self.budget += Income(self, player=True).total
|
||||
|
||||
def process_enemy_income(self):
|
||||
# TODO: Clean up save compat.
|
||||
if not hasattr(self, "enemy_budget"):
|
||||
self.enemy_budget = 0
|
||||
|
||||
production = 0.0
|
||||
for enemy_point in self.theater.enemy_points():
|
||||
for g in enemy_point.ground_objects:
|
||||
if g.category in REWARDS.keys() and not g.is_dead:
|
||||
production = production + REWARDS[g.category]
|
||||
|
||||
self.enemy_budget += production * self.settings.enemy_income_multiplier
|
||||
|
||||
def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent:
|
||||
event = UnitsDeliveryEvent(attacker_name=self.player_name,
|
||||
defender_name=self.player_name,
|
||||
from_cp=to_cp,
|
||||
to_cp=to_cp,
|
||||
game=self)
|
||||
self.events.append(event)
|
||||
return event
|
||||
self.enemy_budget += Income(self, player=False).total
|
||||
|
||||
def initiate_event(self, event: Event) -> UnitMap:
|
||||
#assert event in self.events
|
||||
# assert event in self.events
|
||||
logging.info("Generating {} (regular)".format(event))
|
||||
return event.generate()
|
||||
|
||||
def finish_event(self, event: Event, debriefing: Debriefing):
|
||||
logging.info("Finishing event {}".format(event))
|
||||
event.commit(debriefing)
|
||||
self.budget += int(event.bonus() *
|
||||
self.settings.player_income_multiplier)
|
||||
|
||||
if event in self.events:
|
||||
self.events.remove(event)
|
||||
@@ -217,86 +286,150 @@ 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_faction.name
|
||||
)
|
||||
else:
|
||||
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:
|
||||
if not hasattr(self, "name_generator"):
|
||||
self.name_generator = naming.namegen
|
||||
# Hack: Replace the global name generator state with the state from the save
|
||||
# game.
|
||||
#
|
||||
# We need to persist this state so that names generated after game load don't
|
||||
# conflict with those generated before exit.
|
||||
naming.namegen = self.name_generator
|
||||
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)
|
||||
|
||||
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 reset_ato(self) -> None:
|
||||
self.blue_ato.clear()
|
||||
self.red_ato.clear()
|
||||
|
||||
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()
|
||||
|
||||
# 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()
|
||||
|
||||
# Needs to happen *before* planning transfers so we don't cancel them.
|
||||
self.reset_ato()
|
||||
for control_point in self.theater.controlpoints:
|
||||
control_point.process_turn()
|
||||
control_point.process_turn(self)
|
||||
|
||||
self.process_enemy_income()
|
||||
self.blue_air_wing.replenish()
|
||||
self.red_air_wing.replenish()
|
||||
|
||||
self.process_player_income()
|
||||
|
||||
if not no_action and self.turn > 1:
|
||||
if not skipped:
|
||||
for cp in self.theater.player_points():
|
||||
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
|
||||
else:
|
||||
elif self.turn > 1:
|
||||
for cp in self.theater.player_points():
|
||||
if not cp.is_carrier and not cp.is_lha:
|
||||
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)
|
||||
|
||||
self.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):
|
||||
captured_states = {i.captured for i in self.theater.controlpoints}
|
||||
if True not in captured_states:
|
||||
player_airbases = {
|
||||
cp for cp in self.theater.player_points() if cp.runway_is_operational()
|
||||
}
|
||||
if not player_airbases:
|
||||
return TurnState.LOSS
|
||||
if False not in captured_states:
|
||||
|
||||
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:
|
||||
cp.pending_unit_deliveries = self.units_delivery_event(cp)
|
||||
self.aircraft_inventory.set_from_control_point(cp)
|
||||
|
||||
# Check for win or loss condition
|
||||
turn_state = self.check_win_loss()
|
||||
if turn_state in (TurnState.LOSS,TurnState.WIN):
|
||||
if turn_state in (TurnState.LOSS, TurnState.WIN):
|
||||
return self.process_win_loss(turn_state)
|
||||
|
||||
# Plan flights & combat for next turn
|
||||
self.compute_conflicts_position()
|
||||
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()
|
||||
|
||||
blue_planner = CoalitionMissionPlanner(self, is_player=True)
|
||||
blue_planner.plan_missions()
|
||||
self.blue_procurement_requests.clear()
|
||||
self.red_procurement_requests.clear()
|
||||
|
||||
red_planner = CoalitionMissionPlanner(self, is_player=False)
|
||||
red_planner.plan_missions()
|
||||
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:
|
||||
@@ -304,18 +437,23 @@ class Game:
|
||||
gplanner.plan_groundwar()
|
||||
self.ground_planners[cp.id] = gplanner
|
||||
|
||||
self.plan_procurement(blue_planner, red_planner)
|
||||
self.plan_procurement()
|
||||
|
||||
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
|
||||
|
||||
def plan_procurement(self, blue_planner: CoalitionMissionPlanner,
|
||||
red_planner: CoalitionMissionPlanner) -> None:
|
||||
self.budget = ProcurementAi(
|
||||
self,
|
||||
for_player=True,
|
||||
faction=self.player_faction,
|
||||
manage_runways=self.settings.automate_runway_repair,
|
||||
manage_front_line=self.settings.automate_front_line_reinforcements,
|
||||
manage_aircraft=self.settings.automate_aircraft_reinforcements
|
||||
).spend_budget(self.budget, blue_planner.procurement_requests)
|
||||
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
||||
).spend_budget(self.budget)
|
||||
|
||||
self.enemy_budget = ProcurementAi(
|
||||
self,
|
||||
@@ -323,8 +461,8 @@ class Game:
|
||||
faction=self.enemy_faction,
|
||||
manage_runways=True,
|
||||
manage_front_line=True,
|
||||
manage_aircraft=True
|
||||
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
|
||||
manage_aircraft=True,
|
||||
).spend_budget(self.enemy_budget)
|
||||
|
||||
def message(self, text: str) -> None:
|
||||
self.informations.append(Information(text, turn=self.turn))
|
||||
@@ -351,30 +489,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(front_line.control_point_a,
|
||||
front_line.control_point_b,
|
||||
self.theater)
|
||||
points.append(position[0])
|
||||
points.append(front_line.control_point_a.position)
|
||||
points.append(front_line.control_point_b.position)
|
||||
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)
|
||||
|
||||
# If do_not_cull_carrier is enabled, add carriers as culling point
|
||||
if self.settings.perf_do_not_cull_carrier:
|
||||
for cp in self.theater.controlpoints:
|
||||
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:
|
||||
points.append(cp.position)
|
||||
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():
|
||||
@@ -382,26 +545,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)
|
||||
|
||||
for package in self.blue_ato.packages:
|
||||
points.append(package.target.position)
|
||||
for package in self.red_ato.packages:
|
||||
points.append(package.target.position)
|
||||
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))
|
||||
|
||||
self.__culling_points = points
|
||||
self.__culling_zones = zones
|
||||
|
||||
def add_destroyed_units(self, data):
|
||||
pos = Point(data["x"], data["z"])
|
||||
@@ -417,20 +589,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):
|
||||
@@ -453,6 +624,10 @@ class Game:
|
||||
|
||||
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.")
|
||||
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.")
|
||||
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,7 +1,7 @@
|
||||
import datetime
|
||||
|
||||
class Information():
|
||||
|
||||
class Information:
|
||||
def __init__(self, title="", text="", turn=0):
|
||||
self.title = title
|
||||
self.text = text
|
||||
@@ -9,9 +9,11 @@ class Information():
|
||||
self.timestamp = datetime.datetime.now()
|
||||
|
||||
def __str__(self):
|
||||
return '[{}][{}] {} {}'.format(
|
||||
self.timestamp.strftime("%Y-%m-%d %H:%M:%S") if self.timestamp is not None else '',
|
||||
return "[{}][{}] {} {}".format(
|
||||
self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if self.timestamp is not None
|
||||
else "",
|
||||
self.turn,
|
||||
self.title,
|
||||
self.text
|
||||
)
|
||||
self.text,
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -17,9 +18,9 @@ class ControlPointAircraftInventory:
|
||||
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
self.control_point = control_point
|
||||
self.inventory: Dict[Type[FlyingType], int] = defaultdict(int)
|
||||
self.inventory: Dict[AircraftType, int] = defaultdict(int)
|
||||
|
||||
def add_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
|
||||
def add_aircraft(self, aircraft: AircraftType, count: int) -> None:
|
||||
"""Adds aircraft to the inventory.
|
||||
|
||||
Args:
|
||||
@@ -28,7 +29,7 @@ class ControlPointAircraftInventory:
|
||||
"""
|
||||
self.inventory[aircraft] += count
|
||||
|
||||
def remove_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
|
||||
def remove_aircraft(self, aircraft: AircraftType, count: int) -> None:
|
||||
"""Removes aircraft from the inventory.
|
||||
|
||||
Args:
|
||||
@@ -42,12 +43,12 @@ class ControlPointAircraftInventory:
|
||||
available = self.inventory[aircraft]
|
||||
if available < count:
|
||||
raise ValueError(
|
||||
f"Cannot remove {count} {aircraft.id} from "
|
||||
f"Cannot remove {count} {aircraft} from "
|
||||
f"{self.control_point.name}. Only have {available}."
|
||||
)
|
||||
self.inventory[aircraft] -= count
|
||||
|
||||
def available(self, aircraft: Type[FlyingType]) -> int:
|
||||
def available(self, aircraft: AircraftType) -> int:
|
||||
"""Returns the number of available aircraft of the given type.
|
||||
|
||||
Args:
|
||||
@@ -59,14 +60,14 @@ class ControlPointAircraftInventory:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def types_available(self) -> Iterator[Type[FlyingType]]:
|
||||
def types_available(self) -> Iterator[AircraftType]:
|
||||
"""Iterates over all available aircraft types."""
|
||||
for aircraft, count in self.inventory.items():
|
||||
if count > 0:
|
||||
yield aircraft
|
||||
|
||||
@property
|
||||
def all_aircraft(self) -> Iterator[Tuple[Type[FlyingType], int]]:
|
||||
def all_aircraft(self) -> Iterator[Tuple[AircraftType, int]]:
|
||||
"""Iterates over all available aircraft types, including amounts."""
|
||||
for aircraft, count in self.inventory.items():
|
||||
if count > 0:
|
||||
@@ -79,6 +80,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
|
||||
@@ -100,15 +102,15 @@ 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[Type[FlyingType]]:
|
||||
def available_types_for_player(self) -> Iterator[AircraftType]:
|
||||
"""Iterates over all aircraft types available to the player."""
|
||||
seen: Set[Type[FlyingType]] = set()
|
||||
seen: Set[AircraftType] = set()
|
||||
for control_point, inventory in self.inventories.items():
|
||||
if control_point.captured:
|
||||
for aircraft in inventory.types_available:
|
||||
|
||||
@@ -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 game.theater import ControlPoint
|
||||
|
||||
|
||||
class FrontlineData:
|
||||
"""
|
||||
This Data structure will store information about an existing frontline
|
||||
"""
|
||||
|
||||
def __init__(self, from_cp:ControlPoint, to_cp: ControlPoint):
|
||||
self.to_cp = to_cp
|
||||
self.from_cp = from_cp
|
||||
self.enemy_units_position = []
|
||||
self.blue_units_position = []
|
||||
@@ -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,10 +1,9 @@
|
||||
from __future__ import annotations
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Iterable, List, Optional, Set
|
||||
from typing import Iterable, List, Set, TYPE_CHECKING
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.action import DoScript, DoScriptFile
|
||||
@@ -14,25 +13,28 @@ from dcs.lua.parse import loads
|
||||
from dcs.mapping import Point
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import TriggerStart
|
||||
|
||||
from game.plugins import LuaPluginManager
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
from gen import Conflict, FlightType, VisualGenerator
|
||||
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
|
||||
from gen.aircraft import AircraftConflictGenerator, FlightData
|
||||
from gen.airfields import AIRFIELD_DATA
|
||||
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
|
||||
from gen.armor import GroundConflictGenerator, JtacInfo
|
||||
from gen.beacons import load_beacons_for_terrain
|
||||
from gen.briefinggen import BriefingGenerator, MissionInfoGenerator
|
||||
from gen.cargoshipgen import CargoShipGenerator
|
||||
from gen.convoygen import ConvoyGenerator
|
||||
from gen.environmentgen import EnvironmentGenerator
|
||||
from gen.forcedoptionsgen import ForcedOptionsGenerator
|
||||
from gen.groundobjectsgen import GroundObjectsGenerator
|
||||
from gen.kneeboard import KneeboardGenerator
|
||||
from gen.naming import namegen
|
||||
from gen.radios import RadioFrequency, RadioRegistry
|
||||
from gen.tacan import TacanRegistry
|
||||
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
|
||||
|
||||
from .. import db
|
||||
from ..debriefing import Debriefing
|
||||
from ..theater import Airfield
|
||||
from ..theater import Airfield, FrontLine
|
||||
from ..unitmap import UnitMap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -41,18 +43,14 @@ if TYPE_CHECKING:
|
||||
|
||||
class Operation:
|
||||
"""Static class for managing the final Mission generation"""
|
||||
current_mission = None # type: Mission
|
||||
airgen = None # type: AircraftConflictGenerator
|
||||
triggersgen = None # type: TriggersGenerator
|
||||
airsupportgen = None # type: AirSupportConflictGenerator
|
||||
visualgen = None # type: VisualGenerator
|
||||
groundobjectgen = None # type: GroundObjectsGenerator
|
||||
briefinggen = None # type: BriefingGenerator
|
||||
forcedoptionsgen = None # type: ForcedOptionsGenerator
|
||||
radio_registry: Optional[RadioRegistry] = None
|
||||
tacan_registry: Optional[TacanRegistry] = None
|
||||
game = None # type: Game
|
||||
environment_settings = None
|
||||
|
||||
current_mission: Mission
|
||||
airgen: AircraftConflictGenerator
|
||||
airsupportgen: AirSupportConflictGenerator
|
||||
groundobjectgen: GroundObjectsGenerator
|
||||
radio_registry: RadioRegistry
|
||||
tacan_registry: TacanRegistry
|
||||
game: Game
|
||||
trigger_radius = TRIGGER_RADIUS_MEDIUM
|
||||
is_quick = None
|
||||
player_awacs_enabled = True
|
||||
@@ -78,32 +76,30 @@ class Operation:
|
||||
for frontline in cls.game.theater.conflicts():
|
||||
yield Conflict(
|
||||
cls.game.theater,
|
||||
frontline.control_point_a,
|
||||
frontline.control_point_b,
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
frontline,
|
||||
cls.game.player_faction.name,
|
||||
cls.game.enemy_faction.name,
|
||||
cls.game.player_country,
|
||||
cls.game.enemy_country,
|
||||
frontline.position
|
||||
frontline.position,
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def air_conflict(cls) -> Conflict:
|
||||
assert cls.game
|
||||
player_cp, enemy_cp = cls.game.theater.closest_opposing_control_points()
|
||||
mid_point = player_cp.position.point_from_heading(
|
||||
player_cp.position.heading_between_point(enemy_cp.position),
|
||||
player_cp.position.distance_to_point(enemy_cp.position) / 2
|
||||
player_cp.position.distance_to_point(enemy_cp.position) / 2,
|
||||
)
|
||||
return Conflict(
|
||||
cls.game.theater,
|
||||
player_cp,
|
||||
enemy_cp,
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
FrontLine(player_cp, enemy_cp),
|
||||
cls.game.player_faction.name,
|
||||
cls.game.enemy_faction.name,
|
||||
cls.game.player_country,
|
||||
cls.game.enemy_country,
|
||||
mid_point
|
||||
mid_point,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -112,15 +108,21 @@ class Operation:
|
||||
|
||||
@classmethod
|
||||
def _setup_mission_coalitions(cls):
|
||||
cls.current_mission.coalition["blue"] = Coalition("blue")
|
||||
cls.current_mission.coalition["red"] = Coalition("red")
|
||||
cls.current_mission.coalition["blue"] = Coalition(
|
||||
"blue", bullseye=cls.game.blue_bullseye.to_pydcs()
|
||||
)
|
||||
cls.current_mission.coalition["red"] = Coalition(
|
||||
"red", bullseye=cls.game.red_bullseye.to_pydcs()
|
||||
)
|
||||
|
||||
p_country = cls.game.player_country
|
||||
e_country = cls.game.enemy_country
|
||||
cls.current_mission.coalition["blue"].add_country(
|
||||
country_dict[db.country_id_from_name(p_country)]())
|
||||
country_dict[db.country_id_from_name(p_country)]()
|
||||
)
|
||||
cls.current_mission.coalition["red"].add_country(
|
||||
country_dict[db.country_id_from_name(e_country)]())
|
||||
country_dict[db.country_id_from_name(e_country)]()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def inject_lua_trigger(cls, contents: str, comment: str) -> None:
|
||||
@@ -133,12 +135,11 @@ class Operation:
|
||||
cls.plugin_scripts.append(mnemonic)
|
||||
|
||||
@classmethod
|
||||
def inject_plugin_script(cls, plugin_mnemonic: str, script: str,
|
||||
script_mnemonic: str) -> None:
|
||||
def inject_plugin_script(
|
||||
cls, plugin_mnemonic: str, script: str, script_mnemonic: str
|
||||
) -> None:
|
||||
if script_mnemonic in cls.plugin_scripts:
|
||||
logging.debug(
|
||||
f"Skipping already loaded {script} for {plugin_mnemonic}"
|
||||
)
|
||||
logging.debug(f"Skipping already loaded {script} for {plugin_mnemonic}")
|
||||
else:
|
||||
cls.plugin_scripts.append(script_mnemonic)
|
||||
|
||||
@@ -146,15 +147,12 @@ class Operation:
|
||||
|
||||
script_path = Path(plugin_path, script)
|
||||
if not script_path.exists():
|
||||
logging.error(
|
||||
f"Cannot find {script_path} for plugin {plugin_mnemonic}"
|
||||
)
|
||||
logging.error(f"Cannot find {script_path} for plugin {plugin_mnemonic}")
|
||||
return
|
||||
|
||||
trigger = TriggerStart(comment=f"Load {script_mnemonic}")
|
||||
filename = script_path.resolve()
|
||||
fileref = cls.current_mission.map_resource.add_resource_file(
|
||||
filename)
|
||||
fileref = cls.current_mission.map_resource.add_resource_file(filename)
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
cls.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
@@ -166,25 +164,27 @@ class Operation:
|
||||
jtacs: List[JtacInfo],
|
||||
airgen: AircraftConflictGenerator,
|
||||
):
|
||||
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)
|
||||
"""
|
||||
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
|
||||
|
||||
gens: List[MissionInfoGenerator] = [
|
||||
KneeboardGenerator(cls.current_mission, cls.game),
|
||||
BriefingGenerator(cls.current_mission, cls.game)
|
||||
BriefingGenerator(cls.current_mission, cls.game),
|
||||
]
|
||||
for gen in gens:
|
||||
for dynamic_runway in groundobjectgen.runways.values():
|
||||
gen.add_dynamic_runway(dynamic_runway)
|
||||
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
gen.add_tanker(tanker)
|
||||
if tanker.blue:
|
||||
gen.add_tanker(tanker)
|
||||
|
||||
if cls.player_awacs_enabled:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
gen.add_awacs(awacs)
|
||||
for aewc in airsupportgen.air_support.awacs:
|
||||
if aewc.blue:
|
||||
gen.add_awacs(aewc)
|
||||
|
||||
for jtac in jtacs:
|
||||
gen.add_jtac(jtac)
|
||||
if jtac.blue:
|
||||
gen.add_jtac(jtac)
|
||||
|
||||
for flight in airgen.flights:
|
||||
gen.add_flight(flight)
|
||||
@@ -199,38 +199,28 @@ class Operation:
|
||||
|
||||
@classmethod
|
||||
def create_radio_registries(cls) -> None:
|
||||
unique_map_frequencies = set() # type: Set[RadioFrequency]
|
||||
unique_map_frequencies: Set[RadioFrequency] = set()
|
||||
cls._create_tacan_registry(unique_map_frequencies)
|
||||
cls._create_radio_registry(unique_map_frequencies)
|
||||
|
||||
assert cls.radio_registry is not None
|
||||
for frequency in unique_map_frequencies:
|
||||
cls.radio_registry.reserve(frequency)
|
||||
|
||||
@classmethod
|
||||
def assign_channels_to_flights(cls, flights: List[FlightData],
|
||||
air_support: AirSupport) -> None:
|
||||
def assign_channels_to_flights(
|
||||
cls, flights: List[FlightData], air_support: AirSupport
|
||||
) -> None:
|
||||
"""Assigns preset radio channels for client flights."""
|
||||
for flight in flights:
|
||||
if not flight.client_units:
|
||||
continue
|
||||
cls.assign_channels_to_flight(flight, air_support)
|
||||
|
||||
@staticmethod
|
||||
def assign_channels_to_flight(flight: FlightData,
|
||||
air_support: AirSupport) -> None:
|
||||
"""Assigns preset radio channels for a client flight."""
|
||||
airframe = flight.aircraft_type
|
||||
|
||||
try:
|
||||
aircraft_data = AIRCRAFT_DATA[airframe.id]
|
||||
except KeyError:
|
||||
logging.warning(f"No aircraft data for {airframe.id}")
|
||||
return
|
||||
|
||||
if aircraft_data.channel_allocator is not None:
|
||||
aircraft_data.channel_allocator.assign_channels_for_flight(
|
||||
flight, air_support
|
||||
)
|
||||
flight.aircraft_type.assign_channels_for_flight(flight, air_support)
|
||||
|
||||
@classmethod
|
||||
def _create_tacan_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None:
|
||||
def _create_tacan_registry(
|
||||
cls, unique_map_frequencies: Set[RadioFrequency]
|
||||
) -> None:
|
||||
"""
|
||||
Dedup beacon/radio frequencies, since some maps have some frequencies
|
||||
used multiple times.
|
||||
@@ -242,13 +232,14 @@ class Operation:
|
||||
unique_map_frequencies.add(beacon.frequency)
|
||||
if beacon.is_tacan:
|
||||
if beacon.channel is None:
|
||||
logging.error(
|
||||
f"TACAN beacon has no channel: {beacon.callsign}")
|
||||
logging.error(f"TACAN beacon has no channel: {beacon.callsign}")
|
||||
else:
|
||||
cls.tacan_registry.reserve(beacon.tacan_channel)
|
||||
|
||||
@classmethod
|
||||
def _create_radio_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None:
|
||||
def _create_radio_registry(
|
||||
cls, unique_map_frequencies: Set[RadioFrequency]
|
||||
) -> None:
|
||||
cls.radio_registry = RadioRegistry()
|
||||
for data in AIRFIELD_DATA.values():
|
||||
if data.theater == cls.game.theater.terrain.name and data.atc:
|
||||
@@ -256,8 +247,8 @@ class Operation:
|
||||
unique_map_frequencies.add(data.atc.vhf_fm)
|
||||
unique_map_frequencies.add(data.atc.vhf_am)
|
||||
unique_map_frequencies.add(data.atc.uhf)
|
||||
# No need to reserve ILS or TACAN because those are in the
|
||||
# beacon list.
|
||||
# No need to reserve ILS or TACAN because those are in the
|
||||
# beacon list.
|
||||
|
||||
@classmethod
|
||||
def _generate_ground_units(cls):
|
||||
@@ -266,7 +257,7 @@ class Operation:
|
||||
cls.game,
|
||||
cls.radio_registry,
|
||||
cls.tacan_registry,
|
||||
cls.unit_map
|
||||
cls.unit_map,
|
||||
)
|
||||
cls.groundobjectgen.generate()
|
||||
|
||||
@@ -280,10 +271,13 @@ class Operation:
|
||||
continue
|
||||
|
||||
pos = Point(d["x"], d["z"])
|
||||
if utype is not None and not cls.game.position_culled(pos) and cls.game.settings.perf_destroyed_units:
|
||||
if (
|
||||
utype is not None
|
||||
and not cls.game.position_culled(pos)
|
||||
and cls.game.settings.perf_destroyed_units
|
||||
):
|
||||
cls.current_mission.static_group(
|
||||
country=cls.current_mission.country(
|
||||
cls.game.player_country),
|
||||
country=cls.current_mission.country(cls.game.player_country),
|
||||
name="",
|
||||
_type=utype,
|
||||
hidden=True,
|
||||
@@ -291,20 +285,21 @@ class Operation:
|
||||
heading=d["orientation"],
|
||||
dead=True,
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def generate(cls) -> UnitMap:
|
||||
"""Build the final Mission to be exported"""
|
||||
cls.create_unit_map()
|
||||
cls.create_radio_registries()
|
||||
# Set mission time and weather conditions.
|
||||
EnvironmentGenerator(cls.current_mission,
|
||||
cls.game.conditions).generate()
|
||||
EnvironmentGenerator(cls.current_mission, cls.game.conditions).generate()
|
||||
cls._generate_ground_units()
|
||||
cls._generate_transports()
|
||||
cls._generate_destroyed_units()
|
||||
cls._generate_air_units()
|
||||
cls.assign_channels_to_flights(cls.airgen.flights,
|
||||
cls.airsupportgen.air_support)
|
||||
cls.assign_channels_to_flights(
|
||||
cls.airgen.flights, cls.airsupportgen.air_support
|
||||
)
|
||||
cls._generate_ground_conflicts()
|
||||
|
||||
# Triggers
|
||||
@@ -313,14 +308,11 @@ class Operation:
|
||||
|
||||
# Setup combined arms parameters
|
||||
cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0
|
||||
if cls.game.player_country in [country.name for country in cls.current_mission.coalition["blue"].countries.values()]:
|
||||
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
|
||||
else:
|
||||
cls.current_mission.groundControl.red_tactical_commander = cls.ca_slots
|
||||
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
|
||||
cls.current_mission.groundControl.blue_observer = 1
|
||||
|
||||
# Options
|
||||
forcedoptionsgen = ForcedOptionsGenerator(
|
||||
cls.current_mission, cls.game)
|
||||
forcedoptionsgen = ForcedOptionsGenerator(cls.current_mission, cls.game)
|
||||
forcedoptionsgen.generate()
|
||||
|
||||
# Generate Visuals Smoke Effects
|
||||
@@ -337,15 +329,13 @@ class Operation:
|
||||
plugin.inject_scripts(cls)
|
||||
plugin.inject_configuration(cls)
|
||||
|
||||
cls.assign_channels_to_flights(cls.airgen.flights,
|
||||
cls.airsupportgen.air_support)
|
||||
cls.notify_info_generators(
|
||||
cls.groundobjectgen,
|
||||
cls.airsupportgen,
|
||||
cls.jtacs,
|
||||
cls.airgen
|
||||
cls.assign_channels_to_flights(
|
||||
cls.airgen.flights, cls.airsupportgen.air_support
|
||||
)
|
||||
|
||||
cls.notify_info_generators(
|
||||
cls.groundobjectgen, cls.airsupportgen, cls.jtacs, cls.airgen
|
||||
)
|
||||
cls.reset_naming_ids()
|
||||
return cls.unit_map
|
||||
|
||||
@classmethod
|
||||
@@ -355,62 +345,89 @@ class Operation:
|
||||
# Air Support (Tanker & Awacs)
|
||||
assert cls.radio_registry and cls.tacan_registry
|
||||
cls.airsupportgen = AirSupportConflictGenerator(
|
||||
cls.current_mission, cls.air_conflict(), cls.game, cls.radio_registry,
|
||||
cls.tacan_registry)
|
||||
cls.current_mission,
|
||||
cls.air_conflict(),
|
||||
cls.game,
|
||||
cls.radio_registry,
|
||||
cls.tacan_registry,
|
||||
)
|
||||
cls.airsupportgen.generate()
|
||||
|
||||
# Generate Aircraft Activity on the map
|
||||
cls.airgen = AircraftConflictGenerator(
|
||||
cls.current_mission, cls.game.settings, cls.game,
|
||||
cls.radio_registry, cls.unit_map)
|
||||
cls.current_mission,
|
||||
cls.game.settings,
|
||||
cls.game,
|
||||
cls.radio_registry,
|
||||
cls.tacan_registry,
|
||||
cls.unit_map,
|
||||
air_support=cls.airsupportgen.air_support,
|
||||
)
|
||||
|
||||
cls.airgen.clear_parking_slots()
|
||||
|
||||
cls.airgen.generate_flights(
|
||||
cls.current_mission.country(cls.game.player_country),
|
||||
cls.game.blue_ato,
|
||||
cls.groundobjectgen.runways
|
||||
cls.groundobjectgen.runways,
|
||||
)
|
||||
cls.airgen.generate_flights(
|
||||
cls.current_mission.country(cls.game.enemy_country),
|
||||
cls.game.red_ato,
|
||||
cls.groundobjectgen.runways
|
||||
cls.groundobjectgen.runways,
|
||||
)
|
||||
cls.airgen.spawn_unused_aircraft(
|
||||
cls.current_mission.country(cls.game.player_country),
|
||||
cls.current_mission.country(cls.game.enemy_country))
|
||||
cls.current_mission.country(cls.game.enemy_country),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _generate_ground_conflicts(cls) -> None:
|
||||
"""For each frontline in the Operation, generate the ground conflicts and JTACs"""
|
||||
for front_line in cls.game.theater.conflicts(True):
|
||||
player_cp = front_line.control_point_a
|
||||
enemy_cp = front_line.control_point_b
|
||||
cls.jtacs = []
|
||||
for front_line in cls.game.theater.conflicts():
|
||||
player_cp = front_line.blue_cp
|
||||
enemy_cp = front_line.red_cp
|
||||
conflict = Conflict.frontline_cas_conflict(
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
cls.game.player_faction.name,
|
||||
cls.game.enemy_faction.name,
|
||||
cls.current_mission.country(cls.game.player_country),
|
||||
cls.current_mission.country(cls.game.enemy_country),
|
||||
player_cp,
|
||||
enemy_cp,
|
||||
cls.game.theater
|
||||
front_line,
|
||||
cls.game.theater,
|
||||
)
|
||||
# Generate frontline ops
|
||||
player_gp = cls.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id]
|
||||
enemy_gp = cls.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
|
||||
ground_conflict_gen = GroundConflictGenerator(
|
||||
cls.current_mission,
|
||||
conflict, cls.game,
|
||||
player_gp, enemy_gp,
|
||||
conflict,
|
||||
cls.game,
|
||||
player_gp,
|
||||
enemy_gp,
|
||||
player_cp.stances[enemy_cp.id],
|
||||
cls.unit_map
|
||||
cls.unit_map,
|
||||
)
|
||||
ground_conflict_gen.generate()
|
||||
cls.jtacs.extend(ground_conflict_gen.jtacs)
|
||||
|
||||
@classmethod
|
||||
def generate_lua(cls, airgen: AircraftConflictGenerator,
|
||||
airsupportgen: AirSupportConflictGenerator,
|
||||
jtacs: List[JtacInfo]) -> None:
|
||||
def _generate_transports(cls) -> None:
|
||||
"""Generates convoys for unit transfers by road."""
|
||||
ConvoyGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
|
||||
CargoShipGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
|
||||
|
||||
@classmethod
|
||||
def reset_naming_ids(cls):
|
||||
namegen.reset_numbers()
|
||||
|
||||
@classmethod
|
||||
def generate_lua(
|
||||
cls,
|
||||
airgen: AircraftConflictGenerator,
|
||||
airsupportgen: AirSupportConflictGenerator,
|
||||
jtacs: List[JtacInfo],
|
||||
) -> None:
|
||||
# TODO: Refactor this
|
||||
luaData = {
|
||||
"AircraftCarriers": {},
|
||||
@@ -418,39 +435,43 @@ class Operation:
|
||||
"AWACs": {},
|
||||
"JTACs": {},
|
||||
"TargetPoints": {},
|
||||
"RedAA": {},
|
||||
"BlueAA": {},
|
||||
} # type: ignore
|
||||
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
luaData["Tankers"][tanker.callsign] = {
|
||||
"dcsGroupName": tanker.dcsGroupName,
|
||||
"dcsGroupName": tanker.group_name,
|
||||
"callsign": tanker.callsign,
|
||||
"variant": tanker.variant,
|
||||
"radio": tanker.freq.mhz,
|
||||
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name
|
||||
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name,
|
||||
}
|
||||
|
||||
if airsupportgen.air_support.awacs:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
luaData["AWACs"][awacs.callsign] = {
|
||||
"dcsGroupName": awacs.dcsGroupName,
|
||||
"dcsGroupName": awacs.group_name,
|
||||
"callsign": awacs.callsign,
|
||||
"radio": awacs.freq.mhz
|
||||
"radio": awacs.freq.mhz,
|
||||
}
|
||||
|
||||
for jtac in jtacs:
|
||||
luaData["JTACs"][jtac.callsign] = {
|
||||
"dcsGroupName": jtac.dcsGroupName,
|
||||
"dcsGroupName": jtac.group_name,
|
||||
"callsign": jtac.callsign,
|
||||
"zone": jtac.region,
|
||||
"dcsUnit": jtac.unit_name,
|
||||
"laserCode": jtac.code
|
||||
"laserCode": jtac.code,
|
||||
}
|
||||
|
||||
for flight in airgen.flights:
|
||||
if flight.friendly and flight.flight_type in [FlightType.ANTISHIP,
|
||||
FlightType.DEAD,
|
||||
FlightType.SEAD,
|
||||
FlightType.STRIKE]:
|
||||
if flight.friendly and flight.flight_type in [
|
||||
FlightType.ANTISHIP,
|
||||
FlightType.DEAD,
|
||||
FlightType.SEAD,
|
||||
FlightType.STRIKE,
|
||||
]:
|
||||
flightType = str(flight.flight_type)
|
||||
flightTarget = flight.package.target
|
||||
if flightTarget:
|
||||
@@ -458,23 +479,47 @@ class Operation:
|
||||
flightTargetType = None
|
||||
if isinstance(flightTarget, TheaterGroundObject):
|
||||
flightTargetName = flightTarget.obj_name
|
||||
flightTargetType = flightType + \
|
||||
f" TGT ({flightTarget.category})"
|
||||
elif hasattr(flightTarget, 'name'):
|
||||
flightTargetType = (
|
||||
flightType + f" TGT ({flightTarget.category})"
|
||||
)
|
||||
elif hasattr(flightTarget, "name"):
|
||||
flightTargetName = flightTarget.name
|
||||
flightTargetType = flightType + " TGT (Airbase)"
|
||||
luaData["TargetPoints"][flightTargetName] = {
|
||||
"name": flightTargetName,
|
||||
"type": flightTargetType,
|
||||
"position": {"x": flightTarget.position.x,
|
||||
"y": flightTarget.position.y}
|
||||
"position": {
|
||||
"x": flightTarget.position.x,
|
||||
"y": flightTarget.position.y,
|
||||
},
|
||||
}
|
||||
|
||||
for cp in cls.game.theater.controlpoints:
|
||||
for ground_object in cp.ground_objects:
|
||||
if ground_object.might_have_aa and not ground_object.is_dead:
|
||||
for g in ground_object.groups:
|
||||
threat_range = ground_object.threat_range(g)
|
||||
|
||||
if not threat_range:
|
||||
continue
|
||||
|
||||
faction = "BlueAA" if cp.captured else "RedAA"
|
||||
|
||||
luaData[faction][g.name] = {
|
||||
"name": ground_object.name,
|
||||
"range": threat_range.meters,
|
||||
"position": {
|
||||
"x": ground_object.position.x,
|
||||
"y": ground_object.position.y,
|
||||
},
|
||||
}
|
||||
|
||||
# set a LUA table with data from Liberation that we want to set
|
||||
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
|
||||
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
|
||||
state_location = "[[" + os.path.abspath(".") + "]]"
|
||||
lua = """
|
||||
lua = (
|
||||
"""
|
||||
-- setting configuration table
|
||||
env.info("DCSLiberation|: setting configuration table")
|
||||
|
||||
@@ -482,9 +527,12 @@ class Operation:
|
||||
dcsLiberation = {}
|
||||
|
||||
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
|
||||
dcsLiberation.installPath=""" + state_location + """
|
||||
dcsLiberation.installPath="""
|
||||
+ state_location
|
||||
+ """
|
||||
|
||||
"""
|
||||
)
|
||||
# Process the tankers
|
||||
lua += """
|
||||
|
||||
@@ -530,8 +578,7 @@ class Operation:
|
||||
zone = data["zone"]
|
||||
laserCode = data["laserCode"]
|
||||
dcsUnit = data["dcsUnit"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone='{zone}', laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
|
||||
# lua += f" {{name='{dcsGroupName}', description='JTAC {callsign} ', information='Laser:{laserCode}', jtac={laserCode} }}, \n"
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone={repr(zone)}, laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the Target Points
|
||||
@@ -558,7 +605,33 @@ class Operation:
|
||||
-- list the aircraft carriers generated by Liberation
|
||||
-- dcsLiberation.Carriers = {}
|
||||
|
||||
-- later, we'll add more data to the table
|
||||
-- list the Red AA generated by Liberation
|
||||
dcsLiberation.RedAA = {
|
||||
"""
|
||||
for key in luaData["RedAA"]:
|
||||
data = luaData["RedAA"][key]
|
||||
name = data["name"]
|
||||
radius = data["range"]
|
||||
positionX = data["position"]["x"]
|
||||
positionY = data["position"]["y"]
|
||||
lua += f" {{dcsGroupName='{key}', name='{name}', range='{radius}', positionX='{positionX}', positionY='{positionY}' }}, \n"
|
||||
lua += "}"
|
||||
|
||||
lua += """
|
||||
|
||||
-- list the Blue AA generated by Liberation
|
||||
dcsLiberation.BlueAA = {
|
||||
"""
|
||||
for key in luaData["BlueAA"]:
|
||||
data = luaData["BlueAA"][key]
|
||||
name = data["name"]
|
||||
radius = data["range"]
|
||||
positionX = data["position"]["x"]
|
||||
positionY = data["position"]["y"]
|
||||
lua += f" {{dcsGroupName='{key}', name='{name}', range='{radius}', positionX='{positionX}', positionY='{positionY}' }}, \n"
|
||||
lua += "}"
|
||||
|
||||
lua += """
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@@ -2,16 +2,18 @@ import logging
|
||||
import os
|
||||
import pickle
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
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")
|
||||
if not save_dir().exists():
|
||||
save_dir().mkdir(parents=True)
|
||||
|
||||
|
||||
def base_path() -> str:
|
||||
@@ -20,16 +22,20 @@ def base_path() -> str:
|
||||
return _dcs_saved_game_folder
|
||||
|
||||
|
||||
def save_dir() -> Path:
|
||||
return Path(base_path()) / "Liberation" / "Saves"
|
||||
|
||||
|
||||
def _temporary_save_file() -> str:
|
||||
return os.path.join(base_path(), "tmpsave.liberation")
|
||||
return str(save_dir() / "tmpsave.liberation")
|
||||
|
||||
|
||||
def _autosave_path() -> str:
|
||||
return os.path.join(base_path(), "autosave.liberation")
|
||||
return str(save_dir() / "autosave.liberation")
|
||||
|
||||
|
||||
def mission_path_for(name: str) -> str:
|
||||
return os.path.join(base_path(), "Missions", "{}".format(name))
|
||||
return os.path.join(base_path(), "Missions", name)
|
||||
|
||||
|
||||
def load_game(path):
|
||||
@@ -67,4 +73,3 @@ def autosave(game) -> bool:
|
||||
except Exception:
|
||||
logging.exception("Could not save game")
|
||||
return False
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ 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
|
||||
@@ -26,8 +26,9 @@ class LuaPluginWorkOrder:
|
||||
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:
|
||||
@@ -155,12 +162,12 @@ class LuaPlugin(PluginSettings):
|
||||
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
|
||||
@@ -1,60 +1,134 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
import random
|
||||
from typing import Iterator, List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.task import CAP, CAS
|
||||
from dcs.unittype import FlyingType, UnitType, VehicleType
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
|
||||
|
||||
from game import db
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from gen.flights.ai_flight_planner_db import (
|
||||
capable_aircraft_for_task,
|
||||
preferred_aircraft_for_task,
|
||||
)
|
||||
from 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: int
|
||||
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:
|
||||
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 spend_budget(
|
||||
self, budget: int,
|
||||
aircraft_requests: List[AircraftProcurementRequest]) -> int:
|
||||
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 = math.ceil(budget / 2)
|
||||
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, aircraft_requests)
|
||||
budget = self.purchase_aircraft(budget)
|
||||
return budget
|
||||
|
||||
def repair_runways(self, budget: int) -> int:
|
||||
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 += aircraft.price
|
||||
return total
|
||||
|
||||
def repair_runways(self, budget: float) -> float:
|
||||
for control_point in self.owned_points:
|
||||
if budget < db.RUNWAY_REPAIR_COST:
|
||||
break
|
||||
@@ -63,92 +137,146 @@ class ProcurementAi:
|
||||
budget -= db.RUNWAY_REPAIR_COST
|
||||
if self.is_player:
|
||||
self.game.message(
|
||||
"OPFOR has begun repairing the runway at "
|
||||
f"{control_point}"
|
||||
"OPFOR has begun repairing the runway at " f"{control_point}"
|
||||
)
|
||||
else:
|
||||
self.game.message(
|
||||
"We have begun repairing the runway at "
|
||||
f"{control_point}"
|
||||
"We have begun repairing the runway at " f"{control_point}"
|
||||
)
|
||||
return budget
|
||||
|
||||
def random_affordable_ground_unit(
|
||||
self, budget: int) -> Optional[Type[VehicleType]]:
|
||||
affordable_units = [u for u in self.faction.frontline_units if
|
||||
db.PRICES[u] <= budget]
|
||||
def affordable_ground_unit_of_class(
|
||||
self, budget: float, unit_class: GroundUnitClass
|
||||
) -> Optional[GroundUnitType]:
|
||||
faction_units = set(self.faction.frontline_units) | set(
|
||||
self.faction.artillery_units
|
||||
)
|
||||
of_class = {u for u in faction_units if u.unit_class is unit_class}
|
||||
|
||||
# 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 u.price <= budget]
|
||||
if not affordable_units:
|
||||
return None
|
||||
return random.choice(affordable_units)
|
||||
|
||||
def reinforce_front_line(self, budget: int) -> int:
|
||||
if not self.faction.frontline_units:
|
||||
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:
|
||||
candidates = self.front_line_candidates()
|
||||
if not candidates:
|
||||
cp = self.ground_reinforcement_candidate()
|
||||
if cp is None:
|
||||
break
|
||||
|
||||
cp = random.choice(candidates)
|
||||
unit = self.random_affordable_ground_unit(budget)
|
||||
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]
|
||||
assert cp.pending_unit_deliveries is not None
|
||||
cp.pending_unit_deliveries.deliver({unit: 1})
|
||||
budget -= unit.price
|
||||
cp.pending_unit_deliveries.order({unit: 1})
|
||||
|
||||
return budget
|
||||
|
||||
def _affordable_aircraft_of_types(
|
||||
self, types: List[Type[FlyingType]], airbase: ControlPoint,
|
||||
number: int, max_price: int) -> Optional[Type[FlyingType]]:
|
||||
unit_pool = [u for u in self.faction.aircrafts if u in types]
|
||||
affordable_units = [
|
||||
u for u in unit_pool
|
||||
if db.PRICES[u] * number <= max_price and airbase.can_operate(u)
|
||||
]
|
||||
if not affordable_units:
|
||||
return None
|
||||
return random.choice(affordable_units)
|
||||
def 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[AircraftType]:
|
||||
best_choice: Optional[AircraftType] = None
|
||||
for unit in aircraft_for_task(task):
|
||||
if unit not in self.faction.aircrafts:
|
||||
continue
|
||||
if unit.price * 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: int) -> Optional[Type[FlyingType]]:
|
||||
aircraft = self._affordable_aircraft_of_types(
|
||||
preferred_aircraft_for_task(request.task_capability),
|
||||
airbase, request.number, budget)
|
||||
if aircraft is not None:
|
||||
return aircraft
|
||||
return self._affordable_aircraft_of_types(
|
||||
capable_aircraft_for_task(request.task_capability),
|
||||
airbase, request.number, budget)
|
||||
self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
|
||||
) -> Optional[AircraftType]:
|
||||
return self._affordable_aircraft_for_task(
|
||||
request.task_capability, airbase, request.number, budget
|
||||
)
|
||||
|
||||
def purchase_aircraft(
|
||||
self, budget: int,
|
||||
aircraft_requests: List[AircraftProcurementRequest]) -> int:
|
||||
unit_pool = [u for u in self.faction.aircrafts
|
||||
if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
|
||||
if not unit_pool:
|
||||
return budget
|
||||
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
|
||||
|
||||
for request in aircraft_requests:
|
||||
for airbase in self.best_airbases_for(request):
|
||||
unit = self.affordable_aircraft_for(request, airbase, budget)
|
||||
if unit is None:
|
||||
# Can't afford any aircraft capable of performing the
|
||||
# required mission that can operate from this airbase. We
|
||||
# might be able to afford aircraft at other airbases though,
|
||||
# in the case where the airbase we attempted to use is only
|
||||
# able to operate expensive aircraft.
|
||||
continue
|
||||
|
||||
budget -= db.PRICES[unit] * request.number
|
||||
assert airbase.pending_unit_deliveries is not None
|
||||
airbase.pending_unit_deliveries.deliver({unit: request.number})
|
||||
budget -= unit.price * 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
|
||||
@@ -159,36 +287,83 @@ class ProcurementAi:
|
||||
return self.game.theater.enemy_points()
|
||||
|
||||
def best_airbases_for(
|
||||
self,
|
||||
request: AircraftProcurementRequest) -> Iterator[ControlPoint]:
|
||||
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
|
||||
request.near
|
||||
)
|
||||
for cp in distance_cache.airfields_within(request.range):
|
||||
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 not cp.runway_is_operational():
|
||||
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 front_line_candidates(self) -> List[ControlPoint]:
|
||||
candidates = []
|
||||
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 cp.base.total_armor >= 30:
|
||||
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
|
||||
for connected in cp.connected_points:
|
||||
if not connected.is_friendly(to_player=self.is_player):
|
||||
candidates.append(cp)
|
||||
if allocated.total < worst_supply:
|
||||
worst_supply = allocated.total
|
||||
understaffed = cp
|
||||
|
||||
if not candidates:
|
||||
# Otherwise buy them anywhere valid.
|
||||
candidates = [p for p in self.owned_points
|
||||
if p.can_deploy_ground_units]
|
||||
if understaffed is not None:
|
||||
return understaffed
|
||||
|
||||
return candidates
|
||||
# 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 = unit_type.price * count
|
||||
total_cost += cost
|
||||
if unit_type.unit_class is 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)
|
||||
298
game/radio/channels.py
Normal file
298
game/radio/channels.py
Normal file
@@ -0,0 +1,298 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen import FlightData, AirSupport
|
||||
|
||||
|
||||
class RadioChannelAllocator:
|
||||
"""Base class for radio channel allocators."""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
"""Assigns mission frequencies to preset channels for the flight."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def from_cfg(cls, cfg: dict[str, Any]) -> RadioChannelAllocator:
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommonRadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Radio channel allocator suitable for most aircraft.
|
||||
|
||||
Most of the aircraft with preset channels available have one or more radios
|
||||
with 20 or more channels available (typically per-radio, but this is not the
|
||||
case for the JF-17).
|
||||
"""
|
||||
|
||||
#: Index of the radio used for intra-flight communications. Matches the
|
||||
#: index of the panel_radio field of the pydcs.dcs.planes object.
|
||||
inter_flight_radio_index: Optional[int]
|
||||
|
||||
#: Index of the radio used for intra-flight communications. Matches the
|
||||
#: index of the panel_radio field of the pydcs.dcs.planes object.
|
||||
intra_flight_radio_index: Optional[int]
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
if self.intra_flight_radio_index is not None:
|
||||
flight.assign_channel(
|
||||
self.intra_flight_radio_index, 1, flight.intra_flight_channel
|
||||
)
|
||||
|
||||
if self.inter_flight_radio_index is None:
|
||||
return
|
||||
|
||||
# For cases where the inter-flight and intra-flight radios share presets
|
||||
# (the JF-17 only has one set of channels, even though it can use two
|
||||
# channels simultaneously), start assigning inter-flight channels at 2.
|
||||
radio_id = self.inter_flight_radio_index
|
||||
if self.intra_flight_radio_index == radio_id:
|
||||
first_channel = 2
|
||||
else:
|
||||
first_channel = 1
|
||||
|
||||
last_channel = flight.num_radio_channels(radio_id)
|
||||
channel_alloc = iter(range(first_channel, last_channel + 1))
|
||||
|
||||
if flight.departure.atc is not None:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc)
|
||||
|
||||
# TODO: If there ever are multiple AWACS, limit to mission relevant.
|
||||
for awacs in air_support.awacs:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
|
||||
|
||||
if flight.arrival != flight.departure and flight.arrival.atc is not None:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), flight.arrival.atc)
|
||||
|
||||
try:
|
||||
# TODO: Skip incompatible tankers.
|
||||
for tanker in air_support.tankers:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), tanker.freq)
|
||||
|
||||
if flight.divert is not None and flight.divert.atc is not None:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), flight.divert.atc)
|
||||
except StopIteration:
|
||||
# Any remaining channels are nice-to-haves, but not necessary for
|
||||
# the few aircraft with a small number of channels available.
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def from_cfg(cls, cfg: dict[str, Any]) -> CommonRadioChannelAllocator:
|
||||
return CommonRadioChannelAllocator(
|
||||
inter_flight_radio_index=cfg["inter_flight_radio_index"],
|
||||
intra_flight_radio_index=cfg["intra_flight_radio_index"],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "common"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NoOpChannelAllocator(RadioChannelAllocator):
|
||||
"""Channel allocator for aircraft that don't support preset channels."""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "noop"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FarmerRadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Preset channel allocator for the MiG-19P."""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
# The Farmer only has 6 preset channels. It also only has a VHF radio,
|
||||
# and currently our ATC data and AWACS are only in the UHF band.
|
||||
radio_id = 1
|
||||
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
|
||||
# TODO: Assign 4-6 to VHF frequencies of departure, arrival, and divert.
|
||||
# TODO: Assign 2 and 3 to AWACS if it is VHF.
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "farmer"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ViggenRadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Preset channel allocator for the AJS37."""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
# The Viggen's preset channels are handled differently from other
|
||||
# aircraft. The aircraft automatically configures channels for every
|
||||
# allied flight in the game (including AWACS) and for every airfield. As
|
||||
# such, we don't need to allocate any of those. There are seven presets
|
||||
# we can modify, however: three channels for the main radio intended for
|
||||
# communication with wingmen, and four emergency channels for the backup
|
||||
# radio. We'll set the first channel of the main radio to the
|
||||
# intra-flight channel, and the first three emergency channels to each
|
||||
# of the flight plan's airfields. The fourth emergency channel is always
|
||||
# the guard channel.
|
||||
radio_id = 1
|
||||
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
|
||||
if flight.departure.atc is not None:
|
||||
flight.assign_channel(radio_id, 4, flight.departure.atc)
|
||||
if flight.arrival.atc is not None:
|
||||
flight.assign_channel(radio_id, 5, flight.arrival.atc)
|
||||
# TODO: Assign divert to 6 when we support divert airfields.
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "viggen"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SCR522RadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Preset channel allocator for the SCR522 WW2 radios. (4 channels)"""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
radio_id = 1
|
||||
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
|
||||
if flight.departure.atc is not None:
|
||||
flight.assign_channel(radio_id, 2, flight.departure.atc)
|
||||
if flight.arrival.atc is not None:
|
||||
flight.assign_channel(radio_id, 3, flight.arrival.atc)
|
||||
|
||||
# TODO : Some GCI on Channel 4 ?
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "SCR-522"
|
||||
|
||||
|
||||
class ChannelNamer:
|
||||
"""Base class allowing channel name customization per-aircraft.
|
||||
|
||||
Most aircraft will want to customize this behavior, but the default is
|
||||
reasonable for any aircraft with numbered radios.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
"""Returns the name of the channel for the given radio and channel."""
|
||||
return f"COMM{radio_id} Ch {channel_id}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "default"
|
||||
|
||||
|
||||
class SingleRadioChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the aircraft with only a single radio.
|
||||
|
||||
Aircraft like the MiG-19P and the MiG-21bis only have a single radio, so
|
||||
it's not necessary for us to name the radio when naming the channel.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
return f"Ch {channel_id}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "single"
|
||||
|
||||
|
||||
class HueyChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the UH-1H."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
return f"COM3 Ch {channel_id}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "huey"
|
||||
|
||||
|
||||
class MirageChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the M-2000."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
radio_name = ["V/UHF", "UHF"][radio_id - 1]
|
||||
return f"{radio_name} Ch {channel_id}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "mirage"
|
||||
|
||||
|
||||
class TomcatChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the F-14."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
radio_name = ["UHF", "VHF/UHF"][radio_id - 1]
|
||||
return f"{radio_name} Ch {channel_id}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "tomcat"
|
||||
|
||||
|
||||
class ViggenChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the AJS37."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
if channel_id >= 4:
|
||||
channel_letter = "EFGH"[channel_id - 4]
|
||||
return f"FR 24 {channel_letter}"
|
||||
return f"FR 22 Special {channel_id}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "viggen"
|
||||
|
||||
|
||||
class ViperChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the F-16."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
return f"COM{radio_id} Ch {channel_id}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "viper"
|
||||
|
||||
|
||||
class SCR522ChannelNamer(ChannelNamer):
|
||||
"""
|
||||
Channel namer for P-51 & P-47D
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
if channel_id > 3:
|
||||
return "?"
|
||||
else:
|
||||
return f"Button " + "ABCD"[channel_id - 1]
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "SCR-522"
|
||||
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
|
||||
@@ -1,15 +1,26 @@
|
||||
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:
|
||||
|
||||
# Difficulty settings
|
||||
player_skill: str = "Good"
|
||||
enemy_skill: str = "Average"
|
||||
ai_pilot_levelling: bool = True
|
||||
enemy_vehicle_skill: str = "Average"
|
||||
map_coalition_visibility: ForcedOptions.Views = ForcedOptions.Views.All
|
||||
labels: str = "Full"
|
||||
@@ -19,24 +30,48 @@ class Settings:
|
||||
supercarrier: bool = False
|
||||
generate_marks: bool = True
|
||||
manpads: bool = True
|
||||
cold_start: bool = False # Legacy parameter do not use
|
||||
version: Optional[str] = None
|
||||
player_income_multiplier: float = 1.0
|
||||
enemy_income_multiplier: float = 1.0
|
||||
|
||||
#: Feature flag for squadron limits.
|
||||
enable_squadron_pilot_limits: bool = False
|
||||
|
||||
#: The maximum number of pilots a squadron can have at one time. Changing this after
|
||||
#: the campaign has started will have no immediate effect; pilots already in the
|
||||
#: squadron will not be removed if the limit is lowered and pilots will not be
|
||||
#: immediately created if the limit is raised.
|
||||
squadron_pilot_limit: int = 12
|
||||
|
||||
#: The number of pilots a squadron can replace per turn.
|
||||
squadron_replenishment_rate: int = 4
|
||||
|
||||
default_start_type: str = "Cold"
|
||||
|
||||
# Mission specific
|
||||
desired_player_mission_duration: timedelta = timedelta(minutes=60)
|
||||
|
||||
# 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
|
||||
disable_legacy_tanker: 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
|
||||
|
||||
# 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_ai_parking_start: bool = True
|
||||
perf_destroyed_units: bool = True
|
||||
reserves_procurement_target: int = 10
|
||||
|
||||
# Performance culling
|
||||
perf_culling: bool = False
|
||||
@@ -48,6 +83,8 @@ class Settings:
|
||||
|
||||
# Cheating
|
||||
show_red_ato: bool = False
|
||||
enable_frontline_cheats: bool = False
|
||||
enable_base_capture_cheat: bool = False
|
||||
|
||||
never_delay_player_flights: bool = False
|
||||
|
||||
@@ -55,8 +92,7 @@ class Settings:
|
||||
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:
|
||||
|
||||
455
game/squadrons.py
Normal file
455
game/squadrons.py
Normal file
@@ -0,0 +1,455 @@
|
||||
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 (
|
||||
Tuple,
|
||||
TYPE_CHECKING,
|
||||
Optional,
|
||||
Iterator,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
import yaml
|
||||
from faker import Faker
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
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: AircraftType
|
||||
livery: Optional[str]
|
||||
mission_types: tuple[FlightType, ...]
|
||||
|
||||
#: The pool of pilots that have not yet been assigned to the squadron. This only
|
||||
#: happens when a preset squadron defines more preset pilots than the squadron limit
|
||||
#: allows. This pool will be consumed before random pilots are generated.
|
||||
pilot_pool: list[Pilot]
|
||||
|
||||
current_roster: list[Pilot] = field(default_factory=list, init=False, hash=False)
|
||||
available_pilots: list[Pilot] = field(
|
||||
default_factory=list, 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:
|
||||
if any(p.status is not PilotStatus.Active for p in self.pilot_pool):
|
||||
raise ValueError("Squadrons can only be created with active pilots.")
|
||||
self._recruit_pilots(self.game.settings.squadron_pilot_limit)
|
||||
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}"'
|
||||
|
||||
@property
|
||||
def pilot_limits_enabled(self) -> bool:
|
||||
return self.game.settings.enable_squadron_pilot_limits
|
||||
|
||||
def claim_new_pilot_if_allowed(self) -> Optional[Pilot]:
|
||||
if self.pilot_limits_enabled:
|
||||
return None
|
||||
self._recruit_pilots(1)
|
||||
return self.available_pilots.pop()
|
||||
|
||||
def claim_available_pilot(self) -> Optional[Pilot]:
|
||||
if not self.available_pilots:
|
||||
return self.claim_new_pilot_if_allowed()
|
||||
|
||||
# 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.
|
||||
#
|
||||
# If they only *prefer* players and we're out of players, just return an AI
|
||||
# pilot.
|
||||
if not prefer_players:
|
||||
return self.claim_new_pilot_if_allowed()
|
||||
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 _recruit_pilots(self, count: int) -> None:
|
||||
new_pilots = self.pilot_pool[:count]
|
||||
self.pilot_pool = self.pilot_pool[count:]
|
||||
count -= len(new_pilots)
|
||||
new_pilots.extend([Pilot(self.faker.name()) for _ in range(count)])
|
||||
self.current_roster.extend(new_pilots)
|
||||
self.available_pilots.extend(new_pilots)
|
||||
|
||||
def replenish_lost_pilots(self) -> None:
|
||||
if not self.pilot_limits_enabled:
|
||||
return
|
||||
|
||||
replenish_count = min(
|
||||
self.game.settings.squadron_replenishment_rate,
|
||||
self._number_of_unfilled_pilot_slots,
|
||||
)
|
||||
if replenish_count > 0:
|
||||
self._recruit_pilots(replenish_count)
|
||||
|
||||
def return_all_pilots(self) -> None:
|
||||
self.available_pilots = list(self.active_pilots)
|
||||
|
||||
@staticmethod
|
||||
def send_on_leave(pilot: Pilot) -> None:
|
||||
pilot.send_on_leave()
|
||||
|
||||
def return_from_leave(self, pilot: Pilot):
|
||||
if not self.has_unfilled_pilot_slots:
|
||||
raise RuntimeError(
|
||||
f"Cannot return {pilot} from leave because {self} is full"
|
||||
)
|
||||
pilot.return_from_leave()
|
||||
|
||||
@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.current_roster if p.status == status]
|
||||
|
||||
def _pilots_without_status(self, status: PilotStatus) -> list[Pilot]:
|
||||
return [p for p in self.current_roster 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_inactive(self) -> int:
|
||||
return len(self.current_roster)
|
||||
|
||||
@property
|
||||
def _number_of_unfilled_pilot_slots(self) -> int:
|
||||
return self.game.settings.squadron_pilot_limit - len(self.active_pilots)
|
||||
|
||||
@property
|
||||
def number_of_available_pilots(self) -> int:
|
||||
return len(self.available_pilots)
|
||||
|
||||
def can_provide_pilots(self, count: int) -> bool:
|
||||
return not self.pilot_limits_enabled or self.number_of_available_pilots >= count
|
||||
|
||||
@property
|
||||
def has_available_pilots(self) -> bool:
|
||||
return not self.pilot_limits_enabled or bool(self.available_pilots)
|
||||
|
||||
@property
|
||||
def has_unfilled_pilot_slots(self) -> bool:
|
||||
return not self.pilot_limits_enabled or self._number_of_unfilled_pilot_slots > 0
|
||||
|
||||
def can_auto_assign(self, task: FlightType) -> bool:
|
||||
return task in self.auto_assignable_mission_types
|
||||
|
||||
def pilot_at_index(self, index: int) -> Pilot:
|
||||
return self.current_roster[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(encoding="utf8") as squadron_file:
|
||||
data = yaml.safe_load(squadron_file)
|
||||
|
||||
name = data["aircraft"]
|
||||
try:
|
||||
unit_type = AircraftType.named(name)
|
||||
except KeyError as ex:
|
||||
raise KeyError(f"Could not find any aircraft named {name}") from ex
|
||||
|
||||
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),
|
||||
pilot_pool=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[AircraftType, list[Squadron]]:
|
||||
squadrons: dict[AircraftType, 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)),
|
||||
pilot_pool=[],
|
||||
game=game,
|
||||
player=player,
|
||||
)
|
||||
]
|
||||
|
||||
def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]:
|
||||
return self.squadrons[aircraft]
|
||||
|
||||
def can_auto_plan(self, task: FlightType) -> bool:
|
||||
try:
|
||||
next(self.auto_assignable_for_task(task))
|
||||
return True
|
||||
except StopIteration:
|
||||
return False
|
||||
|
||||
def auto_assignable_for_task(self, task: FlightType) -> Iterator[Squadron]:
|
||||
for squadron in self.iter_squadrons():
|
||||
if squadron.can_auto_assign(task):
|
||||
yield squadron
|
||||
|
||||
def auto_assignable_for_task_with_type(
|
||||
self, aircraft: AircraftType, task: FlightType
|
||||
) -> Iterator[Squadron]:
|
||||
for squadron in self.squadrons_for(aircraft):
|
||||
if squadron.can_auto_assign(task) and squadron.has_available_pilots:
|
||||
yield squadron
|
||||
|
||||
def squadron_for(self, aircraft: AircraftType) -> 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 replenish(self) -> None:
|
||||
for squadron in self.iter_squadrons():
|
||||
squadron.replenish_lost_pilots()
|
||||
|
||||
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
|
||||
@@ -1,5 +1,6 @@
|
||||
from .base import *
|
||||
from .conflicttheater import *
|
||||
from .controlpoint import *
|
||||
from .frontline import FrontLine
|
||||
from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import SamGroundObject
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
import itertools
|
||||
import logging
|
||||
import math
|
||||
import typing
|
||||
from typing import Dict, Type
|
||||
from typing import Any
|
||||
|
||||
from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task
|
||||
from dcs.unittype import FlyingType, UnitType, VehicleType
|
||||
from dcs.vehicles import AirDefence, Armor
|
||||
|
||||
from game import db
|
||||
|
||||
STRENGTH_AA_ASSEMBLE_MIN = 0.2
|
||||
PLANES_SCRAMBLE_MIN_BASE = 2
|
||||
PLANES_SCRAMBLE_MAX_BASE = 8
|
||||
PLANES_SCRAMBLE_FACTOR = 0.3
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.unittype import UnitType
|
||||
|
||||
BASE_MAX_STRENGTH = 1
|
||||
BASE_MIN_STRENGTH = 0
|
||||
|
||||
|
||||
class Base:
|
||||
|
||||
def __init__(self):
|
||||
self.aircraft: Dict[Type[FlyingType], int] = {}
|
||||
self.armor: Dict[Type[VehicleType], int] = {}
|
||||
self.aa: Dict[AirDefence, int] = {}
|
||||
self.commision_points: Dict[Type, float] = {}
|
||||
self.aircraft: dict[AircraftType, int] = {}
|
||||
self.armor: dict[GroundUnitType, int] = {}
|
||||
self.strength = 1
|
||||
|
||||
@property
|
||||
@@ -37,106 +25,55 @@ class Base:
|
||||
return sum(self.armor.values())
|
||||
|
||||
@property
|
||||
def total_aa(self) -> int:
|
||||
return sum(self.aa.values())
|
||||
def total_armor_value(self) -> int:
|
||||
total = 0
|
||||
for unit_type, count in self.armor.items():
|
||||
total += unit_type.price * count
|
||||
return total
|
||||
|
||||
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]])
|
||||
|
||||
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])
|
||||
|
||||
@property
|
||||
def all_units(self):
|
||||
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]:
|
||||
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.sort(key=lambda x: db.PRICES[x], reverse=True)
|
||||
|
||||
result: Dict[UnitType, int] = {}
|
||||
for unit_type in sorted_units:
|
||||
existing_count = available_units[unit_type] # type: int
|
||||
if not existing_count:
|
||||
continue
|
||||
|
||||
if count <= 0:
|
||||
break
|
||||
|
||||
result_unit_count = min(count, existing_count)
|
||||
count -= result_unit_count
|
||||
|
||||
assert result_unit_count > 0
|
||||
result[unit_type] = result.get(unit_type, 0) + result_unit_count
|
||||
|
||||
logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
|
||||
return result
|
||||
|
||||
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
|
||||
points = self.commision_points[for_type]
|
||||
if points >= 1:
|
||||
self.commision_points[for_type] = points - math.floor(points)
|
||||
return int(math.floor(points))
|
||||
|
||||
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.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)
|
||||
def total_units_of_type(self, unit_type: UnitType) -> int:
|
||||
return sum(
|
||||
[
|
||||
c
|
||||
for t, c in itertools.chain(self.aircraft.items(), self.armor.items())
|
||||
if t == unit_type
|
||||
]
|
||||
)
|
||||
|
||||
def commission_units(self, units: dict[Any, int]):
|
||||
for unit_type, unit_count in units.items():
|
||||
for_task = db.unit_task(unit_type)
|
||||
if unit_count <= 0:
|
||||
continue
|
||||
|
||||
target_dict = None
|
||||
if for_task == CAS or for_task == CAP or for_task == Embarking:
|
||||
target_dict: dict[Any, int]
|
||||
if isinstance(unit_type, AircraftType):
|
||||
target_dict = self.aircraft
|
||||
elif for_task == PinpointStrike:
|
||||
elif isinstance(unit_type, GroundUnitType):
|
||||
target_dict = self.armor
|
||||
elif for_task == AirDefence:
|
||||
target_dict = self.aa
|
||||
|
||||
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))
|
||||
logging.error(f"Unexpected unit type of {unit_type}")
|
||||
return
|
||||
|
||||
def commit_losses(self, units_lost: typing.Dict[typing.Any, int]):
|
||||
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
||||
|
||||
def commit_losses(self, units_lost: dict[Any, int]):
|
||||
for unit_type, count in units_lost.items():
|
||||
|
||||
target_dict: dict[Any, int]
|
||||
if unit_type in self.aircraft:
|
||||
target_array = self.aircraft
|
||||
target_dict = self.aircraft
|
||||
elif unit_type in self.armor:
|
||||
target_array = self.armor
|
||||
target_dict = self.armor
|
||||
else:
|
||||
print("Base didn't find event type {}".format(unit_type))
|
||||
continue
|
||||
|
||||
if unit_type not in target_array:
|
||||
if unit_type not in target_dict:
|
||||
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]
|
||||
|
||||
target_dict[unit_type] = max(target_dict[unit_type] - count, 0)
|
||||
if target_dict[unit_type] == 0:
|
||||
del target_dict[unit_type]
|
||||
|
||||
def affect_strength(self, amount):
|
||||
self.strength += amount
|
||||
@@ -147,43 +84,3 @@ class Base:
|
||||
|
||||
def set_strength_to_minimum(self) -> None:
|
||||
self.strength = BASE_MIN_STRENGTH
|
||||
|
||||
def scramble_count(self, multiplier: float, task: Task = None) -> int:
|
||||
if task:
|
||||
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task])
|
||||
else:
|
||||
count = self.total_aircraft
|
||||
|
||||
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
|
||||
return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count)
|
||||
|
||||
def assemble_count(self):
|
||||
return int(self.total_armor * 0.5)
|
||||
|
||||
def assemble_aa_count(self) -> int:
|
||||
# previous logic removed because we always want the full air defense capabilities.
|
||||
return self.total_aa
|
||||
|
||||
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_aircraft, 20))
|
||||
|
||||
def scramble_cas(self, multiplier: float) -> typing.Dict[FlyingType, int]:
|
||||
return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS))
|
||||
|
||||
def scramble_interceptors(self, multiplier: float) -> typing.Dict[FlyingType, int]:
|
||||
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
|
||||
|
||||
def assemble_attack(self) -> typing.Dict[Armor, int]:
|
||||
return self._find_best_armor(PinpointStrike, self.assemble_count())
|
||||
|
||||
def assemble_defense(self) -> typing.Dict[Armor, int]:
|
||||
count = int(self.total_armor * min(self.strength + 0.5, 1))
|
||||
return self._find_best_armor(PinpointStrike, count)
|
||||
|
||||
def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]:
|
||||
return self._find_best_unit(self.aa, AirDefence, count and min(count, self.total_aa) or self.assemble_aa_count())
|
||||
|
||||
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,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
180
game/theater/frontline.py
Normal file
180
game/theater/frontline.py
Normal file
@@ -0,0 +1,180 @@
|
||||
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,
|
||||
FlightType.REFUELING
|
||||
# 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
|
||||
@@ -1,11 +1,30 @@
|
||||
from dataclasses import dataclass
|
||||
import pickle
|
||||
from typing import Collection, Optional, Tuple
|
||||
from functools import cached_property
|
||||
from typing import Optional, Tuple, Union
|
||||
import logging
|
||||
|
||||
from shapely import geometry
|
||||
from shapely.geometry import MultiPolygon, Polygon
|
||||
|
||||
Zone = Collection[Tuple[float, float]]
|
||||
Landmap = Tuple[Collection[geometry.Polygon], Collection[geometry.Polygon], Collection[geometry.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]:
|
||||
@@ -17,7 +36,7 @@ def load_landmap(filename: str) -> Optional[Landmap]:
|
||||
return None
|
||||
|
||||
|
||||
def poly_contains(x, y, poly:geometry.Polygon):
|
||||
def poly_contains(x, y, poly: Union[MultiPolygon, Polygon]):
|
||||
return poly.contains(geometry.Point(x, y))
|
||||
|
||||
|
||||
@@ -27,4 +46,3 @@ def poly_centroid(poly) -> Tuple[float, float]:
|
||||
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),
|
||||
]
|
||||
)
|
||||
@@ -1,8 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterator, TYPE_CHECKING
|
||||
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
|
||||
@@ -29,15 +30,20 @@ class MissionTarget:
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
if self.is_friendly(for_player):
|
||||
yield FlightType.BARCAP
|
||||
else:
|
||||
yield from [
|
||||
FlightType.ESCORT,
|
||||
FlightType.TARCAP,
|
||||
FlightType.SEAD,
|
||||
FlightType.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",
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -1,12 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import pickle
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set
|
||||
from typing import Any, Dict, Iterable, List, Set
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike
|
||||
@@ -14,33 +13,34 @@ from dcs.vehicles import AirDefence
|
||||
|
||||
from game import Game, db
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import Carrier, Lha, LocationType
|
||||
from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
|
||||
from game.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.locations.preset_location_finder import MizDataLocationFinder
|
||||
from gen.missiles.missiles_group_generator import generate_missile_group
|
||||
from gen.sam.airdefensegroupgenerator import AirDefenseRange
|
||||
from gen.sam.sam_group_generator import (
|
||||
generate_anti_air_group,
|
||||
generate_ewr_group,
|
||||
)
|
||||
from gen.sam.ewr_group_generator import generate_ewr_group
|
||||
from gen.sam.sam_group_generator import generate_anti_air_group
|
||||
from . import (
|
||||
ConflictTheater,
|
||||
ControlPoint,
|
||||
@@ -48,6 +48,7 @@ from . import (
|
||||
Fob,
|
||||
OffMapSpawn,
|
||||
)
|
||||
from ..profiling import logged_duration
|
||||
from ..settings import Settings
|
||||
|
||||
GroundObjectTemplates = Dict[str, Dict[str, Any]]
|
||||
@@ -77,32 +78,52 @@ class GeneratorSettings:
|
||||
no_enemy_navy: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModSettings:
|
||||
a4_skyhawk: bool = False
|
||||
f22_raptor: bool = False
|
||||
hercules: bool = False
|
||||
jas39_gripen: bool = False
|
||||
su57_felon: bool = False
|
||||
frenchpack: bool = False
|
||||
high_digit_sams: bool = False
|
||||
|
||||
|
||||
class GameGenerator:
|
||||
def __init__(self, player: str, enemy: str, theater: ConflictTheater,
|
||||
settings: Settings,
|
||||
generator_settings: GeneratorSettings) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
player: Faction,
|
||||
enemy: Faction,
|
||||
theater: ConflictTheater,
|
||||
settings: Settings,
|
||||
generator_settings: GeneratorSettings,
|
||||
mod_settings: ModSettings,
|
||||
) -> None:
|
||||
self.player = player
|
||||
self.enemy = enemy
|
||||
self.theater = theater
|
||||
self.settings = settings
|
||||
self.generator_settings = generator_settings
|
||||
self.mod_settings = mod_settings
|
||||
|
||||
def generate(self) -> Game:
|
||||
# Reset name generator
|
||||
namegen.reset()
|
||||
self.prepare_theater()
|
||||
game = Game(
|
||||
player_name=self.player,
|
||||
enemy_name=self.enemy,
|
||||
theater=self.theater,
|
||||
start_date=self.generator_settings.start_date,
|
||||
settings=self.settings,
|
||||
player_budget=self.generator_settings.player_budget,
|
||||
enemy_budget=self.generator_settings.enemy_budget
|
||||
)
|
||||
with logged_duration("TGO population"):
|
||||
# Reset name generator
|
||||
namegen.reset()
|
||||
self.prepare_theater()
|
||||
game = Game(
|
||||
player_faction=self.player.apply_mod_settings(self.mod_settings),
|
||||
enemy_faction=self.enemy.apply_mod_settings(self.mod_settings),
|
||||
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()
|
||||
GroundObjectGenerator(game, self.generator_settings).generate()
|
||||
game.settings.version = VERSION
|
||||
game.begin_turn_0()
|
||||
return game
|
||||
|
||||
def prepare_theater(self) -> None:
|
||||
@@ -110,7 +131,7 @@ class GameGenerator:
|
||||
# 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]:
|
||||
for control_point in control_points[: len(control_points) // 2]:
|
||||
control_point.captured = True
|
||||
|
||||
# Remove carrier and lha, invert situation if needed
|
||||
@@ -137,163 +158,23 @@ class GameGenerator:
|
||||
cp.captured = True
|
||||
|
||||
|
||||
class LocationFinder:
|
||||
def __init__(self, game: Game, control_point: ControlPoint) -> None:
|
||||
self.game = game
|
||||
self.control_point = control_point
|
||||
self.miz_data = MizDataLocationFinder.compute_possible_locations(
|
||||
game.theater.terrain.name, control_point.full_name)
|
||||
|
||||
def location_for(self, location_type: LocationType) -> Optional[Point]:
|
||||
position = self.control_point.preset_locations.random_for(location_type)
|
||||
if position is not None:
|
||||
return position
|
||||
|
||||
logging.warning(f"No campaign location for %s at %s",
|
||||
location_type.value, self.control_point)
|
||||
position = self.random_from_miz_data(
|
||||
location_type == LocationType.OffshoreStrikeTarget)
|
||||
if position is not None:
|
||||
return position
|
||||
|
||||
logging.debug(f"No mizdata location for %s at %s", location_type.value,
|
||||
self.control_point)
|
||||
position = self.random_position(location_type)
|
||||
if position is not None:
|
||||
return position
|
||||
|
||||
logging.error(f"Could not find position for %s at %s",
|
||||
location_type.value, self.control_point)
|
||||
return None
|
||||
|
||||
def random_from_miz_data(self, offshore: bool) -> Optional[Point]:
|
||||
if offshore:
|
||||
locations = self.miz_data.offshore_locations
|
||||
else:
|
||||
locations = self.miz_data.ashore_locations
|
||||
if self.miz_data.offshore_locations:
|
||||
preset = random.choice(locations)
|
||||
locations.remove(preset)
|
||||
return preset.position
|
||||
return None
|
||||
|
||||
def random_position(self, location_type: LocationType) -> Optional[Point]:
|
||||
# TODO: Flesh out preset locations so we never hit this case.
|
||||
logging.warning("Falling back to random location for %s at %s",
|
||||
location_type.value, self.control_point)
|
||||
|
||||
is_base_defense = location_type in {
|
||||
LocationType.BaseAirDefense,
|
||||
LocationType.Garrison,
|
||||
LocationType.Shorad,
|
||||
}
|
||||
|
||||
on_land = location_type not in {
|
||||
LocationType.OffshoreStrikeTarget,
|
||||
LocationType.Ship,
|
||||
}
|
||||
|
||||
avoid_others = location_type not in {
|
||||
LocationType.Garrison,
|
||||
LocationType.MissileSite,
|
||||
LocationType.Sam,
|
||||
LocationType.Ship,
|
||||
LocationType.Shorad,
|
||||
}
|
||||
|
||||
if is_base_defense:
|
||||
min_range = 400
|
||||
max_range = 3200
|
||||
elif location_type == LocationType.Ship:
|
||||
min_range = 5000
|
||||
max_range = 40000
|
||||
elif location_type == LocationType.MissileSite:
|
||||
min_range = 2500
|
||||
max_range = 40000
|
||||
else:
|
||||
min_range = 10000
|
||||
max_range = 40000
|
||||
|
||||
position = self._find_random_position(min_range, max_range,
|
||||
on_land, is_base_defense,
|
||||
avoid_others)
|
||||
|
||||
# Retry once, searching a bit further (On some big airbases, 3200 is too
|
||||
# short (Ex : Incirlik)), but searching farther on every base would be
|
||||
# problematic, as some base defense units would end up very far away
|
||||
# from small airfields.
|
||||
if position is None and is_base_defense:
|
||||
position = self._find_random_position(3200, 4800,
|
||||
on_land, is_base_defense,
|
||||
avoid_others)
|
||||
return position
|
||||
|
||||
def _find_random_position(self, min_range: int, max_range: int,
|
||||
on_ground: bool, is_base_defense: bool,
|
||||
avoid_others: bool) -> Optional[Point]:
|
||||
"""
|
||||
Find a valid ground object location
|
||||
:param on_ground: Whether it should be on ground or on sea (True = on
|
||||
ground)
|
||||
:param min_range: Minimal range from point
|
||||
:param max_range: Max range from point
|
||||
:param is_base_defense: True if the location is for base defense.
|
||||
:return:
|
||||
"""
|
||||
near = self.control_point.position
|
||||
others = self.control_point.ground_objects
|
||||
|
||||
def is_valid(point: Optional[Point]) -> bool:
|
||||
if point is None:
|
||||
return False
|
||||
|
||||
if on_ground and not self.game.theater.is_on_land(point):
|
||||
return False
|
||||
elif not on_ground and not self.game.theater.is_in_sea(point):
|
||||
return False
|
||||
|
||||
if avoid_others:
|
||||
for other in others:
|
||||
if other.position.distance_to_point(point) < 10000:
|
||||
return False
|
||||
|
||||
if is_base_defense:
|
||||
# If it's a base defense we don't care how close it is to other
|
||||
# points.
|
||||
return True
|
||||
|
||||
# Else verify that it's not too close to another control point.
|
||||
for control_point in self.game.theater.controlpoints:
|
||||
if control_point != self.control_point:
|
||||
if control_point.position.distance_to_point(point) < 30000:
|
||||
return False
|
||||
for ground_obj in control_point.ground_objects:
|
||||
if ground_obj.position.distance_to_point(point) < 10000:
|
||||
return False
|
||||
return True
|
||||
|
||||
for _ in range(300):
|
||||
# Check if on land or sea
|
||||
p = near.random_point_within(max_range, min_range)
|
||||
if is_valid(p):
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
class ControlPointGroundObjectGenerator:
|
||||
def __init__(self, game: Game, generator_settings: GeneratorSettings,
|
||||
control_point: ControlPoint) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
generator_settings: GeneratorSettings,
|
||||
control_point: ControlPoint,
|
||||
) -> None:
|
||||
self.game = game
|
||||
self.generator_settings = generator_settings
|
||||
self.control_point = control_point
|
||||
self.location_finder = LocationFinder(game, control_point)
|
||||
|
||||
@property
|
||||
def faction_name(self) -> str:
|
||||
if self.control_point.captured:
|
||||
return self.game.player_name
|
||||
return self.game.player_faction.name
|
||||
else:
|
||||
return self.game.enemy_name
|
||||
return self.game.enemy_faction.name
|
||||
|
||||
@property
|
||||
def faction(self) -> Faction:
|
||||
@@ -302,10 +183,7 @@ class ControlPointGroundObjectGenerator:
|
||||
def generate(self) -> bool:
|
||||
self.control_point.connected_objectives = []
|
||||
if self.faction.navy_generators:
|
||||
# Even airbases can generate navies if they are close enough to the
|
||||
# water. This is not controlled by the control point definition, but
|
||||
# rather by whether or not the generator can find a valid position
|
||||
# for the ship.
|
||||
# Even airbases can generate navies if they are close enough to the water.
|
||||
self.generate_navy()
|
||||
|
||||
return True
|
||||
@@ -319,19 +197,15 @@ class ControlPointGroundObjectGenerator:
|
||||
if not self.control_point.captured and skip_enemy_navy:
|
||||
return
|
||||
|
||||
for _ in range(self.faction.navy_group_count):
|
||||
self.generate_ship()
|
||||
|
||||
def generate_ship(self) -> None:
|
||||
point = self.location_finder.location_for(
|
||||
LocationType.OffshoreStrikeTarget)
|
||||
if point is None:
|
||||
return
|
||||
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, point,
|
||||
self.control_point)
|
||||
g = ShipGroundObject(
|
||||
namegen.random_objective_name(), group_id, position, self.control_point
|
||||
)
|
||||
|
||||
group = generate_ship_group(self.game, g, self.faction_name)
|
||||
g.groups = []
|
||||
@@ -354,13 +228,15 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
if not carrier_names:
|
||||
logging.info(
|
||||
f"Skipping generation of {self.control_point.name} because "
|
||||
f"{self.faction_name} has no carriers")
|
||||
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)
|
||||
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:
|
||||
@@ -379,13 +255,15 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
if not lha_names:
|
||||
logging.info(
|
||||
f"Skipping generation of {self.control_point.name} because "
|
||||
f"{self.faction_name} has no LHAs")
|
||||
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)
|
||||
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:
|
||||
@@ -395,144 +273,14 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
return True
|
||||
|
||||
|
||||
class BaseDefenseGenerator:
|
||||
def __init__(self, game: Game, control_point: ControlPoint) -> None:
|
||||
self.game = game
|
||||
self.control_point = control_point
|
||||
self.location_finder = LocationFinder(game, control_point)
|
||||
|
||||
@property
|
||||
def faction_name(self) -> str:
|
||||
if self.control_point.captured:
|
||||
return self.game.player_name
|
||||
else:
|
||||
return self.game.enemy_name
|
||||
|
||||
@property
|
||||
def faction(self) -> Faction:
|
||||
return db.FACTIONS[self.faction_name]
|
||||
|
||||
def generate(self) -> None:
|
||||
self.generate_ewr()
|
||||
self.generate_garrison()
|
||||
self.generate_base_defenses()
|
||||
|
||||
def generate_ewr(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Ewr)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = EwrGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point)
|
||||
|
||||
group = generate_ewr_group(self.game, g, self.faction)
|
||||
if group is None:
|
||||
logging.error(f"Could not generate EWR at {self.control_point}")
|
||||
return
|
||||
|
||||
g.groups = [group]
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_base_defenses(self) -> None:
|
||||
# First group has a 1/2 chance of being a SAM, 1/6 chance of SHORAD,
|
||||
# and a 1/6 chance of a garrison.
|
||||
#
|
||||
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
|
||||
# being a garrison.
|
||||
for i in range(random.randint(2, 5)):
|
||||
if i == 0 and random.randint(0, 1) == 0:
|
||||
self.generate_sam()
|
||||
elif random.randint(0, 2) == 1:
|
||||
self.generate_shorad()
|
||||
else:
|
||||
self.generate_garrison()
|
||||
|
||||
def generate_garrison(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Garrison)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = VehicleGroupGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point,
|
||||
for_airbase=True)
|
||||
|
||||
group = generate_armor_group(self.faction_name, self.game, g)
|
||||
if group is None:
|
||||
logging.error(
|
||||
f"Could not generate garrison at {self.control_point}")
|
||||
return
|
||||
g.groups.append(group)
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_sam(self) -> None:
|
||||
position = self.location_finder.location_for(
|
||||
LocationType.BaseAirDefense)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point, for_airbase=True)
|
||||
|
||||
group = generate_anti_air_group(self.game, g, self.faction)
|
||||
if group is None:
|
||||
logging.error(f"Could not generate SAM at {self.control_point}")
|
||||
return
|
||||
g.groups.append(group)
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_shorad(self) -> None:
|
||||
position = self.location_finder.location_for(
|
||||
LocationType.BaseAirDefense)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point, for_airbase=True)
|
||||
|
||||
group = generate_anti_air_group(self.game, g, self.faction,
|
||||
ranges=[{AirDefenseRange.Short}])
|
||||
if group is None:
|
||||
logging.error(
|
||||
f"Could not generate SHORAD group at {self.control_point}")
|
||||
return
|
||||
g.groups.append(group)
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
|
||||
class FobDefenseGenerator(BaseDefenseGenerator):
|
||||
def generate(self) -> None:
|
||||
self.generate_garrison()
|
||||
self.generate_fob_defenses()
|
||||
|
||||
def generate_fob_defenses(self):
|
||||
# First group has a 1/2 chance of being a SHORAD,
|
||||
# and a 1/2 chance of a garrison.
|
||||
#
|
||||
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
|
||||
# being a garrison.
|
||||
for i in range(random.randint(2, 5)):
|
||||
if i == 0 and random.randint(0, 1) == 0:
|
||||
self.generate_shorad()
|
||||
elif i == 0 and random.randint(0, 1) == 0:
|
||||
self.generate_garrison()
|
||||
elif random.randint(0, 2) == 1:
|
||||
self.generate_shorad()
|
||||
else:
|
||||
self.generate_garrison()
|
||||
|
||||
|
||||
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
def __init__(self, game: Game, generator_settings: GeneratorSettings,
|
||||
control_point: ControlPoint,
|
||||
templates: GroundObjectTemplates) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
generator_settings: GeneratorSettings,
|
||||
control_point: ControlPoint,
|
||||
templates: GroundObjectTemplates,
|
||||
) -> None:
|
||||
super().__init__(game, generator_settings, control_point)
|
||||
self.templates = templates
|
||||
|
||||
@@ -540,77 +288,93 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
if not super().generate():
|
||||
return False
|
||||
|
||||
BaseDefenseGenerator(self.game, self.control_point).generate()
|
||||
self.generate_ground_points()
|
||||
|
||||
if self.faction.missiles:
|
||||
self.generate_missile_sites()
|
||||
|
||||
return True
|
||||
|
||||
def generate_ground_points(self) -> None:
|
||||
"""Generate ground objects and AA sites for the control point."""
|
||||
skip_sams = self.generate_required_aa()
|
||||
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.control_point.is_global:
|
||||
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)
|
||||
|
||||
# Always generate at least one AA point.
|
||||
self.generate_aa_site()
|
||||
|
||||
# And between 2 and 7 other objectives.
|
||||
amount = random.randrange(2, 7)
|
||||
for i in range(amount):
|
||||
# 1 in 4 additional objectives are AA.
|
||||
if random.randint(0, 3) == 0:
|
||||
if skip_sams > 0:
|
||||
skip_sams -= 1
|
||||
else:
|
||||
self.generate_aa_site()
|
||||
else:
|
||||
self.generate_ground_point()
|
||||
|
||||
def generate_required_aa(self) -> int:
|
||||
"""Generates the AA sites that are required by the campaign.
|
||||
|
||||
Returns:
|
||||
The number of AA sites that were generated.
|
||||
"""
|
||||
def generate_aa(self) -> None:
|
||||
presets = self.control_point.preset_locations
|
||||
for position in presets.required_long_range_sams:
|
||||
self.generate_aa_at(position, ranges=[
|
||||
{AirDefenseRange.Long},
|
||||
{AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
])
|
||||
for position in presets.required_medium_range_sams:
|
||||
self.generate_aa_at(position, ranges=[
|
||||
{AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
])
|
||||
return (len(presets.required_long_range_sams) +
|
||||
len(presets.required_medium_range_sams))
|
||||
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_ground_point(self) -> None:
|
||||
try:
|
||||
category = random.choice(self.faction.building_set)
|
||||
except IndexError:
|
||||
logging.exception("Faction has no buildings defined")
|
||||
return
|
||||
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()))
|
||||
|
||||
if category == "oil":
|
||||
location_type = LocationType.OffshoreStrikeTarget
|
||||
else:
|
||||
location_type = LocationType.StrikeTarget
|
||||
|
||||
# Pick from preset locations
|
||||
point = self.location_finder.location_for(location_type)
|
||||
if point is None:
|
||||
return
|
||||
|
||||
object_id = 0
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
@@ -620,49 +384,126 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
|
||||
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"])
|
||||
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_aa_site(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Sam)
|
||||
if position is None:
|
||||
return
|
||||
self.generate_aa_at(position, ranges=[
|
||||
# Prefer to use proper SAMs, but fall back to SHORADs if needed.
|
||||
{AirDefenseRange.Long, AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
])
|
||||
def generate_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_aa_at(
|
||||
self, position: Point,
|
||||
ranges: Iterable[Set[AirDefenseRange]]) -> None:
|
||||
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 = SamGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point, for_airbase=False)
|
||||
group = generate_anti_air_group(self.game, g, self.faction, ranges)
|
||||
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 air defense group for %s at %s",
|
||||
g.name, self.control_point)
|
||||
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 i in range(self.faction.missiles_group_count):
|
||||
self.generate_missile_site()
|
||||
|
||||
def generate_missile_site(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.MissileSite)
|
||||
if position is None:
|
||||
return
|
||||
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)
|
||||
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:
|
||||
@@ -670,21 +511,66 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
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()
|
||||
FobDefenseGenerator(self.game, self.control_point).generate()
|
||||
self.generate_required_aa()
|
||||
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:
|
||||
try:
|
||||
category = self.faction.building_set[self.faction.building_set.index('fob')]
|
||||
except IndexError:
|
||||
logging.exception("Faction has no fob buildings defined")
|
||||
return
|
||||
|
||||
category = "fob"
|
||||
obj_name = self.control_point.name
|
||||
template = random.choice(list(self.templates[category].values()))
|
||||
point = self.control_point.position
|
||||
@@ -698,14 +584,21 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||
|
||||
template_point = Point(unit["offset"].x, unit["offset"].y)
|
||||
g = BuildingGroundObject(
|
||||
obj_name, category, group_id, object_id, point + template_point,
|
||||
unit["heading"], self.control_point, unit["type"], airbase_group=True)
|
||||
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:
|
||||
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:
|
||||
@@ -723,19 +616,22 @@ class GroundObjectGenerator:
|
||||
generator: ControlPointGroundObjectGenerator
|
||||
if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
|
||||
generator = CarrierGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point)
|
||||
self.game, self.generator_settings, control_point
|
||||
)
|
||||
elif control_point.cptype == ControlPointType.LHA_GROUP:
|
||||
generator = LhaGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point)
|
||||
self.game, self.generator_settings, control_point
|
||||
)
|
||||
elif isinstance(control_point, OffMapSpawn):
|
||||
generator = NoOpGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point)
|
||||
self.game, self.generator_settings, control_point
|
||||
)
|
||||
elif isinstance(control_point, Fob):
|
||||
generator = FobGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point,
|
||||
self.templates)
|
||||
self.game, self.generator_settings, control_point, self.templates
|
||||
)
|
||||
else:
|
||||
generator = AirbaseGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point,
|
||||
self.templates)
|
||||
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,
|
||||
)
|
||||
@@ -1,11 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from typing import Iterator, List, TYPE_CHECKING
|
||||
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
|
||||
@@ -14,81 +25,53 @@ if TYPE_CHECKING:
|
||||
from .missiontarget import MissionTarget
|
||||
|
||||
NAME_BY_CATEGORY = {
|
||||
"power": "Power plant",
|
||||
"ammo": "Ammo depot",
|
||||
"fuel": "Fuel depot",
|
||||
"ewr": "Early Warning Radar",
|
||||
"aa": "AA Defense Site",
|
||||
"ware": "Warehouse",
|
||||
"farp": "FARP",
|
||||
"fob": "FOB",
|
||||
"factory": "Factory",
|
||||
"comms": "Comms. tower",
|
||||
"oil": "Oil platform",
|
||||
"derrick": "Derrick",
|
||||
"ww2bunker": "Bunker",
|
||||
"village": "Village",
|
||||
"allycamp": "Camp",
|
||||
"EWR":"EWR",
|
||||
}
|
||||
|
||||
ABBREV_NAME = {
|
||||
"power": "PLANT",
|
||||
"ammo": "AMMO",
|
||||
"fuel": "FUEL",
|
||||
"aa": "AA",
|
||||
"ware": "WARE",
|
||||
"ammo": "Ammo depot",
|
||||
"armor": "Armor group",
|
||||
"coastal": "Coastal defense",
|
||||
"comms": "Communications tower",
|
||||
"derrick": "Derrick",
|
||||
"factory": "Factory",
|
||||
"farp": "FARP",
|
||||
"fob": "FOB",
|
||||
"factory": "FACTORY",
|
||||
"comms": "COMMST",
|
||||
"oil": "OILP",
|
||||
"derrick": "DERK",
|
||||
"ww2bunker": "BUNK",
|
||||
"village": "VLG",
|
||||
"allycamp": "CMP",
|
||||
}
|
||||
|
||||
CATEGORY_MAP = {
|
||||
|
||||
# Special cases
|
||||
"CARRIER": ["CARRIER"],
|
||||
"LHA": ["LHA"],
|
||||
"aa": ["AA"],
|
||||
|
||||
# Buildings
|
||||
"power": ["Workshop A", "Electric power box", "Garage small A", "Farm B", "Repair workshop", "Garage B"],
|
||||
"ware": ["Warehouse", "Hangar A"],
|
||||
"fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"],
|
||||
"ammo": [".Ammunition depot", "Hangar B"],
|
||||
"farp": ["FARP Tent", "FARP Ammo Dump Coating", "FARP Fuel Depot", "FARP Command Post", "FARP CP Blindage"],
|
||||
"fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"],
|
||||
"factory": ["Tech combine", "Tech hangar A"],
|
||||
"comms": ["TV tower", "Comms tower M"],
|
||||
"oil": ["Oil platform"],
|
||||
"derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"],
|
||||
"ww2bunker": ["Siegfried Line", "Fire Control Bunker", "SK_C_28_naval_gun", "Concertina Wire", "Czech hedgehogs 1"],
|
||||
"village": ["Small house 1B", "Small House 1A", "Small warehouse 1"],
|
||||
"allycamp": [],
|
||||
"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,
|
||||
airbase_group: bool, sea_object: bool) -> None:
|
||||
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.airbase_group = airbase_group
|
||||
self.sea_object = sea_object
|
||||
self.is_dead = False
|
||||
# TODO: There is never more than one group.
|
||||
self.groups: List[Group] = []
|
||||
|
||||
@property
|
||||
def is_dead(self) -> bool:
|
||||
return self.alive_unit_count == 0
|
||||
|
||||
@property
|
||||
def units(self) -> List[Unit]:
|
||||
"""
|
||||
@@ -96,6 +79,17 @@ class TheaterGroundObject(MissionTarget):
|
||||
"""
|
||||
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."""
|
||||
@@ -124,6 +118,7 @@ class TheaterGroundObject(MissionTarget):
|
||||
|
||||
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
|
||||
@@ -144,11 +139,86 @@ class TheaterGroundObject(MissionTarget):
|
||||
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, airbase_group=False) -> None:
|
||||
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,
|
||||
@@ -157,10 +227,13 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
heading=heading,
|
||||
control_point=control_point,
|
||||
dcs_identifier=dcs_identifier,
|
||||
airbase_group=airbase_group,
|
||||
sea_object=False
|
||||
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:
|
||||
@@ -171,10 +244,104 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
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)
|
||||
@@ -183,15 +350,24 @@ class NavalGroundObject(TheaterGroundObject):
|
||||
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):
|
||||
pass
|
||||
@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:
|
||||
def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="CARRIER",
|
||||
@@ -200,8 +376,7 @@ class CarrierGroundObject(GenericCarrierGroundObject):
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="CARRIER",
|
||||
airbase_group=True,
|
||||
sea_object=True
|
||||
sea_object=True,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -213,8 +388,7 @@ class CarrierGroundObject(GenericCarrierGroundObject):
|
||||
|
||||
# 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:
|
||||
def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="LHA",
|
||||
@@ -223,8 +397,7 @@ class LhaGroundObject(GenericCarrierGroundObject):
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="LHA",
|
||||
airbase_group=True,
|
||||
sea_object=True
|
||||
sea_object=True,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -235,31 +408,69 @@ class LhaGroundObject(GenericCarrierGroundObject):
|
||||
|
||||
|
||||
class MissileSiteGroundObject(TheaterGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint) -> None:
|
||||
def __init__(
|
||||
self, name: str, group_id: int, position: Point, control_point: ControlPoint
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
category="missile",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=False,
|
||||
sea_object=False
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return False
|
||||
|
||||
class BaseDefenseGroundObject(TheaterGroundObject):
|
||||
"""Base type for all base defenses."""
|
||||
@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(BaseDefenseGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint, for_airbase: bool) -> None:
|
||||
class SamGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
@@ -268,8 +479,7 @@ class SamGroundObject(BaseDefenseGroundObject):
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=for_airbase,
|
||||
sea_object=False
|
||||
sea_object=False,
|
||||
)
|
||||
# Set by the SAM unit generator if the generated group is compatible
|
||||
# with Skynet.
|
||||
@@ -286,53 +496,113 @@ class SamGroundObject(BaseDefenseGroundObject):
|
||||
|
||||
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)
|
||||
|
||||
class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint, for_airbase: bool) -> None:
|
||||
@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="aa",
|
||||
category="armor",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=for_airbase,
|
||||
sea_object=False
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return False
|
||||
|
||||
class EwrGroundObject(BaseDefenseGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint) -> None:
|
||||
@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",
|
||||
category="ewr",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="EWR",
|
||||
airbase_group=True,
|
||||
sea_object=False
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them.
|
||||
return f"{self.faction_color}|{super().group_name}"
|
||||
# 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)
|
||||
@@ -341,20 +611,28 @@ class EwrGroundObject(BaseDefenseGroundObject):
|
||||
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:
|
||||
def __init__(
|
||||
self, name: str, group_id: int, position: Point, control_point: ControlPoint
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
category="ship",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=False,
|
||||
sea_object=True
|
||||
sea_object=True,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
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)
|
||||
649
game/transfers.py
Normal file
649
game/transfers.py
Normal file
@@ -0,0 +1,649 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from functools import singledispatchmethod
|
||||
from typing import (
|
||||
Generic,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
TypeVar,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
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, aircraft_for_task
|
||||
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[GroundUnitType, int]
|
||||
|
||||
transport: Optional[Transport] = field(default=None)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Returns the text that should be displayed for the transfer."""
|
||||
count = self.size
|
||||
origin = self.origin.name
|
||||
destination = self.destination.name
|
||||
description = "Transfer" if self.player else "Enemy transfer"
|
||||
return f"{description} of {count} units from {origin} to {destination}"
|
||||
|
||||
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: GroundUnitType) -> None:
|
||||
if unit_type not in self.units or not self.units[unit_type]:
|
||||
raise KeyError(f"{self} has no {unit_type} remaining")
|
||||
self.units[unit_type] -= 1
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return sum(self.units.values())
|
||||
|
||||
def iter_units(self) -> Iterator[GroundUnitType]:
|
||||
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.size
|
||||
|
||||
def disband_at(self, location: ControlPoint) -> None:
|
||||
logging.info(f"Units halting at {location}.")
|
||||
location.base.commission_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 find_escape_route(self) -> Optional[ControlPoint]:
|
||||
if self.transport is not None:
|
||||
return self.transport.find_escape_route()
|
||||
return None
|
||||
|
||||
def proceed(self) -> None:
|
||||
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.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
|
||||
|
||||
if self.transport is None:
|
||||
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[GroundUnitType, 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: AircraftType, airfield: ControlPoint
|
||||
) -> bool:
|
||||
if unit_type not in aircraft_for_task(FlightType.TRANSPORT):
|
||||
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.dcs_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
|
||||
)
|
||||
air_wing = self.game.air_wing_for(self.for_player)
|
||||
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 = air_wing.auto_assignable_for_task_with_type(
|
||||
unit_type, FlightType.TRANSPORT
|
||||
)
|
||||
for squadron in squadrons:
|
||||
if self.compatible_with_mission(unit_type, cp):
|
||||
while (
|
||||
available
|
||||
and squadron.has_available_pilots
|
||||
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_aircraft = inventory.available(squadron.aircraft)
|
||||
capacity_each = 1 if squadron.aircraft.dcs_unit_type.helicopter else 2
|
||||
required = math.ceil(self.transfer.size / capacity_each)
|
||||
flight_size = min(
|
||||
required,
|
||||
available_aircraft,
|
||||
squadron.aircraft.dcs_unit_type.group_size_max,
|
||||
)
|
||||
# TODO: Use number_of_available_pilots directly once feature flag is gone.
|
||||
# The number of currently available pilots is not relevant when pilot limits
|
||||
# are disabled.
|
||||
if not squadron.can_provide_pilots(flight_size):
|
||||
flight_size = squadron.number_of_available_pilots
|
||||
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: GroundUnitType) -> 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(t.size for t in self.transfers)
|
||||
|
||||
@property
|
||||
def units(self) -> dict[GroundUnitType, int]:
|
||||
units: dict[GroundUnitType, int] = defaultdict(int)
|
||||
for transfer in self.transfers:
|
||||
for unit_type, count in transfer.units.items():
|
||||
units[unit_type] += count
|
||||
return units
|
||||
|
||||
def iter_units(self) -> Iterator[GroundUnitType]:
|
||||
for unit_type, count in self.units.items():
|
||||
for _ in range(count):
|
||||
yield unit_type
|
||||
|
||||
@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.commission_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:
|
||||
if self.game.air_wing_for(control_point.captured).can_auto_plan(
|
||||
FlightType.TRANSPORT
|
||||
):
|
||||
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
|
||||
).auto_assignable_for_task(FlightType.TRANSPORT)
|
||||
unit_types = {s.aircraft for s in squadrons}
|
||||
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
|
||||
)
|
||||
)
|
||||
167
game/unitdelivery.py
Normal file
167
game/unitdelivery.py
Normal file
@@ -0,0 +1,167 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, TYPE_CHECKING, Any
|
||||
|
||||
from game.theater import ControlPoint
|
||||
from .dcs.groundunittype import GroundUnitType
|
||||
from .dcs.unittype import UnitType
|
||||
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[UnitType, int] = defaultdict(int)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Pending delivery to {self.destination}"
|
||||
|
||||
def order(self, units: dict[UnitType, int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] += v
|
||||
|
||||
def sell(self, units: dict[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_ground_units(self, game: Game) -> None:
|
||||
ground_units: dict[UnitType[Any], int] = {
|
||||
u: self.units[u] for u in self.units.keys() if isinstance(u, GroundUnitType)
|
||||
}
|
||||
self.refund(game, ground_units)
|
||||
for gu in ground_units.keys():
|
||||
del self.units[gu]
|
||||
|
||||
def refund(self, game: Game, units: dict[UnitType, int]) -> None:
|
||||
for unit_type, count in units.items():
|
||||
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
|
||||
game.adjust_budget(
|
||||
unit_type.price * count, player=self.destination.captured
|
||||
)
|
||||
|
||||
def pending_orders(self, unit_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: 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_ground_units(game)
|
||||
|
||||
bought_units: dict[UnitType, int] = {}
|
||||
units_needing_transfer: dict[GroundUnitType, int] = {}
|
||||
sold_units: dict[UnitType, int] = {}
|
||||
for unit_type, count in self.units.items():
|
||||
coalition = "Ally" if self.destination.captured else "Enemy"
|
||||
d: dict[Any, int]
|
||||
if (
|
||||
isinstance(unit_type, GroundUnitType)
|
||||
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: {unit_type} x {count} at {source}"
|
||||
)
|
||||
else:
|
||||
sold_units[unit_type] = -count
|
||||
game.message(f"{coalition} sold: {unit_type} x {-count} at {source}")
|
||||
|
||||
self.units = defaultdict(int)
|
||||
self.destination.base.commission_units(bought_units)
|
||||
self.destination.base.commit_losses(sold_units)
|
||||
|
||||
if units_needing_transfer:
|
||||
if ground_unit_source is None:
|
||||
raise RuntimeError(
|
||||
f"ground unit source could not be found for {self.destination} but still tried to "
|
||||
f"transfer units to there"
|
||||
)
|
||||
ground_unit_source.base.commission_units(units_needing_transfer)
|
||||
self.create_transfer(game, ground_unit_source, units_needing_transfer)
|
||||
|
||||
def create_transfer(
|
||||
self, game: Game, source: ControlPoint, units: dict[GroundUnitType, 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
|
||||
140
game/unitmap.py
140
game/unitmap.py
@@ -1,20 +1,29 @@
|
||||
"""Maps generated units back to their Liberation types."""
|
||||
import itertools
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional, Type
|
||||
from typing import Dict, Optional
|
||||
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game import db
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.squadrons import Pilot
|
||||
from game.theater import Airfield, ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import BuildingGroundObject
|
||||
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: Optional[Pilot]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrontLineUnit:
|
||||
unit_type: Type[VehicleType]
|
||||
unit_type: GroundUnitType
|
||||
origin: ControlPoint
|
||||
|
||||
|
||||
@@ -25,6 +34,18 @@ class GroundObjectUnit:
|
||||
unit: Unit
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ConvoyUnit:
|
||||
unit_type: GroundUnitType
|
||||
convoy: Convoy
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirliftUnits:
|
||||
cargo: tuple[GroundUnitType, ...]
|
||||
transfer: TransferOrder
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Building:
|
||||
ground_object: BuildingGroundObject
|
||||
@@ -32,22 +53,27 @@ class Building:
|
||||
|
||||
class UnitMap:
|
||||
def __init__(self) -> None:
|
||||
self.aircraft: Dict[str, Flight] = {}
|
||||
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 unit in group.units:
|
||||
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}")
|
||||
self.aircraft[name] = flight
|
||||
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[Flight]:
|
||||
def flight(self, unit_name: str) -> Optional[FlyingUnit]:
|
||||
return self.aircraft.get(unit_name, None)
|
||||
|
||||
def add_airfield(self, airfield: Airfield) -> None:
|
||||
@@ -58,27 +84,26 @@ class UnitMap:
|
||||
def airfield(self, name: str) -> Optional[Airfield]:
|
||||
return self.airfields.get(name, None)
|
||||
|
||||
def add_front_line_units(self, group: Group, origin: ControlPoint) -> None:
|
||||
def add_front_line_units(
|
||||
self, group: Group, origin: ControlPoint, unit_type: GroundUnitType
|
||||
) -> 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:
|
||||
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:
|
||||
@@ -103,22 +128,77 @@ class UnitMap:
|
||||
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)
|
||||
ground_object, persistence_group, persistent_unit
|
||||
)
|
||||
|
||||
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
|
||||
return self.ground_object_units.get(name, None)
|
||||
|
||||
def add_building(self, ground_object: BuildingGroundObject,
|
||||
group: Group) -> None:
|
||||
def add_convoy_units(self, group: Group, convoy: Convoy) -> None:
|
||||
for unit, unit_type in zip(group.units, convoy.iter_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}")
|
||||
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(group.name)
|
||||
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:
|
||||
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]
|
||||
@@ -129,5 +209,15 @@ class UnitMap:
|
||||
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)
|
||||
|
||||
240
game/utils.py
240
game/utils.py
@@ -1,65 +1,19 @@
|
||||
def meter_to_feet(value_in_meter: float) -> int:
|
||||
"""Converts meters to feets
|
||||
from __future__ import annotations
|
||||
|
||||
:arg value_in_meter Value in meters
|
||||
"""
|
||||
return int(3.28084 * value_in_meter)
|
||||
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
|
||||
|
||||
def feet_to_meter(value_in_feet: float) -> int:
|
||||
"""Converts feets to meters
|
||||
|
||||
:arg value_in_feet Value in feets
|
||||
"""
|
||||
return int(value_in_feet / 3.28084)
|
||||
|
||||
|
||||
def meter_to_nm(value_in_meter: float) -> int:
|
||||
"""Converts meters to nautic miles
|
||||
|
||||
:arg value_in_meter Value in meters
|
||||
"""
|
||||
return int(value_in_meter / 1852)
|
||||
|
||||
|
||||
def nm_to_meter(value_in_nm: float) -> int:
|
||||
"""Converts nautic miles to meters
|
||||
|
||||
:arg value_in_nm Value in nautic miles
|
||||
"""
|
||||
return int(value_in_nm * 1852)
|
||||
|
||||
|
||||
def knots_to_kph(value_in_knots: float) -> int:
|
||||
"""Converts Knots to Kilometer Per Hour
|
||||
|
||||
:arg value_in_knots Knots
|
||||
"""
|
||||
return int(value_in_knots * 1.852)
|
||||
|
||||
|
||||
def mps_to_knots(value_in_mps: float) -> int:
|
||||
"""Converts Meters Per Second To Knots
|
||||
|
||||
:arg value_in_mps Meters Per Second
|
||||
"""
|
||||
return int(value_in_mps * 1.943)
|
||||
|
||||
|
||||
def mps_to_kph(speed: float) -> int:
|
||||
"""Converts meters per second to kilometers per hour.
|
||||
|
||||
:arg speed Speed in m/s.
|
||||
"""
|
||||
return int(speed * 3.6)
|
||||
|
||||
|
||||
def kph_to_mps(speed: float) -> int:
|
||||
"""Converts kilometers per hour to meters per second.
|
||||
|
||||
:arg speed Speed in KPH.
|
||||
"""
|
||||
return int(speed / 3.6)
|
||||
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 heading_sum(h, a) -> int:
|
||||
@@ -71,5 +25,171 @@ def heading_sum(h, a) -> int:
|
||||
else:
|
||||
return h
|
||||
|
||||
|
||||
def opposite_heading(h):
|
||||
return heading_sum(h, 180)
|
||||
return heading_sum(h, 180)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
@classmethod
|
||||
def inf(cls) -> Distance:
|
||||
return cls.from_meters(math.inf)
|
||||
|
||||
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.3.0"]
|
||||
components = ["4.0.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,84 @@ 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):
|
||||
#: * Flak18,
|
||||
#: * Vulcan,
|
||||
#: * ZSU_23_4_Shilka,
|
||||
#:
|
||||
#: 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.
|
||||
#:
|
||||
#: Version 6.1
|
||||
#: * Support for new Syrian airfields in DCS 2.7.2.7910.1 (Cyprus update).
|
||||
#:
|
||||
#: Version 7.0
|
||||
#: * DCS 2.7.2.7910.1 (Cyprus update) changed the IDs of scenery strike targets. Any
|
||||
#: mission using map buildings as strike targets must check and potentially recreate
|
||||
#: all those objectives. This definitely affects all Syria campaigns, other maps are
|
||||
#: not yet verified.
|
||||
CAMPAIGN_FORMAT_VERSION = (7, 0)
|
||||
|
||||
@@ -3,13 +3,15 @@ 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, 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 game.utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater
|
||||
@@ -35,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
|
||||
|
||||
|
||||
@@ -56,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:
|
||||
@@ -75,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
|
||||
@@ -100,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)
|
||||
@@ -113,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)
|
||||
@@ -130,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:
|
||||
@@ -144,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 = {
|
||||
@@ -180,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()
|
||||
|
||||
1780
gen/aircraft.py
1780
gen/aircraft.py
File diff suppressed because it is too large
Load Diff
489
gen/airfields.py
489
gen/airfields.py
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Type, Tuple
|
||||
from datetime import timedelta
|
||||
from typing import List, Type, Tuple, Optional
|
||||
|
||||
from dcs.mission import Mission, StartType
|
||||
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
|
||||
@@ -15,12 +16,14 @@ from dcs.task import (
|
||||
)
|
||||
|
||||
from game import db
|
||||
from .flights.ai_flight_planner_db import AEWC_CAPABLE
|
||||
from .naming import namegen
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .conflictgen import Conflict
|
||||
from .radios import RadioFrequency, RadioRegistry
|
||||
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||
|
||||
|
||||
TANKER_DISTANCE = 15000
|
||||
TANKER_ALT = 4572
|
||||
TANKER_HEADING_OFFSET = 45
|
||||
@@ -32,19 +35,28 @@ 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
|
||||
start_time: Optional[timedelta]
|
||||
end_time: Optional[timedelta]
|
||||
blue: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -54,10 +66,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
|
||||
@@ -78,74 +94,118 @@ class AirSupportConflictGenerator:
|
||||
elif unit_type is KC135MPRS:
|
||||
return (TANKER_ALT + 500, 596)
|
||||
return (TANKER_ALT, 574)
|
||||
|
||||
|
||||
def generate(self):
|
||||
player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp
|
||||
player_cp = (
|
||||
self.conflict.blue_cp
|
||||
if self.conflict.blue_cp.captured
|
||||
else self.conflict.red_cp
|
||||
)
|
||||
|
||||
fallback_tanker_number = 0
|
||||
if not self.game.settings.disable_legacy_tanker:
|
||||
fallback_tanker_number = 0
|
||||
|
||||
for i, tanker_unit_type in enumerate(db.find_unittype(Refueling, self.conflict.attackers_side)):
|
||||
alt, airspeed = self._get_tanker_params(tanker_unit_type)
|
||||
variant = db.unit_type_name(tanker_unit_type)
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
|
||||
tanker_heading = self.conflict.to_cp.position.heading_between_point(self.conflict.from_cp.position) + TANKER_HEADING_OFFSET * i
|
||||
tanker_position = player_cp.position.point_from_heading(tanker_heading, TANKER_DISTANCE)
|
||||
tanker_group = self.mission.refuel_flight(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
name=namegen.next_tanker_name(self.mission.country(self.game.player_country), tanker_unit_type),
|
||||
airport=None,
|
||||
plane_type=tanker_unit_type,
|
||||
position=tanker_position,
|
||||
altitude=alt,
|
||||
race_distance=58000,
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
speed=airspeed,
|
||||
tacanchannel=str(tacan),
|
||||
)
|
||||
tanker_group.set_frequency(freq.mhz)
|
||||
for i, tanker_unit_type in enumerate(
|
||||
self.game.faction_for(player=True).tankers
|
||||
):
|
||||
# TODO: Make loiter altitude a property of the unit type.
|
||||
alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
|
||||
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
|
||||
),
|
||||
airport=None,
|
||||
plane_type=tanker_unit_type,
|
||||
position=tanker_position,
|
||||
altitude=alt,
|
||||
race_distance=58000,
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
speed=airspeed,
|
||||
tacanchannel=str(tacan),
|
||||
)
|
||||
tanker_group.set_frequency(freq.mhz)
|
||||
|
||||
callsign = callsign_for_support_unit(tanker_group)
|
||||
tacan_callsign = {
|
||||
"Texaco": "TEX",
|
||||
"Arco": "ARC",
|
||||
"Shell": "SHL",
|
||||
}.get(callsign)
|
||||
if tacan_callsign is None:
|
||||
# The dict above is all the callsigns currently in the game, but
|
||||
# non-Western countries don't use the callsigns and instead just
|
||||
# use numbers. It's possible that none of those nations have
|
||||
# TACAN compatible refueling aircraft, but fallback just in
|
||||
# case.
|
||||
tacan_callsign = f"TK{fallback_tanker_number}"
|
||||
fallback_tanker_number += 1
|
||||
callsign = callsign_for_support_unit(tanker_group)
|
||||
tacan_callsign = {
|
||||
"Texaco": "TEX",
|
||||
"Arco": "ARC",
|
||||
"Shell": "SHL",
|
||||
}.get(callsign)
|
||||
if tacan_callsign is None:
|
||||
# The dict above is all the callsigns currently in the game, but
|
||||
# non-Western countries don't use the callsigns and instead just
|
||||
# use numbers. It's possible that none of those nations have
|
||||
# TACAN compatible refueling aircraft, but fallback just in
|
||||
# case.
|
||||
tacan_callsign = f"TK{fallback_tanker_number}"
|
||||
fallback_tanker_number += 1
|
||||
|
||||
if tanker_unit_type != IL_78M:
|
||||
# Override PyDCS tacan channel.
|
||||
tanker_group.points[0].tasks.pop()
|
||||
tanker_group.points[0].tasks.append(ActivateBeaconCommand(
|
||||
tacan.number, tacan.band.value, tacan_callsign, True,
|
||||
tanker_group.units[0].id, True))
|
||||
if tanker_unit_type != IL_78M:
|
||||
# Override PyDCS tacan channel.
|
||||
tanker_group.points[0].tasks.pop()
|
||||
tanker_group.points[0].tasks.append(
|
||||
ActivateBeaconCommand(
|
||||
tacan.number,
|
||||
tacan.band.value,
|
||||
tacan_callsign,
|
||||
True,
|
||||
tanker_group.units[0].id,
|
||||
True,
|
||||
)
|
||||
)
|
||||
|
||||
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
|
||||
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,
|
||||
tanker_unit_type.name,
|
||||
freq,
|
||||
tacan,
|
||||
blue=True,
|
||||
)
|
||||
)
|
||||
|
||||
possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
|
||||
if not self.game.settings.disable_legacy_aewc:
|
||||
possible_awacs = [
|
||||
a
|
||||
for a in self.game.faction_for(player=True).aircrafts
|
||||
if a in AEWC_CAPABLE
|
||||
]
|
||||
|
||||
if not possible_awacs:
|
||||
logging.warning("No AWACS for faction")
|
||||
return
|
||||
|
||||
if len(possible_awacs) > 0:
|
||||
awacs_unit = possible_awacs[0]
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
|
||||
|
||||
awacs_flight = self.mission.awacs_flight(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
name=namegen.next_awacs_name(self.mission.country(self.game.player_country)),
|
||||
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,
|
||||
)
|
||||
@@ -154,7 +214,14 @@ 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))
|
||||
else:
|
||||
logging.warning("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,
|
||||
)
|
||||
)
|
||||
|
||||
475
gen/armor.py
475
gen/armor.py
@@ -10,23 +10,33 @@ from dcs.action import AITaskPush
|
||||
from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged
|
||||
from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
from dcs.planes import MQ_9_Reaper
|
||||
from dcs.point import PointAction
|
||||
from dcs.task import (EPLRS, AttackGroup, ControlledTask, FireAtPoint,
|
||||
GoToWaypoint, Hold, OrbitAction, SetImmortalCommand,
|
||||
SetInvisibleCommand)
|
||||
from dcs.task import (
|
||||
EPLRS,
|
||||
AttackGroup,
|
||||
ControlledTask,
|
||||
FireAtPoint,
|
||||
GoToWaypoint,
|
||||
Hold,
|
||||
OrbitAction,
|
||||
SetImmortalCommand,
|
||||
SetInvisibleCommand,
|
||||
)
|
||||
from dcs.triggers import Event, TriggerOnce
|
||||
from dcs.unit import Vehicle
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
from dcs.unittype import VehicleType
|
||||
from game import db
|
||||
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import heading_sum, opposite_heading
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
|
||||
from gen.ground_forces.ai_ground_planner import (DISTANCE_FROM_FRONTLINE,
|
||||
CombatGroup, CombatGroupRole)
|
||||
|
||||
from gen.ground_forces.ai_ground_planner import (
|
||||
DISTANCE_FROM_FRONTLINE,
|
||||
CombatGroup,
|
||||
CombatGroupRole,
|
||||
)
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .conflictgen import Conflict
|
||||
from .ground_forces.combat_stance import CombatStance
|
||||
@@ -50,29 +60,33 @@ FIGHT_DISTANCE = 3500
|
||||
|
||||
RANDOM_OFFSET_ATTACK = 250
|
||||
|
||||
INFANTRY_GROUP_SIZE = 5
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JtacInfo:
|
||||
"""JTAC information."""
|
||||
dcsGroupName: str
|
||||
|
||||
group_name: str
|
||||
unit_name: str
|
||||
callsign: str
|
||||
region: str
|
||||
code: str
|
||||
blue: bool
|
||||
# TODO: Radio info? Type?
|
||||
|
||||
|
||||
class GroundConflictGenerator:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mission: Mission,
|
||||
conflict: Conflict,
|
||||
game: Game,
|
||||
player_planned_combat_groups: List[CombatGroup],
|
||||
enemy_planned_combat_groups: List[CombatGroup],
|
||||
player_stance: CombatStance,
|
||||
unit_map: UnitMap) -> None:
|
||||
self,
|
||||
mission: Mission,
|
||||
conflict: Conflict,
|
||||
game: Game,
|
||||
player_planned_combat_groups: List[CombatGroup],
|
||||
enemy_planned_combat_groups: List[CombatGroup],
|
||||
player_stance: CombatStance,
|
||||
unit_map: UnitMap,
|
||||
) -> None:
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
self.enemy_planned_combat_groups = enemy_planned_combat_groups
|
||||
@@ -85,14 +99,16 @@ class GroundConflictGenerator:
|
||||
|
||||
def _enemy_stance(self):
|
||||
"""Picks the enemy stance according to the number of planned groups on the frontline for each side"""
|
||||
if len(self.enemy_planned_combat_groups) > len(self.player_planned_combat_groups):
|
||||
if len(self.enemy_planned_combat_groups) > len(
|
||||
self.player_planned_combat_groups
|
||||
):
|
||||
return random.choice(
|
||||
[
|
||||
CombatStance.AGGRESSIVE,
|
||||
CombatStance.AGGRESSIVE,
|
||||
CombatStance.AGGRESSIVE,
|
||||
CombatStance.ELIMINATION,
|
||||
CombatStance.BREAKTHROUGH
|
||||
CombatStance.BREAKTHROUGH,
|
||||
]
|
||||
)
|
||||
else:
|
||||
@@ -102,31 +118,37 @@ class GroundConflictGenerator:
|
||||
CombatStance.DEFENSIVE,
|
||||
CombatStance.DEFENSIVE,
|
||||
CombatStance.AMBUSH,
|
||||
CombatStance.AGGRESSIVE
|
||||
CombatStance.AGGRESSIVE,
|
||||
]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _group_point(point: Point, base_distance) -> Point:
|
||||
distance = random.randint(
|
||||
int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
|
||||
int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
|
||||
)
|
||||
return point.random_point_within(distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR)
|
||||
int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
|
||||
int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
|
||||
)
|
||||
return point.random_point_within(
|
||||
distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
position = Conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp, self.game.theater)
|
||||
position = Conflict.frontline_position(
|
||||
self.conflict.front_line, self.game.theater
|
||||
)
|
||||
frontline_vector = Conflict.frontline_vector(
|
||||
self.conflict.from_cp,
|
||||
self.conflict.to_cp,
|
||||
self.game.theater
|
||||
)
|
||||
self.conflict.front_line, self.game.theater
|
||||
)
|
||||
|
||||
# Create player groups at random position
|
||||
player_groups = self._generate_groups(self.player_planned_combat_groups, frontline_vector, True)
|
||||
player_groups = self._generate_groups(
|
||||
self.player_planned_combat_groups, frontline_vector, True
|
||||
)
|
||||
|
||||
# Create enemy groups at random position
|
||||
enemy_groups = self._generate_groups(self.enemy_planned_combat_groups, frontline_vector, False)
|
||||
enemy_groups = self._generate_groups(
|
||||
self.enemy_planned_combat_groups, frontline_vector, False
|
||||
)
|
||||
|
||||
# Plan combat actions for groups
|
||||
self.plan_action_for_groups(
|
||||
@@ -134,116 +156,140 @@ class GroundConflictGenerator:
|
||||
player_groups,
|
||||
enemy_groups,
|
||||
self.conflict.heading + 90,
|
||||
self.conflict.from_cp,
|
||||
self.conflict.to_cp
|
||||
self.conflict.blue_cp,
|
||||
self.conflict.red_cp,
|
||||
)
|
||||
self.plan_action_for_groups(
|
||||
self.enemy_stance,
|
||||
enemy_groups,
|
||||
player_groups,
|
||||
self.conflict.heading - 90,
|
||||
self.conflict.to_cp,
|
||||
self.conflict.from_cp
|
||||
self.conflict.red_cp,
|
||||
self.conflict.blue_cp,
|
||||
)
|
||||
|
||||
# Add JTAC
|
||||
if self.game.player_faction.has_jtac:
|
||||
n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id)
|
||||
n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id)
|
||||
code = 1688 - len(self.jtacs)
|
||||
|
||||
utype = MQ_9_Reaper
|
||||
if self.game.player_faction.jtac_unit is not None:
|
||||
utype = self.game.player_faction.jtac_unit
|
||||
utype = self.game.player_faction.jtac_unit
|
||||
if self.game.player_faction.jtac_unit is None:
|
||||
utype = AircraftType.named("MQ-9 Reaper")
|
||||
|
||||
jtac = self.mission.flight_group(country=self.mission.country(self.game.player_country),
|
||||
name=n,
|
||||
aircraft_type=utype,
|
||||
position=position[0],
|
||||
airport=None,
|
||||
altitude=5000)
|
||||
jtac = self.mission.flight_group(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
name=n,
|
||||
aircraft_type=utype.dcs_unit_type,
|
||||
position=position[0],
|
||||
airport=None,
|
||||
altitude=5000,
|
||||
)
|
||||
jtac.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
jtac.points[0].tasks.append(SetImmortalCommand(True))
|
||||
jtac.points[0].tasks.append(OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle))
|
||||
frontline = f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}"
|
||||
jtac.points[0].tasks.append(
|
||||
OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle)
|
||||
)
|
||||
frontline = (
|
||||
f"Frontline {self.conflict.blue_cp.name}/{self.conflict.red_cp.name}"
|
||||
)
|
||||
# Note: Will need to change if we ever add ground based JTAC.
|
||||
callsign = callsign_for_support_unit(jtac)
|
||||
self.jtacs.append(JtacInfo(str(jtac.name), n, callsign, frontline, str(code)))
|
||||
self.jtacs.append(
|
||||
JtacInfo(
|
||||
str(jtac.name),
|
||||
n,
|
||||
callsign,
|
||||
frontline,
|
||||
str(code),
|
||||
blue=True,
|
||||
)
|
||||
)
|
||||
|
||||
def gen_infantry_group_for_group(
|
||||
self,
|
||||
group: VehicleGroup,
|
||||
is_player: bool,
|
||||
side: Country,
|
||||
forward_heading: int
|
||||
self, group: VehicleGroup, is_player: bool, side: Country, forward_heading: int
|
||||
) -> None:
|
||||
|
||||
infantry_position = self.conflict.find_ground_position(
|
||||
group.points[0].position.random_point_within(250, 50),
|
||||
500,
|
||||
forward_heading,
|
||||
self.conflict.theater
|
||||
)
|
||||
self.conflict.theater,
|
||||
)
|
||||
if not infantry_position:
|
||||
logging.warning("Could not find infantry position")
|
||||
return
|
||||
if side == self.conflict.attackers_country:
|
||||
cp = self.conflict.from_cp
|
||||
cp = self.conflict.blue_cp
|
||||
else:
|
||||
cp = self.conflict.to_cp
|
||||
cp = self.conflict.red_cp
|
||||
|
||||
if is_player:
|
||||
faction = self.game.player_name
|
||||
else:
|
||||
faction = self.game.enemy_name
|
||||
faction = self.game.faction_for(is_player)
|
||||
|
||||
# Disable infantry unit gen if disabled
|
||||
if not self.game.settings.perf_infantry:
|
||||
if self.game.settings.manpads:
|
||||
# 50% of armored units protected by manpad
|
||||
if random.choice([True, False]):
|
||||
manpads = db.find_manpad(faction)
|
||||
if len(manpads) > 0:
|
||||
u = random.choice(manpads)
|
||||
manpads = list(faction.infantry_with_class(GroundUnitClass.Manpads))
|
||||
if manpads:
|
||||
u = random.choices(
|
||||
manpads, weights=[m.spawn_weight for m in manpads]
|
||||
)[0]
|
||||
self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp, u), u,
|
||||
namegen.next_infantry_name(side, cp.id, u),
|
||||
u.dcs_unit_type,
|
||||
position=infantry_position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
move_formation=PointAction.OffRoad)
|
||||
move_formation=PointAction.OffRoad,
|
||||
)
|
||||
return
|
||||
|
||||
possible_infantry_units = db.find_infantry(faction, allow_manpad=self.game.settings.manpads)
|
||||
if len(possible_infantry_units) == 0:
|
||||
possible_infantry_units = set(
|
||||
faction.infantry_with_class(GroundUnitClass.Infantry)
|
||||
)
|
||||
if self.game.settings.manpads:
|
||||
possible_infantry_units |= set(
|
||||
faction.infantry_with_class(GroundUnitClass.Manpads)
|
||||
)
|
||||
if not possible_infantry_units:
|
||||
return
|
||||
|
||||
u = random.choice(possible_infantry_units)
|
||||
infantry_choices = list(possible_infantry_units)
|
||||
units = random.choices(
|
||||
infantry_choices,
|
||||
weights=[u.spawn_weight for u in infantry_choices],
|
||||
k=INFANTRY_GROUP_SIZE,
|
||||
)
|
||||
self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp, u), u,
|
||||
position=infantry_position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
move_formation=PointAction.OffRoad)
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp.id, units[0]),
|
||||
units[0].dcs_unit_type,
|
||||
position=infantry_position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
move_formation=PointAction.OffRoad,
|
||||
)
|
||||
|
||||
for i in range(random.randint(3, 10)):
|
||||
u = random.choice(possible_infantry_units)
|
||||
for unit in units[1:]:
|
||||
position = infantry_position.random_point_within(55, 5)
|
||||
self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp, u), u,
|
||||
namegen.next_infantry_name(side, cp.id, unit),
|
||||
unit.dcs_unit_type,
|
||||
position=position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
move_formation=PointAction.OffRoad)
|
||||
move_formation=PointAction.OffRoad,
|
||||
)
|
||||
|
||||
def _set_reform_waypoint(
|
||||
self,
|
||||
dcs_group: VehicleGroup,
|
||||
forward_heading: int
|
||||
self, dcs_group: VehicleGroup, forward_heading: int
|
||||
) -> None:
|
||||
"""Setting a waypoint close to the spawn position allows the group to reform gracefully
|
||||
rather than spin
|
||||
rather than spin
|
||||
"""
|
||||
reform_point = dcs_group.position.point_from_heading(forward_heading, 50)
|
||||
dcs_group.add_waypoint(reform_point)
|
||||
@@ -254,7 +300,7 @@ class GroundConflictGenerator:
|
||||
gen_group: CombatGroup,
|
||||
dcs_group: VehicleGroup,
|
||||
forward_heading: int,
|
||||
target: Point
|
||||
target: Point,
|
||||
) -> bool:
|
||||
"""
|
||||
Handles adding the DCS tasks for artillery groups for all combat stances.
|
||||
@@ -267,10 +313,12 @@ class GroundConflictGenerator:
|
||||
dcs_group.add_trigger_action(hold_task)
|
||||
|
||||
# Artillery strike random start
|
||||
artillery_trigger = TriggerOnce(Event.NoEvent, "ArtilleryFireTask #" + str(dcs_group.id))
|
||||
artillery_trigger = TriggerOnce(
|
||||
Event.NoEvent, "ArtilleryFireTask #" + str(dcs_group.id)
|
||||
)
|
||||
artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45) * 60))
|
||||
# TODO: Update to fire at group instead of point
|
||||
fire_task = FireAtPoint(target, len(gen_group.units) * 10, 100)
|
||||
fire_task = FireAtPoint(target, gen_group.size * 10, 100)
|
||||
fire_task.number = 2 if stance != CombatStance.RETREAT else 1
|
||||
dcs_group.add_trigger_action(fire_task)
|
||||
artillery_trigger.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
|
||||
@@ -281,12 +329,19 @@ class GroundConflictGenerator:
|
||||
|
||||
# Hold position
|
||||
dcs_group.points[1].tasks.append(Hold())
|
||||
retreat = self.find_retreat_point(dcs_group, heading_sum(forward_heading, 180), (int)(RETREAT_DISTANCE/3))
|
||||
dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad)
|
||||
retreat = self.find_retreat_point(
|
||||
dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 3)
|
||||
)
|
||||
dcs_group.add_waypoint(
|
||||
dcs_group.position.point_from_heading(forward_heading, 1),
|
||||
PointAction.OffRoad,
|
||||
)
|
||||
dcs_group.points[2].tasks.append(Hold())
|
||||
dcs_group.add_waypoint(retreat, PointAction.OffRoad)
|
||||
|
||||
artillery_fallback = TriggerOnce(Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id))
|
||||
artillery_fallback = TriggerOnce(
|
||||
Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id)
|
||||
)
|
||||
for i, u in enumerate(dcs_group.units):
|
||||
artillery_fallback.add_condition(UnitDamaged(u.id))
|
||||
if i < len(dcs_group.units) - 1:
|
||||
@@ -300,7 +355,9 @@ class GroundConflictGenerator:
|
||||
retreat_task.number = 4
|
||||
dcs_group.add_trigger_action(retreat_task)
|
||||
|
||||
artillery_fallback.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
|
||||
artillery_fallback.add_action(
|
||||
AITaskPush(dcs_group.id, len(dcs_group.tasks))
|
||||
)
|
||||
self.mission.triggerrules.triggers.append(artillery_fallback)
|
||||
|
||||
for u in dcs_group.units:
|
||||
@@ -328,12 +385,8 @@ class GroundConflictGenerator:
|
||||
target = self.find_nearest_enemy_group(dcs_group, enemy_groups)
|
||||
if target is not None:
|
||||
rand_offset = Point(
|
||||
random.randint(
|
||||
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
|
||||
),
|
||||
random.randint(
|
||||
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
|
||||
)
|
||||
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
||||
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
||||
)
|
||||
target_point = self.conflict.theater.nearest_land_pos(
|
||||
target.points[0].position + rand_offset
|
||||
@@ -343,49 +396,58 @@ class GroundConflictGenerator:
|
||||
|
||||
if (
|
||||
to_cp.position.distance_to_point(dcs_group.points[0].position)
|
||||
<=
|
||||
AGGRESIVE_MOVE_DISTANCE
|
||||
<= AGGRESIVE_MOVE_DISTANCE
|
||||
):
|
||||
attack_point = self.conflict.theater.nearest_land_pos(
|
||||
to_cp.position.random_point_within(500, 0)
|
||||
)
|
||||
else:
|
||||
# We use an offset heading here because DCS doesn't always
|
||||
# force vehicles to move if there's no heading change.
|
||||
offset_heading = forward_heading - 2
|
||||
if offset_heading < 0:
|
||||
offset_heading = 358
|
||||
attack_point = self.find_offensive_point(
|
||||
dcs_group,
|
||||
forward_heading,
|
||||
AGGRESIVE_MOVE_DISTANCE
|
||||
dcs_group, offset_heading, AGGRESIVE_MOVE_DISTANCE
|
||||
)
|
||||
dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
|
||||
elif stance == CombatStance.BREAKTHROUGH:
|
||||
# In breakthrough mode, the units will move forward
|
||||
# If the enemy base is close enough, the units will attack the base
|
||||
if to_cp.position.distance_to_point(
|
||||
dcs_group.points[0].position) <= BREAKTHROUGH_OFFENSIVE_DISTANCE:
|
||||
if (
|
||||
to_cp.position.distance_to_point(dcs_group.points[0].position)
|
||||
<= BREAKTHROUGH_OFFENSIVE_DISTANCE
|
||||
):
|
||||
attack_point = self.conflict.theater.nearest_land_pos(
|
||||
to_cp.position.random_point_within(500, 0)
|
||||
)
|
||||
else:
|
||||
attack_point = self.find_offensive_point(dcs_group, forward_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE)
|
||||
# We use an offset heading here because DCS doesn't always
|
||||
# force vehicles to move if there's no heading change.
|
||||
offset_heading = forward_heading - 1
|
||||
if offset_heading < 0:
|
||||
offset_heading = 359
|
||||
attack_point = self.find_offensive_point(
|
||||
dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE
|
||||
)
|
||||
dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
|
||||
elif stance == CombatStance.ELIMINATION:
|
||||
# In elimination mode, the units focus on destroying as much enemy groups as possible
|
||||
targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3)
|
||||
for i, target in enumerate(targets, start=1):
|
||||
rand_offset = Point(
|
||||
random.randint(
|
||||
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
|
||||
),
|
||||
random.randint(
|
||||
-RANDOM_OFFSET_ATTACK,
|
||||
RANDOM_OFFSET_ATTACK
|
||||
)
|
||||
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
||||
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
||||
)
|
||||
target_point = self.conflict.theater.nearest_land_pos(
|
||||
target.points[0].position+rand_offset
|
||||
target.points[0].position + rand_offset
|
||||
)
|
||||
dcs_group.add_waypoint(target_point, PointAction.OffRoad)
|
||||
dcs_group.points[i + 1].tasks.append(AttackGroup(target.id))
|
||||
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
|
||||
if (
|
||||
to_cp.position.distance_to_point(dcs_group.points[0].position)
|
||||
<= AGGRESIVE_MOVE_DISTANCE
|
||||
):
|
||||
attack_point = self.conflict.theater.nearest_land_pos(
|
||||
to_cp.position.random_point_within(500, 0)
|
||||
)
|
||||
@@ -408,12 +470,23 @@ class GroundConflictGenerator:
|
||||
Returns True if tasking was added, returns False if the stance was not a combat stance.
|
||||
"""
|
||||
self._set_reform_waypoint(dcs_group, forward_heading)
|
||||
if stance in [CombatStance.AGGRESSIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]:
|
||||
if stance in [
|
||||
CombatStance.AGGRESSIVE,
|
||||
CombatStance.BREAKTHROUGH,
|
||||
CombatStance.ELIMINATION,
|
||||
]:
|
||||
# APC & ATGM will never move too much forward, but will follow along any offensive
|
||||
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
|
||||
attack_point = self.conflict.theater.nearest_land_pos(to_cp.position.random_point_within(500, 0))
|
||||
if (
|
||||
to_cp.position.distance_to_point(dcs_group.points[0].position)
|
||||
<= AGGRESIVE_MOVE_DISTANCE
|
||||
):
|
||||
attack_point = self.conflict.theater.nearest_land_pos(
|
||||
to_cp.position.random_point_within(500, 0)
|
||||
)
|
||||
else:
|
||||
attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE)
|
||||
attack_point = self.find_offensive_point(
|
||||
dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE
|
||||
)
|
||||
dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
|
||||
|
||||
if stance != CombatStance.RETREAT:
|
||||
@@ -422,29 +495,36 @@ class GroundConflictGenerator:
|
||||
return False
|
||||
|
||||
def plan_action_for_groups(
|
||||
self, stance: CombatStance,
|
||||
self,
|
||||
stance: CombatStance,
|
||||
ally_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
||||
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
||||
forward_heading: int,
|
||||
from_cp: ControlPoint,
|
||||
to_cp: ControlPoint
|
||||
to_cp: ControlPoint,
|
||||
) -> None:
|
||||
|
||||
if not self.game.settings.perf_moving_units:
|
||||
return
|
||||
|
||||
for dcs_group, group in ally_groups:
|
||||
if hasattr(group.units[0], 'eplrs') and group.units[0].eplrs:
|
||||
if group.unit_type.eplrs_capable:
|
||||
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
|
||||
|
||||
if group.role == CombatGroupRole.ARTILLERY:
|
||||
if self.game.settings.perf_artillery:
|
||||
target = self.get_artillery_target_in_range(dcs_group, group, enemy_groups)
|
||||
target = self.get_artillery_target_in_range(
|
||||
dcs_group, group, enemy_groups
|
||||
)
|
||||
if target is not None:
|
||||
self._plan_artillery_action(stance, group, dcs_group, forward_heading, target)
|
||||
self._plan_artillery_action(
|
||||
stance, group, dcs_group, forward_heading, target
|
||||
)
|
||||
|
||||
elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]:
|
||||
self._plan_tank_ifv_action(stance, enemy_groups, dcs_group, forward_heading, to_cp)
|
||||
self._plan_tank_ifv_action(
|
||||
stance, enemy_groups, dcs_group, forward_heading, to_cp
|
||||
)
|
||||
|
||||
elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]:
|
||||
self._plan_apc_atgm_action(stance, dcs_group, forward_heading, to_cp)
|
||||
@@ -452,11 +532,16 @@ class GroundConflictGenerator:
|
||||
if stance == CombatStance.RETREAT:
|
||||
# In retreat mode, the units will fall back
|
||||
# If the ally base is close enough, the units will even regroup there
|
||||
if from_cp.position.distance_to_point(dcs_group.points[0].position) <= RETREAT_DISTANCE:
|
||||
if (
|
||||
from_cp.position.distance_to_point(dcs_group.points[0].position)
|
||||
<= RETREAT_DISTANCE
|
||||
):
|
||||
retreat_point = from_cp.position.random_point_within(500, 250)
|
||||
else:
|
||||
retreat_point = self.find_retreat_point(dcs_group, forward_heading)
|
||||
reposition_point = retreat_point.point_from_heading(forward_heading, 10) # Another point to make the unit face the enemy
|
||||
reposition_point = retreat_point.point_from_heading(
|
||||
forward_heading, 10
|
||||
) # Another point to make the unit face the enemy
|
||||
dcs_group.add_waypoint(retreat_point, PointAction.OffRoad)
|
||||
dcs_group.add_waypoint(reposition_point, PointAction.OffRoad)
|
||||
|
||||
@@ -478,8 +563,10 @@ class GroundConflictGenerator:
|
||||
|
||||
# We add a new retreat waypoint
|
||||
dcs_group.add_waypoint(
|
||||
self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)),
|
||||
PointAction.OffRoad
|
||||
self.find_retreat_point(
|
||||
dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)
|
||||
),
|
||||
PointAction.OffRoad,
|
||||
)
|
||||
|
||||
# Fallback task
|
||||
@@ -503,7 +590,7 @@ class GroundConflictGenerator:
|
||||
self,
|
||||
dcs_group: VehicleGroup,
|
||||
frontline_heading: int,
|
||||
distance: int = RETREAT_DISTANCE
|
||||
distance: int = RETREAT_DISTANCE,
|
||||
) -> Point:
|
||||
"""
|
||||
Find a point to retreat to
|
||||
@@ -511,17 +598,15 @@ class GroundConflictGenerator:
|
||||
:param frontline_heading: Heading of the frontline
|
||||
:return: dcs.mapping.Point object with the desired position
|
||||
"""
|
||||
desired_point = dcs_group.points[0].position.point_from_heading(heading_sum(frontline_heading, +180), distance)
|
||||
desired_point = dcs_group.points[0].position.point_from_heading(
|
||||
heading_sum(frontline_heading, +180), distance
|
||||
)
|
||||
if self.conflict.theater.is_on_land(desired_point):
|
||||
return desired_point
|
||||
return self.conflict.theater.nearest_land_pos(desired_point)
|
||||
|
||||
|
||||
def find_offensive_point(
|
||||
self,
|
||||
dcs_group: VehicleGroup,
|
||||
frontline_heading: int,
|
||||
distance: int
|
||||
self, dcs_group: VehicleGroup, frontline_heading: int, distance: int
|
||||
) -> Point:
|
||||
"""
|
||||
Find a point to attack
|
||||
@@ -530,7 +615,9 @@ class GroundConflictGenerator:
|
||||
:param distance: Distance of the offensive (how far unit should move)
|
||||
:return: dcs.mapping.Point object with the desired position
|
||||
"""
|
||||
desired_point = dcs_group.points[0].position.point_from_heading(frontline_heading, distance)
|
||||
desired_point = dcs_group.points[0].position.point_from_heading(
|
||||
frontline_heading, distance
|
||||
)
|
||||
if self.conflict.theater.is_on_land(desired_point):
|
||||
return desired_point
|
||||
return self.conflict.theater.nearest_land_pos(desired_point)
|
||||
@@ -539,7 +626,7 @@ class GroundConflictGenerator:
|
||||
def find_n_nearest_enemy_groups(
|
||||
player_group: VehicleGroup,
|
||||
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
||||
n: int
|
||||
n: int,
|
||||
) -> List[VehicleGroup]:
|
||||
"""
|
||||
Return the nearest enemy group for the player group
|
||||
@@ -550,7 +637,9 @@ class GroundConflictGenerator:
|
||||
targets = [] # type: List[Optional[VehicleGroup]]
|
||||
sorted_list = sorted(
|
||||
enemy_groups,
|
||||
key=lambda group: player_group.points[0].position.distance_to_point(group[0].points[0].position)
|
||||
key=lambda group: player_group.points[0].position.distance_to_point(
|
||||
group[0].points[0].position
|
||||
),
|
||||
)
|
||||
for i in range(n):
|
||||
# TODO: Is this supposed to return no groups if enemy_groups is less than n?
|
||||
@@ -562,8 +651,7 @@ class GroundConflictGenerator:
|
||||
|
||||
@staticmethod
|
||||
def find_nearest_enemy_group(
|
||||
player_group: VehicleGroup,
|
||||
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
|
||||
player_group: VehicleGroup, enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
|
||||
) -> Optional[VehicleGroup]:
|
||||
"""
|
||||
Search the enemy groups for a potential target suitable to armored assault
|
||||
@@ -573,7 +661,9 @@ class GroundConflictGenerator:
|
||||
min_distance = 99999999
|
||||
target = None
|
||||
for dcs_group, _ in enemy_groups:
|
||||
dist = player_group.points[0].position.distance_to_point(dcs_group.points[0].position)
|
||||
dist = player_group.points[0].position.distance_to_point(
|
||||
dcs_group.points[0].position
|
||||
)
|
||||
if dist < min_distance:
|
||||
min_distance = dist
|
||||
target = dcs_group
|
||||
@@ -583,18 +673,20 @@ class GroundConflictGenerator:
|
||||
def get_artillery_target_in_range(
|
||||
dcs_group: VehicleGroup,
|
||||
group: CombatGroup,
|
||||
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
|
||||
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
||||
) -> Optional[Point]:
|
||||
"""
|
||||
Search the enemy groups for a potential target suitable to an artillery unit
|
||||
"""
|
||||
# TODO: Update to return a list of groups instead of a single point
|
||||
rng = group.units[0].threat_range
|
||||
rng = getattr(group.unit_type.dcs_unit_type, "threat_range", 0)
|
||||
if not enemy_groups:
|
||||
return None
|
||||
for _ in range(10):
|
||||
potential_target = random.choice(enemy_groups)[0]
|
||||
distance_to_target = dcs_group.points[0].position.distance_to_point(potential_target.points[0].position)
|
||||
distance_to_target = dcs_group.points[0].position.distance_to_point(
|
||||
potential_target.points[0].position
|
||||
)
|
||||
if distance_to_target < rng:
|
||||
return potential_target.points[0].position
|
||||
return None
|
||||
@@ -604,16 +696,16 @@ class GroundConflictGenerator:
|
||||
"""
|
||||
For artilery group, decide the distance from frontline with the range of the unit
|
||||
"""
|
||||
rg = group.units[0].threat_range - 7500
|
||||
rg = getattr(group.unit_type.dcs_unit_type, "threat_range", 0) - 7500
|
||||
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
||||
rg = random.randint(
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1],
|
||||
)
|
||||
elif rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
||||
rg = random.randint(
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][0],
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][1]
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][1],
|
||||
)
|
||||
return rg
|
||||
|
||||
@@ -623,51 +715,54 @@ class GroundConflictGenerator:
|
||||
combat_width: int,
|
||||
distance_from_frontline: int,
|
||||
heading: int,
|
||||
spawn_heading: int
|
||||
spawn_heading: int,
|
||||
):
|
||||
shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width))
|
||||
desired_point = shifted.point_from_heading(
|
||||
spawn_heading,
|
||||
distance_from_frontline
|
||||
shifted = conflict_position.point_from_heading(
|
||||
heading, random.randint(0, combat_width)
|
||||
)
|
||||
desired_point = shifted.point_from_heading(
|
||||
spawn_heading, distance_from_frontline
|
||||
)
|
||||
return Conflict.find_ground_position(
|
||||
desired_point, combat_width, heading, self.conflict.theater
|
||||
)
|
||||
return Conflict.find_ground_position(desired_point, combat_width, heading, self.conflict.theater)
|
||||
|
||||
|
||||
def _generate_groups(
|
||||
self,
|
||||
groups: List[CombatGroup],
|
||||
groups: list[CombatGroup],
|
||||
frontline_vector: Tuple[Point, int, int],
|
||||
is_player: bool
|
||||
is_player: bool,
|
||||
) -> List[Tuple[VehicleGroup, CombatGroup]]:
|
||||
"""Finds valid positions for planned groups and generates a pydcs group for them"""
|
||||
positioned_groups = []
|
||||
position, heading, combat_width = frontline_vector
|
||||
spawn_heading = int(heading_sum(heading, -90)) if is_player else int(heading_sum(heading, 90))
|
||||
spawn_heading = (
|
||||
int(heading_sum(heading, -90))
|
||||
if is_player
|
||||
else int(heading_sum(heading, 90))
|
||||
)
|
||||
country = self.game.player_country if is_player else self.game.enemy_country
|
||||
for group in groups:
|
||||
if group.role == CombatGroupRole.ARTILLERY:
|
||||
distance_from_frontline = self.get_artilery_group_distance_from_frontline(group)
|
||||
distance_from_frontline = (
|
||||
self.get_artilery_group_distance_from_frontline(group)
|
||||
)
|
||||
else:
|
||||
distance_from_frontline = random.randint(
|
||||
DISTANCE_FROM_FRONTLINE[group.role][0],
|
||||
DISTANCE_FROM_FRONTLINE[group.role][1]
|
||||
DISTANCE_FROM_FRONTLINE[group.role][0],
|
||||
DISTANCE_FROM_FRONTLINE[group.role][1],
|
||||
)
|
||||
|
||||
final_position = self.get_valid_position_for_group(
|
||||
position,
|
||||
combat_width,
|
||||
distance_from_frontline,
|
||||
heading,
|
||||
spawn_heading
|
||||
position, combat_width, distance_from_frontline, heading, spawn_heading
|
||||
)
|
||||
|
||||
if final_position is not None:
|
||||
g = self._generate_group(
|
||||
self.mission.country(country),
|
||||
group.units[0],
|
||||
len(group.units),
|
||||
group.unit_type,
|
||||
group.size,
|
||||
final_position,
|
||||
distance_from_frontline,
|
||||
heading=opposite_heading(spawn_heading),
|
||||
)
|
||||
if is_player:
|
||||
@@ -675,12 +770,14 @@ class GroundConflictGenerator:
|
||||
else:
|
||||
g.set_skill(self.game.settings.enemy_vehicle_skill)
|
||||
positioned_groups.append((g, group))
|
||||
self.gen_infantry_group_for_group(
|
||||
g,
|
||||
is_player,
|
||||
self.mission.country(country),
|
||||
opposite_heading(spawn_heading)
|
||||
)
|
||||
|
||||
if group.role in [CombatGroupRole.APC, CombatGroupRole.IFV]:
|
||||
self.gen_infantry_group_for_group(
|
||||
g,
|
||||
is_player,
|
||||
self.mission.country(country),
|
||||
opposite_heading(spawn_heading),
|
||||
)
|
||||
else:
|
||||
logging.warning(f"Unable to get valid position for {group}")
|
||||
|
||||
@@ -689,29 +786,29 @@ class GroundConflictGenerator:
|
||||
def _generate_group(
|
||||
self,
|
||||
side: Country,
|
||||
unit: VehicleType,
|
||||
unit_type: GroundUnitType,
|
||||
count: int,
|
||||
at: Point,
|
||||
distance_from_frontline,
|
||||
move_formation: PointAction = PointAction.OffRoad,
|
||||
heading=0,
|
||||
) -> VehicleGroup:
|
||||
|
||||
if side == self.conflict.attackers_country:
|
||||
cp = self.conflict.from_cp
|
||||
cp = self.conflict.blue_cp
|
||||
else:
|
||||
cp = self.conflict.to_cp
|
||||
cp = self.conflict.red_cp
|
||||
|
||||
logging.info("armorgen: {} for {}".format(unit, side.id))
|
||||
group = self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_unit_name(side, cp.id, unit), unit,
|
||||
position=at,
|
||||
group_size=count,
|
||||
heading=heading,
|
||||
move_formation=move_formation)
|
||||
side,
|
||||
namegen.next_unit_name(side, cp.id, unit_type),
|
||||
unit_type.dcs_unit_type,
|
||||
position=at,
|
||||
group_size=count,
|
||||
heading=heading,
|
||||
move_formation=move_formation,
|
||||
)
|
||||
|
||||
self.unit_map.add_front_line_units(group, cp)
|
||||
self.unit_map.add_front_line_units(group, cp, unit_type)
|
||||
|
||||
for c in range(count):
|
||||
vehicle: Vehicle = group.units[c]
|
||||
|
||||
45
gen/ato.py
45
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
|
||||
@@ -17,8 +19,10 @@ from typing import Dict, List, Optional
|
||||
from dcs.mapping import Point
|
||||
|
||||
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)
|
||||
@@ -141,9 +168,10 @@ class Package:
|
||||
# likely to be the main task than others. For example, a package with
|
||||
# only CAP flights is a CAP package, a flight with CAP and strike is a
|
||||
# strike package, a flight with CAP and DEAD is a DEAD package, and a
|
||||
# flight with strike and SEAD is an OCA/Strike package. The type of
|
||||
# package is determined by the highest priority flight in the package.
|
||||
task_priorities = [
|
||||
# flight with strike and SEAD is an OCA/Strike package. This list defines the
|
||||
# priority order for package task names. The package's primary task will be the
|
||||
# first task in this list that matches a flight in the package.
|
||||
tasks_by_priority = [
|
||||
FlightType.CAS,
|
||||
FlightType.STRIKE,
|
||||
FlightType.ANTISHIP,
|
||||
@@ -151,13 +179,16 @@ class Package:
|
||||
FlightType.OCA_RUNWAY,
|
||||
FlightType.BAI,
|
||||
FlightType.DEAD,
|
||||
FlightType.TRANSPORT,
|
||||
FlightType.SEAD,
|
||||
FlightType.TARCAP,
|
||||
FlightType.BARCAP,
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING,
|
||||
FlightType.SWEEP,
|
||||
FlightType.ESCORT,
|
||||
]
|
||||
for task in task_priorities:
|
||||
for task in tasks_by_priority:
|
||||
if flight_counts[task]:
|
||||
return task
|
||||
|
||||
|
||||
@@ -20,12 +20,15 @@ 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
|
||||
|
||||
@@ -33,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.
|
||||
|
||||
@@ -131,7 +137,6 @@ def format_waypoint_time(waypoint: FlightWaypoint, depart_prefix: str) -> str:
|
||||
|
||||
|
||||
class BriefingGenerator(MissionInfoGenerator):
|
||||
|
||||
def __init__(self, mission: Mission, game: Game):
|
||||
super().__init__(mission, game)
|
||||
self.allied_flights_by_departure: Dict[str, List[FlightData]] = {}
|
||||
@@ -141,36 +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 HandyWind
|
||||
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,
|
||||
HandyWind,
|
||||
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.Silkworm_SR,
|
||||
"SR#0",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Launchers
|
||||
for i, p in enumerate(positions):
|
||||
self.add_unit(
|
||||
MissilesSS.Silkworm_SR,
|
||||
"Missile#" + str(i),
|
||||
p[0],
|
||||
p[1],
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Commander
|
||||
self.add_unit(
|
||||
Unarmed.KAMAZ_Truck,
|
||||
"KAMAZ#0",
|
||||
self.position.x - 35,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Shorad
|
||||
self.add_unit(
|
||||
AirDefence.ZSU_23_4_Shilka,
|
||||
"SHILKA#0",
|
||||
self.position.x - 55,
|
||||
self.position.y - 38,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Shorad 2
|
||||
self.add_unit(
|
||||
AirDefence.Strela_1_9P31,
|
||||
"STRELA#0",
|
||||
self.position.x + 200,
|
||||
self.position.y + 15,
|
||||
90,
|
||||
)
|
||||
@@ -1,9 +1,9 @@
|
||||
import logging
|
||||
import random
|
||||
from typing import Tuple, Optional
|
||||
|
||||
from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
from shapely.geometry import LineString, Point as ShapelyPoint
|
||||
|
||||
from game.theater.conflicttheater import ConflictTheater, FrontLine
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
@@ -12,86 +12,134 @@ from game.utils import heading_sum, opposite_heading
|
||||
|
||||
FRONTLINE_LENGTH = 80000
|
||||
|
||||
|
||||
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: Optional[int] = None,
|
||||
size: Optional[int] = 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.size = size
|
||||
|
||||
@property
|
||||
def blue_cp(self) -> ControlPoint:
|
||||
return self.front_line.blue_cp
|
||||
|
||||
@property
|
||||
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, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int]:
|
||||
frontline = FrontLine(from_cp, to_cp, theater)
|
||||
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)
|
||||
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]:
|
||||
"""
|
||||
Returns a vector for a valid frontline location avoiding exclusion zones.
|
||||
"""
|
||||
center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
|
||||
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)
|
||||
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 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)
|
||||
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,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
front_line=front_line,
|
||||
attackers_side=attacker_name,
|
||||
defenders_side=defender_name,
|
||||
attackers_country=attacker,
|
||||
defenders_country=defender,
|
||||
size=distance
|
||||
size=distance,
|
||||
)
|
||||
return conflict
|
||||
|
||||
@classmethod
|
||||
def extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
|
||||
def extend_ground_position(
|
||||
cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater
|
||||
) -> Point:
|
||||
"""Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance"""
|
||||
pos = initial
|
||||
for distance in range(0, int(max_distance), 100):
|
||||
pos = initial.point_from_heading(heading, distance)
|
||||
if not theater.is_on_land(pos):
|
||||
return initial.point_from_heading(heading, distance - 100)
|
||||
return pos
|
||||
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]:
|
||||
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
|
||||
@@ -105,9 +153,10 @@ class Conflict:
|
||||
if theater.is_on_land(pos):
|
||||
return pos
|
||||
pos = initial.point_from_heading(opposite_heading(heading), distance)
|
||||
if theater.is_on_land(pos):
|
||||
return pos
|
||||
if coerce:
|
||||
pos = theater.nearest_land_pos(initial)
|
||||
return pos
|
||||
logging.error("Didn't find ground position ({})!".format(initial))
|
||||
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 TYPE_CHECKING
|
||||
|
||||
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 game.dcs.groundunittype import GroundUnitType
|
||||
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[GroundUnitType, 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.dcs_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.dcs_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
|
||||
@@ -1,24 +1,40 @@
|
||||
import random
|
||||
|
||||
from dcs.vehicles import Armor
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game import db
|
||||
from gen.defenses.armored_group_generator import ArmoredGroupGenerator, FixedSizeArmorGroupGenerator
|
||||
from game import db, Game
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater.theatergroundobject import VehicleGroupGroundObject
|
||||
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()]
|
||||
armor_types = (
|
||||
GroundUnitClass.Apc,
|
||||
GroundUnitClass.Atgm,
|
||||
GroundUnitClass.Ifv,
|
||||
GroundUnitClass.Tank,
|
||||
)
|
||||
possible_unit = [
|
||||
u for u in db.FACTIONS[faction].frontline_units if u.unit_class in armor_types
|
||||
]
|
||||
if len(possible_unit) > 0:
|
||||
unit_type = random.choice(possible_unit)
|
||||
return generate_armor_group_of_type(game, ground_object, unit_type)
|
||||
return None
|
||||
|
||||
|
||||
def generate_armor_group_of_type(game, ground_object, unit_type):
|
||||
def generate_armor_group_of_type(
|
||||
game: Game, ground_object: VehicleGroupGroundObject, unit_type: GroundUnitType
|
||||
) -> VehicleGroup:
|
||||
"""
|
||||
This generate a group of ground units of given type
|
||||
:return: Generated group
|
||||
@@ -28,7 +44,12 @@ def generate_armor_group_of_type(game, ground_object, unit_type):
|
||||
return generator.get_generated_group()
|
||||
|
||||
|
||||
def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size: int):
|
||||
def generate_armor_group_of_type_and_size(
|
||||
game: Game,
|
||||
ground_object: VehicleGroupGroundObject,
|
||||
unit_type: GroundUnitType,
|
||||
size: int,
|
||||
) -> VehicleGroup:
|
||||
"""
|
||||
This generate a group of ground units of given type and size
|
||||
:return: Generated group
|
||||
@@ -36,4 +57,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()
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import random
|
||||
|
||||
from game import Game
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater.theatergroundobject import VehicleGroupGroundObject
|
||||
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)
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
ground_object: VehicleGroupGroundObject,
|
||||
unit_type: GroundUnitType,
|
||||
) -> None:
|
||||
super().__init__(game, ground_object)
|
||||
self.unit_type = unit_type
|
||||
|
||||
def generate(self):
|
||||
|
||||
def generate(self) -> None:
|
||||
grid_x = random.randint(2, 3)
|
||||
grid_y = random.randint(1, 2)
|
||||
|
||||
@@ -20,15 +26,24 @@ 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.dcs_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)
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
ground_object: VehicleGroupGroundObject,
|
||||
unit_type: GroundUnitType,
|
||||
size: int,
|
||||
) -> None:
|
||||
super().__init__(game, ground_object)
|
||||
self.unit_type = unit_type
|
||||
self.size = size
|
||||
|
||||
@@ -38,7 +53,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.dcs_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 USS_Arleigh_Burke_IIa, TICONDEROG
|
||||
|
||||
|
||||
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(
|
||||
USS_Arleigh_Burke_IIa,
|
||||
"USS Ramage",
|
||||
self.position.x + 6482,
|
||||
self.position.y + 6667,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
self.add_unit(
|
||||
USS_Arleigh_Burke_IIa,
|
||||
"USS Mitscher",
|
||||
self.position.x - 7963,
|
||||
self.position.y + 7037,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
self.add_unit(
|
||||
USS_Arleigh_Burke_IIa,
|
||||
"USS Forrest Sherman",
|
||||
self.position.x - 7408,
|
||||
self.position.y - 7408,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
self.add_unit(
|
||||
USS_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(
|
||||
TICONDEROG,
|
||||
"USS Hué City",
|
||||
self.position.x + 2222,
|
||||
self.position.y - 3333,
|
||||
self.heading,
|
||||
)
|
||||
else:
|
||||
self.add_unit(
|
||||
TICONDEROG,
|
||||
"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
|
||||
|
||||
@@ -5,10 +5,9 @@ from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
from dcs.ships import (
|
||||
Type_052C_Destroyer,
|
||||
Type_052B_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
Type_052C,
|
||||
Type_052B,
|
||||
Type_054A,
|
||||
)
|
||||
|
||||
from game.factions.faction import Faction
|
||||
@@ -21,33 +20,54 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
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,
|
||||
"FF1",
|
||||
self.position.x + 1200,
|
||||
self.position.y + 900,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Type_054A,
|
||||
"FF2",
|
||||
self.position.x + 1200,
|
||||
self.position.y - 900,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
if include_dd:
|
||||
dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer])
|
||||
self.add_unit(dd_type, "DD1", self.position.x + 2400, self.position.y + 900, self.heading)
|
||||
self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading)
|
||||
|
||||
if include_cc:
|
||||
cc_type = random.choice([CGN_1144_2_Pyotr_Velikiy])
|
||||
self.add_unit(cc_type, "CC1", self.position.x, self.position.y, self.heading)
|
||||
dd_type = random.choice([Type_052C, Type_052B])
|
||||
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
|
||||
)
|
||||
|
||||
@@ -1,34 +1,59 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from dcs.unittype import ShipType
|
||||
from dcs.ships import Oliver_Hazzard_Perry_class, USS_Arleigh_Burke_IIa
|
||||
from dcs.ships import PERRY, USS_Arleigh_Burke_IIa
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
|
||||
class DDGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction, ddtype: ShipType):
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
ground_object: TheaterGroundObject,
|
||||
faction: Faction,
|
||||
ddtype: Type[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, 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, USS_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 La_Combattante_II
|
||||
|
||||
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, La_Combattante_II
|
||||
)
|
||||
@@ -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,14 +3,13 @@ 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
|
||||
ALBATROS,
|
||||
MOLNIYA,
|
||||
NEUSTRASH,
|
||||
REZKY,
|
||||
MOSCOW,
|
||||
KILO,
|
||||
SOM,
|
||||
)
|
||||
|
||||
from gen.fleet.dd_group import DDGroupGenerator
|
||||
@@ -24,7 +23,6 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
include_frigate = random.choice([True, True, False])
|
||||
@@ -35,38 +33,84 @@ 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([ALBATROS, MOLNIYA])
|
||||
self.add_unit(
|
||||
frigate_type,
|
||||
"FF1",
|
||||
self.position.x + 1200,
|
||||
self.position.y + 900,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
frigate_type,
|
||||
"FF2",
|
||||
self.position.x + 1200,
|
||||
self.position.y - 900,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
if include_dd:
|
||||
dd_type = random.choice([FFG_11540_Neustrashimy, FF_1135M_Rezky])
|
||||
self.add_unit(dd_type, "DD1", self.position.x + 2400, self.position.y + 900, self.heading)
|
||||
self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading)
|
||||
dd_type = random.choice([NEUSTRASH, 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(
|
||||
MOSCOW,
|
||||
"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, ALBATROS
|
||||
)
|
||||
|
||||
|
||||
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, 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, 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, SOM)
|
||||
|
||||
@@ -6,10 +6,15 @@ 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(
|
||||
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
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user