mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
1198 Commits
develop_2_
...
2.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e40b916b07 | ||
|
|
1bc994c102 | ||
|
|
21be4d38e1 | ||
|
|
3b58c571b3 | ||
|
|
e0430cf607 | ||
|
|
fb425d3524 | ||
|
|
45300b64c5 | ||
|
|
4b7ef46f82 | ||
|
|
b67e6d20f1 | ||
|
|
0ae6575087 | ||
|
|
08f67860be | ||
|
|
3fab1d92b7 | ||
|
|
6573157112 | ||
|
|
e83841eb0b | ||
|
|
6a0e18c0e9 | ||
|
|
b71b6473e3 | ||
|
|
c69e5e05a3 | ||
|
|
845e7fb956 | ||
|
|
f5cc2c3a37 | ||
|
|
d56c2f7a50 | ||
|
|
5c47a8f7e1 | ||
|
|
fb724e0150 | ||
|
|
94ef47c89d | ||
|
|
7e415b3fd7 | ||
|
|
fee497219e | ||
|
|
7624f09f98 | ||
|
|
946fe0a94c | ||
|
|
c7e1699546 | ||
|
|
de4a617743 | ||
|
|
228d62dd32 | ||
|
|
51f8a60096 | ||
|
|
83f1a95966 | ||
|
|
646ba94d10 | ||
|
|
8c5c08d678 | ||
|
|
3d0b47a181 | ||
|
|
ea7bece3b8 | ||
|
|
944a8e9cd6 | ||
|
|
88f7d1d572 | ||
|
|
6d78c1e302 | ||
|
|
791aa8b6d1 | ||
|
|
a078b67b36 | ||
|
|
34d4ecd4e6 | ||
|
|
5047b535c4 | ||
|
|
768d239840 | ||
|
|
89392553bd | ||
|
|
9a41217a59 | ||
|
|
f6557e4980 | ||
|
|
cc3cd95e2d | ||
|
|
8fb02136bb | ||
|
|
1c86585f03 | ||
|
|
242085966b | ||
|
|
6217075adc | ||
|
|
883f233c09 | ||
|
|
e4cc749180 | ||
|
|
9780798b75 | ||
|
|
225325cc29 | ||
|
|
2e3307065c | ||
|
|
f6a4316093 | ||
|
|
b80aad7449 | ||
|
|
5dbd1d093b | ||
|
|
64e2e1109e | ||
|
|
9c60140cec | ||
|
|
6099b664ac | ||
|
|
33d084eff0 | ||
|
|
07eb14eaa6 | ||
|
|
b2710fafd4 | ||
|
|
8f44b4571a | ||
|
|
169f010fae | ||
|
|
3e1547e0da | ||
|
|
825b9935ee | ||
|
|
2f2e086fbb | ||
|
|
2f6e0c15fe | ||
|
|
09e0c3dd63 | ||
|
|
4529ac9b92 | ||
|
|
8dac4eca55 | ||
|
|
bf56091dc7 | ||
|
|
f10f580f1c | ||
|
|
c09861d1ca | ||
|
|
8e8df2b846 | ||
|
|
d4a1d5bb9e | ||
|
|
84145aa7a7 | ||
|
|
25f32a4776 | ||
|
|
097c42d1dd | ||
|
|
91ac368a19 | ||
|
|
f959dd0519 | ||
|
|
444605920f | ||
|
|
a102d8b39f | ||
|
|
22eb861d28 | ||
|
|
779b36bf7b | ||
|
|
f36336403b | ||
|
|
b7fbade968 | ||
|
|
0535b20db7 | ||
|
|
ddd91e3078 | ||
|
|
97f734b8fc | ||
|
|
38941f02a8 | ||
|
|
84e09be199 | ||
|
|
995a89d370 | ||
|
|
6f11a269bc | ||
|
|
24a212a987 | ||
|
|
3282ba0302 | ||
|
|
a4db443f93 | ||
|
|
f8276f7e59 | ||
|
|
b545634d87 | ||
|
|
5da4cace94 | ||
|
|
1a2475dc25 | ||
|
|
2374239238 | ||
|
|
f84d77d334 | ||
|
|
5a275e6153 | ||
|
|
5f07069f1d | ||
|
|
1e1cebc3fc | ||
|
|
c40ad75fa2 | ||
|
|
c80db72bf7 | ||
|
|
727ec6bc28 | ||
|
|
9ebad734a9 | ||
|
|
0094628f6b | ||
|
|
abbb046566 | ||
|
|
a1136953d0 | ||
|
|
3298a5c6ad | ||
|
|
5e24fe9bb1 | ||
|
|
bc5b32ddef | ||
|
|
7269dbb79d | ||
|
|
6f5bb6ffa2 | ||
|
|
7d0b738918 | ||
|
|
1539d9c7ed | ||
|
|
4ae95e06ef | ||
|
|
fa1166d014 | ||
|
|
2d9e5fe984 | ||
|
|
7ae934e940 | ||
|
|
64b2eeface | ||
|
|
558dc591a3 | ||
|
|
0bfd766a0b | ||
|
|
7741713a7c | ||
|
|
454b540bce | ||
|
|
591c62b6d5 | ||
|
|
fdb4a7b055 | ||
|
|
f845ad9b31 | ||
|
|
f5f33ec865 | ||
|
|
d35faf15d7 | ||
|
|
7085bce6d4 | ||
|
|
a81890e844 | ||
|
|
8dbec21b02 | ||
|
|
a654c8229a | ||
|
|
f2e35c185b | ||
|
|
062c2643ad | ||
|
|
088c7b35ba | ||
|
|
ef439a6c42 | ||
|
|
40956a4042 | ||
|
|
8680e90e3b | ||
|
|
fac770424c | ||
|
|
042be9da6d | ||
|
|
81b0ea1eef | ||
|
|
52289d1283 | ||
|
|
1843d23203 | ||
|
|
1a32fef987 | ||
|
|
c740c8304b | ||
|
|
c3401d478b | ||
|
|
cf583bcd55 | ||
|
|
ef143a7ebb | ||
|
|
7aec483e73 | ||
|
|
db6a3b9849 | ||
|
|
657c5e1f52 | ||
|
|
3b0466d7cb | ||
|
|
8a9177b459 | ||
|
|
a0e63511d6 | ||
|
|
7c3f7d4b8e | ||
|
|
be1062c373 | ||
|
|
2bd5ab06a7 | ||
|
|
e174c1b147 | ||
|
|
7ef191be2a | ||
|
|
03a29aeedf | ||
|
|
d10b4c1e13 | ||
|
|
ab2046a2c2 | ||
|
|
bc6b2e0f3e | ||
|
|
746c99ebd6 | ||
|
|
34945e7eba | ||
|
|
507b217065 | ||
|
|
e222f17199 | ||
|
|
144cfecc0f | ||
|
|
64066bfc90 | ||
|
|
366ac4ee14 | ||
|
|
851984ee66 | ||
|
|
34bdc0e80b | ||
|
|
61d7d5e041 | ||
|
|
b5278550e7 | ||
|
|
461635c001 | ||
|
|
fcb1d8e104 | ||
|
|
6cbc2b707a | ||
|
|
9671542bdf | ||
|
|
de325c1208 | ||
|
|
802eff1faa | ||
|
|
068f9e42d7 | ||
|
|
4a483c3b27 | ||
|
|
e3bd958069 | ||
|
|
900cf0a9d0 | ||
|
|
64c424b9a6 | ||
|
|
5734c29312 | ||
|
|
362caa6ac1 | ||
|
|
b620976b70 | ||
|
|
daba4ef09e | ||
|
|
c697a34239 | ||
|
|
09b7cb3d85 | ||
|
|
d7e48662e0 | ||
|
|
9fd5c6f230 | ||
|
|
aa7825d4aa | ||
|
|
436725b38e | ||
|
|
922d935bc1 | ||
|
|
3716395453 | ||
|
|
69833f66e3 | ||
|
|
ec787b913c | ||
|
|
89f313295e | ||
|
|
7bc7a44c72 | ||
|
|
317a882386 | ||
|
|
3a9f585b6b | ||
|
|
7bbb1c0822 | ||
|
|
d3b1f6110f | ||
|
|
a6dc3d2aff | ||
|
|
d946a9e526 | ||
|
|
17dd1b193e | ||
|
|
d634fd3236 | ||
|
|
e861e5b3d6 | ||
|
|
6045f4dd91 | ||
|
|
8be2841bdf | ||
|
|
b6e37b9e67 | ||
|
|
0d0d582bd8 | ||
|
|
0c42227e5e | ||
|
|
98ac4bd5c8 | ||
|
|
a43b100781 | ||
|
|
c7f9bfbb43 | ||
|
|
b5f8e6925b | ||
|
|
993e59413a | ||
|
|
9f2fab78a1 | ||
|
|
3bdf1377c0 | ||
|
|
8f24cf07be | ||
|
|
17c40234e9 | ||
|
|
4cecddcdd0 | ||
|
|
1f4516b954 | ||
|
|
1d76ee4871 | ||
|
|
b53cac4c7a | ||
|
|
29a0644719 | ||
|
|
c833078e71 | ||
|
|
e4cba8d19f | ||
|
|
cd6620712f | ||
|
|
85619b156d | ||
|
|
10debbc286 | ||
|
|
dcac5b488a | ||
|
|
e1009bdafa | ||
|
|
38ce842ca8 | ||
|
|
aafd09569c | ||
|
|
67a9df686e | ||
|
|
9a374711fd | ||
|
|
b9138acbc8 | ||
|
|
2a65916f7c | ||
|
|
6aa1f1cca0 | ||
|
|
8c1ebfda02 | ||
|
|
81af5d7497 | ||
|
|
368bf08ade | ||
|
|
d95f623ca9 | ||
|
|
2856fbc42b | ||
|
|
ac59e15bd9 | ||
|
|
91d9bbdc97 | ||
|
|
575f4e1786 | ||
|
|
bff905fae5 | ||
|
|
c0fa135bf6 | ||
|
|
86394d8f19 | ||
|
|
72c233cb0d | ||
|
|
04e2c02eff | ||
|
|
7362744df2 | ||
|
|
01951b5c32 | ||
|
|
f2f52771bd | ||
|
|
b59167d3ca | ||
|
|
88e466562c | ||
|
|
1f85e5d7f8 | ||
|
|
50471d510e | ||
|
|
8b7cf2f725 | ||
|
|
282a5109ba | ||
|
|
3d3b4738d9 | ||
|
|
66149bb591 | ||
|
|
b0ad664ece | ||
|
|
7c29ea836c | ||
|
|
92e9e8c56a | ||
|
|
12bf26223d | ||
|
|
56d7993c8f | ||
|
|
52b63927b4 | ||
|
|
86558bdef6 | ||
|
|
e46262b021 | ||
|
|
c53feb5ccb | ||
|
|
fc6d4f0990 | ||
|
|
df948bde9d | ||
|
|
203a720ae1 | ||
|
|
3410f08cfb | ||
|
|
a553914ef4 | ||
|
|
21220141f2 | ||
|
|
caf2d8436b | ||
|
|
4cc305fa81 | ||
|
|
60f837d0b9 | ||
|
|
05bd7f8e6b | ||
|
|
e58ab34a15 | ||
|
|
d960758ef3 | ||
|
|
a36ccdcc39 | ||
|
|
d582948377 | ||
|
|
d806e0b1c3 | ||
|
|
b9e110a7e3 | ||
|
|
a2f218d56d | ||
|
|
2c475011a1 | ||
|
|
2d7fc33726 | ||
|
|
0c8d1e1dc4 | ||
|
|
bb04ce2abb | ||
|
|
9850b22c0a | ||
|
|
a2f65666a5 | ||
|
|
7730809dbb | ||
|
|
2ac818dcdd | ||
|
|
113947b9f0 | ||
|
|
44bc2d769b | ||
|
|
02ecfebb85 | ||
|
|
a1fed62591 | ||
|
|
778ed6ad91 | ||
|
|
7d539f5810 | ||
|
|
b407acbc07 | ||
|
|
3260260dce | ||
|
|
70c1290993 | ||
|
|
6bae60c51e | ||
|
|
a45adb6b3a | ||
|
|
476aaf5d3e | ||
|
|
58187b6969 | ||
|
|
f3a3d81d96 | ||
|
|
7a40b54153 | ||
|
|
9dd62d3538 | ||
|
|
76e4a6ed83 | ||
|
|
7a9eb06677 | ||
|
|
26f54e7619 | ||
|
|
117b7ae414 | ||
|
|
baeac324d6 | ||
|
|
0db0f003dc | ||
|
|
2d4f341710 | ||
|
|
b8a41dc937 | ||
|
|
2f2bb0de4f | ||
|
|
3b76d7f47e | ||
|
|
10b74e507f | ||
|
|
8a03a9462b | ||
|
|
e5bca224e9 | ||
|
|
197bf5d0cf | ||
|
|
d8b15ebcdb | ||
|
|
078466241f | ||
|
|
57c3eb5d2c | ||
|
|
a4876167c4 | ||
|
|
a38a5654a9 | ||
|
|
69a41879bb | ||
|
|
e3524a506b | ||
|
|
8447c563ea | ||
|
|
fd61a4b23a | ||
|
|
9257311896 | ||
|
|
23e870e416 | ||
|
|
8270b28d85 | ||
|
|
1a0889d3d9 | ||
|
|
5382d99a94 | ||
|
|
3e4bb88089 | ||
|
|
2f3f53a978 | ||
|
|
89755b1005 | ||
|
|
a7203ea90a | ||
|
|
afb0ac14c4 | ||
|
|
745dfc71bc | ||
|
|
82d9689d1b | ||
|
|
6afaef1654 | ||
|
|
bb42d86012 | ||
|
|
5b44580061 | ||
|
|
4eac743812 | ||
|
|
ed8ab37bd5 | ||
|
|
563c3f0f1b | ||
|
|
296e6e8e8f | ||
|
|
334aab2755 | ||
|
|
419f4f3156 | ||
|
|
ec5a26e8dd | ||
|
|
2b7cd36eea | ||
|
|
2f11731052 | ||
|
|
23a0846533 | ||
|
|
666858f8e2 | ||
|
|
2288b7f7b2 | ||
|
|
498af28efb | ||
|
|
3902ab3375 | ||
|
|
6bb0bdf66e | ||
|
|
b9ade2295e | ||
|
|
44b5f5a919 | ||
|
|
17d37494c2 | ||
|
|
f0d81e98a0 | ||
|
|
e3b13f7b4a | ||
|
|
ba2686630a | ||
|
|
e195cfa6a0 | ||
|
|
b9fbd1906f | ||
|
|
1f611bafef | ||
|
|
af7faa59dc | ||
|
|
0b21ee46ea | ||
|
|
f64996a350 | ||
|
|
d7cccd1980 | ||
|
|
b9467d9236 | ||
|
|
69096b15ae | ||
|
|
a075e62bad | ||
|
|
1ebe367e07 | ||
|
|
db229f25bf | ||
|
|
787c93b9d4 | ||
|
|
63953992a9 | ||
|
|
567cb0c0b6 | ||
|
|
f37999a3ef | ||
|
|
545761e3d4 | ||
|
|
97ea67d01d | ||
|
|
561d679a62 | ||
|
|
991cd91dd4 | ||
|
|
9e51ff0253 | ||
|
|
1a1e55e16c | ||
|
|
2fe4a39784 | ||
|
|
7d81b9ef5c | ||
|
|
b355b6dc60 | ||
|
|
819148762b | ||
|
|
4112a86fe9 | ||
|
|
7838c9b49b | ||
|
|
ca5a70e3bc | ||
|
|
d6376c3a91 | ||
|
|
e134143f16 | ||
|
|
0f1577d314 | ||
|
|
793b356c01 | ||
|
|
a36858f3ea | ||
|
|
aaa6637435 | ||
|
|
f0f6739cf8 | ||
|
|
25efdd3d4f | ||
|
|
0b2483ea15 | ||
|
|
2c38ce910c | ||
|
|
4d26ec0789 | ||
|
|
edba923f2f | ||
|
|
7d907aac0f | ||
|
|
d6981550a8 | ||
|
|
8b0636367b | ||
|
|
a6c9d0f9bc | ||
|
|
4b0d2f7abc | ||
|
|
dd28781b69 | ||
|
|
ece56032f1 | ||
|
|
61ebe9780e | ||
|
|
e887082501 | ||
|
|
8e3039dd37 | ||
|
|
1d1c130d19 | ||
|
|
1fd3f70eec | ||
|
|
d9ea33cbb9 | ||
|
|
80778aa267 | ||
|
|
445cb4f146 | ||
|
|
95db2aa14f | ||
|
|
4b0fc637eb | ||
|
|
48d6b4cfa1 | ||
|
|
fc11182bbe | ||
|
|
25b72e1af4 | ||
|
|
1848338ef7 | ||
|
|
ff0446cc12 | ||
|
|
08ceb57c31 | ||
|
|
affb332eb9 | ||
|
|
6455c38ff4 | ||
|
|
f62c2fbabb | ||
|
|
d5276c9d4a | ||
|
|
4c0fc5a407 | ||
|
|
f608cd5aef | ||
|
|
0371b62acb | ||
|
|
eddd66b5c4 | ||
|
|
c8e71d269b | ||
|
|
ae034d5387 | ||
|
|
07e5c568c4 | ||
|
|
817d6a0e15 | ||
|
|
31fdd24c3e | ||
|
|
bfa0e4ba49 | ||
|
|
dff98f0b53 | ||
|
|
d0856ff279 | ||
|
|
c89ff2c3d6 | ||
|
|
4ec88d524a | ||
|
|
d6b762efa7 | ||
|
|
b476a26759 | ||
|
|
48c218b430 | ||
|
|
1a062e2170 | ||
|
|
fe658eb877 | ||
|
|
d316836e90 | ||
|
|
8443f61f0a | ||
|
|
feed55186f | ||
|
|
56591b8655 | ||
|
|
7c52ca15f3 | ||
|
|
06c751f214 | ||
|
|
9d774eaad8 | ||
|
|
babfd4abda | ||
|
|
a029c165a0 | ||
|
|
9b51533d96 | ||
|
|
e1b7e0eb00 | ||
|
|
508a5693c9 | ||
|
|
67806f3d76 | ||
|
|
f687a30c7e | ||
|
|
15615a1077 | ||
|
|
905175c210 | ||
|
|
409e070887 | ||
|
|
f659dc1f76 | ||
|
|
b8922b39fd | ||
|
|
8137d57cdf | ||
|
|
bf290ac1a9 | ||
|
|
77fda00233 | ||
|
|
0255088e30 | ||
|
|
b74d8b12d0 | ||
|
|
2834f2982c | ||
|
|
7d07faa5fb | ||
|
|
fd70f0fc4a | ||
|
|
7744f84e85 | ||
|
|
441ef79aa4 | ||
|
|
c54f6ba4d2 | ||
|
|
61ecdfc48c | ||
|
|
5e2b259af1 | ||
|
|
1258f3e17c | ||
|
|
44ed895277 | ||
|
|
c74e18e449 | ||
|
|
2ea3f914f0 | ||
|
|
d6e4a50064 | ||
|
|
6296896471 | ||
|
|
473cda971a | ||
|
|
cf570adabe | ||
|
|
02196f2883 | ||
|
|
8b49752401 | ||
|
|
8c64867918 | ||
|
|
e544063c40 | ||
|
|
edfaaacd04 | ||
|
|
84b8613cf5 | ||
|
|
aea82e2266 | ||
|
|
b0b9c1c8e6 | ||
|
|
b5ff32c5b6 | ||
|
|
e0223ded54 | ||
|
|
2012ad0aa3 | ||
|
|
15d72a8dcb | ||
|
|
e525b11695 | ||
|
|
8f30e60e1b | ||
|
|
ce977ac937 | ||
|
|
aa9ffa0855 | ||
|
|
6a8ca810ff | ||
|
|
f2d2fd7014 | ||
|
|
ddd06b3162 | ||
|
|
d519aa1dad | ||
|
|
7226359e64 | ||
|
|
f396ff7f12 | ||
|
|
1adee0af17 | ||
|
|
bac47dad83 | ||
|
|
f1a2602cfd | ||
|
|
b8e64d4369 | ||
|
|
72ac8ca872 | ||
|
|
ccb41829c9 | ||
|
|
90697194a1 | ||
|
|
13f4baa34e | ||
|
|
76840ff5c2 | ||
|
|
72ac806cb8 | ||
|
|
bf275fe564 | ||
|
|
68818ae50d | ||
|
|
7315d097c2 | ||
|
|
cdf28700cf | ||
|
|
948c1d0bb0 | ||
|
|
de0a3f929c | ||
|
|
c3023a9f99 | ||
|
|
4f37610dfb | ||
|
|
aef4316f72 | ||
|
|
378dbf254a | ||
|
|
f0b6a37ce2 | ||
|
|
ff12a120e6 | ||
|
|
d04be4d71b | ||
|
|
581aaaad28 | ||
|
|
453eb9feb4 | ||
|
|
4059ee44b8 | ||
|
|
7a222ecfa0 | ||
|
|
be15e9adf2 | ||
|
|
2fd097c613 | ||
|
|
66ee5f5392 | ||
|
|
3bb08f8d30 | ||
|
|
d7787adddc | ||
|
|
1f37b879b1 | ||
|
|
45ce28f9bf | ||
|
|
4e87bed4e5 | ||
|
|
a7421fc670 | ||
|
|
208a7550ef | ||
|
|
37e23f70d6 | ||
|
|
7daefa8ae5 | ||
|
|
292ac42003 | ||
|
|
c501c45c52 | ||
|
|
29b894f8b0 | ||
|
|
55573bf40a | ||
|
|
f2c2ef82c5 | ||
|
|
d6b33d353c | ||
|
|
2ed1c36c54 | ||
|
|
7cbcbc1171 | ||
|
|
b74dcfa053 | ||
|
|
62bf7eb227 | ||
|
|
07bfe8e29a | ||
|
|
0b258997dd | ||
|
|
69421ad7a1 | ||
|
|
fdf571c016 | ||
|
|
bd60760f9d | ||
|
|
e8aa9839b0 | ||
|
|
fcdb22db5b | ||
|
|
e73cf68def | ||
|
|
fa5b842cc7 | ||
|
|
43a21cb341 | ||
|
|
45361b57a7 | ||
|
|
046c7a662a | ||
|
|
a9f1de13b1 | ||
|
|
edbe2d86f2 | ||
|
|
4e12a1cdad | ||
|
|
d24c7ea93e | ||
|
|
484f1e8d51 | ||
|
|
8d5abb877c | ||
|
|
fd454dce74 | ||
|
|
5d4fccd438 | ||
|
|
9f078e1483 | ||
|
|
0e807d84c2 | ||
|
|
3ad57d995b | ||
|
|
28cf42aeb8 | ||
|
|
7fcf74a8ed | ||
|
|
a0d38f7465 | ||
|
|
cd97526d2b | ||
|
|
87fdc16f9b | ||
|
|
b69eb02766 | ||
|
|
7636234649 | ||
|
|
2bd673a531 | ||
|
|
a1b64bc72d | ||
|
|
80bc9d6b23 | ||
|
|
ee768b9147 | ||
|
|
7dfb0c67e5 | ||
|
|
75ea5cc462 | ||
|
|
afabf6fd00 | ||
|
|
0eb4519797 | ||
|
|
611f04ab5a | ||
|
|
a9ba2deafa | ||
|
|
0c4e920af3 | ||
|
|
ef0e565337 | ||
|
|
02e7ab41b4 | ||
|
|
59bd4541c4 | ||
|
|
ca30af4238 | ||
|
|
718b3f2623 | ||
|
|
6e153c6451 | ||
|
|
4a1809d56e | ||
|
|
a2bf0c1bea | ||
|
|
f0d9dae33b | ||
|
|
b99462b628 | ||
|
|
65ac30acda | ||
|
|
2072b6fa63 | ||
|
|
4f604ba687 | ||
|
|
b9fe559b42 | ||
|
|
efcdbebda5 | ||
|
|
8886850c60 | ||
|
|
20276e5230 | ||
|
|
ed96bc83b4 | ||
|
|
c0147f5eb7 | ||
|
|
5bf5f024cb | ||
|
|
4628e8320a | ||
|
|
9c1d36d18a | ||
|
|
789b618e37 | ||
|
|
d0804a6f9e | ||
|
|
17fe977b06 | ||
|
|
60783ca390 | ||
|
|
34a7a37409 | ||
|
|
e68d2b5deb | ||
|
|
b0317055e7 | ||
|
|
6e0af7c144 | ||
|
|
9394ed663a | ||
|
|
967574820f | ||
|
|
da17d1e5d1 | ||
|
|
63bdbebcaa | ||
|
|
c67263662d | ||
|
|
2484457183 | ||
|
|
d7b328b887 | ||
|
|
493e53c28f | ||
|
|
7438c30885 | ||
|
|
fac43ba20b | ||
|
|
c2eb243026 | ||
|
|
2adaee8671 | ||
|
|
57edc5678c | ||
|
|
730130b19e | ||
|
|
17b0cee507 | ||
|
|
2557383946 | ||
|
|
29d3b5dfc6 | ||
|
|
f6fad30852 | ||
|
|
d5a081a15f | ||
|
|
6147e9ac96 | ||
|
|
b32ca4f92f | ||
|
|
b57dd51f86 | ||
|
|
fc6ca162af | ||
|
|
58ba9e9d1d | ||
|
|
939b6c468d | ||
|
|
bf7df6721a | ||
|
|
f6e0dbbb6a | ||
|
|
851c2d88a9 | ||
|
|
200c13dc31 | ||
|
|
8889e35f9e | ||
|
|
398fa1e73d | ||
|
|
316f73138c | ||
|
|
708b615ad7 | ||
|
|
866ff78518 | ||
|
|
f0480b033f | ||
|
|
4d19548736 | ||
|
|
fcf45554ef | ||
|
|
799b0fae94 | ||
|
|
0d95716545 | ||
|
|
8c5b808eba | ||
|
|
007dcf548e | ||
|
|
9a640bf7eb | ||
|
|
edd02d9dd6 | ||
|
|
75edbb62f1 | ||
|
|
2b6227f3b1 | ||
|
|
c4b8a41742 | ||
|
|
f8b2dbe283 | ||
|
|
20091292f4 | ||
|
|
a594f45aae | ||
|
|
d394d01ea8 | ||
|
|
70d982b0ed | ||
|
|
ae68a35a1a | ||
|
|
a9fcfe60f4 | ||
|
|
c3b028ef4b | ||
|
|
833399f068 | ||
|
|
5695cf4ac5 | ||
|
|
1553e5efd5 | ||
|
|
0f1b396dd2 | ||
|
|
976ee51bf5 | ||
|
|
7c22f6e83b | ||
|
|
18b6f7b84c | ||
|
|
206d09f7f8 | ||
|
|
4e910c4b09 | ||
|
|
bc3cd50a6c | ||
|
|
cecf611f91 | ||
|
|
5e4802f05e | ||
|
|
1e70e654ed | ||
|
|
20054b9825 | ||
|
|
5fb6a53cbd | ||
|
|
ff751c30f9 | ||
|
|
c1614ad5a7 | ||
|
|
13e372159a | ||
|
|
1ee0aafd9a | ||
|
|
df80ec635f | ||
|
|
6524286f04 | ||
|
|
87248fec53 | ||
|
|
6470d25d18 | ||
|
|
b7634a8ac3 | ||
|
|
434755a620 | ||
|
|
3eb2529b0b | ||
|
|
e6e4cca076 | ||
|
|
8d57bbc777 | ||
|
|
483db564f9 | ||
|
|
63d5862319 | ||
|
|
365b379798 | ||
|
|
20f97e48a9 | ||
|
|
fd473f0a46 | ||
|
|
94c5ed8bdc | ||
|
|
4b7b4bf110 | ||
|
|
a816877d08 | ||
|
|
2d56ae1cb6 | ||
|
|
a223da8f99 | ||
|
|
216adcc35a | ||
|
|
8d485d5fa2 | ||
|
|
fa549fcf94 | ||
|
|
98fd707aea | ||
|
|
5928f29f11 | ||
|
|
c3ebabbe44 | ||
|
|
29fd094dd0 | ||
|
|
f7966b8d8c | ||
|
|
ee2f4ecbc8 | ||
|
|
b08f6cad1d | ||
|
|
e851223733 | ||
|
|
1f12546ff4 | ||
|
|
8345063e84 | ||
|
|
482bedd739 | ||
|
|
7d7a334418 | ||
|
|
69dbe62b70 | ||
|
|
f61167cedf | ||
|
|
2ac92a75a4 | ||
|
|
fe80a9fd08 | ||
|
|
505af7635f | ||
|
|
426dc69e1d | ||
|
|
082e8c062c | ||
|
|
9f2409bb9e | ||
|
|
14dd8e43a4 | ||
|
|
9fb33526a7 | ||
|
|
8bd00bf450 | ||
|
|
f3553ced78 | ||
|
|
a52dc43c9e | ||
|
|
0b6b40a358 | ||
|
|
f6371d2ef1 | ||
|
|
ecd073e31d | ||
|
|
b7d160631a | ||
|
|
dc235f36c8 | ||
|
|
7503c1e1e9 | ||
|
|
253e8a209c | ||
|
|
e26b692631 | ||
|
|
658d808524 | ||
|
|
28e00055ab | ||
|
|
8eef1eaa7c | ||
|
|
e361a857a4 | ||
|
|
d369ce8847 | ||
|
|
d05897edcb | ||
|
|
1d98432c57 | ||
|
|
e60166dc89 | ||
|
|
87afc2fcef | ||
|
|
bd1457c435 | ||
|
|
c1f88b4a5f | ||
|
|
a080d4b692 | ||
|
|
c20e9e19cb | ||
|
|
d9056acc6d | ||
|
|
21cd764f66 | ||
|
|
dfc31dfd5c | ||
|
|
8ffbf32677 | ||
|
|
6e2124252c | ||
|
|
3dd07b8c23 | ||
|
|
2e067aada6 | ||
|
|
78cd60f3df | ||
|
|
2891649531 | ||
|
|
4b40739918 | ||
|
|
96c401e1b9 | ||
|
|
fad132dcca | ||
|
|
fae9650f56 | ||
|
|
e26e7f53c5 | ||
|
|
696710bf41 | ||
|
|
9019cbfd2b | ||
|
|
f8735927bf | ||
|
|
16cfc4e945 | ||
|
|
85f931316a | ||
|
|
717ea05d38 | ||
|
|
3987274764 | ||
|
|
73a97f9c2a | ||
|
|
80612ba97d | ||
|
|
c4d2b92e34 | ||
|
|
169fba9ab8 | ||
|
|
0b902e19ee | ||
|
|
ab26a76789 | ||
|
|
1f43fbe16e | ||
|
|
f29cb99530 | ||
|
|
ef84703da9 | ||
|
|
9e32ea7413 | ||
|
|
75769df8e2 | ||
|
|
95fd4cab05 | ||
|
|
a81254cd18 | ||
|
|
4cff838de0 | ||
|
|
3838b3ca4f | ||
|
|
7dc3e041c8 | ||
|
|
33b92423d8 | ||
|
|
6237fffa5a | ||
|
|
0b4e2d3b6b | ||
|
|
c4d08fa7b7 | ||
|
|
cec28351e7 | ||
|
|
398630d51e | ||
|
|
61400ba726 | ||
|
|
33885e2216 | ||
|
|
5719b136fe | ||
|
|
ede5ee60c3 | ||
|
|
9620ac7e7e | ||
|
|
c0bfdbf4bb | ||
|
|
efb544a303 | ||
|
|
adfc4b7244 | ||
|
|
7a5ce98569 | ||
|
|
1fcceb0901 | ||
|
|
22c552053f | ||
|
|
818c679d4f | ||
|
|
3ff36c45aa | ||
|
|
623d461b06 | ||
|
|
7535013848 | ||
|
|
a63bac8826 | ||
|
|
d2c831c4ee | ||
|
|
840b5ce071 | ||
|
|
be6abc0025 | ||
|
|
ef585c59dd | ||
|
|
16d9c1ccad | ||
|
|
9a9ef78583 | ||
|
|
fe7ee5b610 | ||
|
|
680804040a | ||
|
|
0d4fe73daa | ||
|
|
407190c6c5 | ||
|
|
73998dbde0 | ||
|
|
8827f7df34 | ||
|
|
4c394a9e2d | ||
|
|
5946fc7404 | ||
|
|
5b8ecb2c14 | ||
|
|
61253e4d4d | ||
|
|
2c0ca5803f | ||
|
|
690f3d0f13 | ||
|
|
42c259bc58 | ||
|
|
11426a0713 | ||
|
|
e6af1b8645 | ||
|
|
103f18191d | ||
|
|
fb312236a2 | ||
|
|
c850c0095d | ||
|
|
58481268f7 | ||
|
|
11604671f8 | ||
|
|
e8feded4c3 | ||
|
|
18f9b38d25 | ||
|
|
676eea3ccc | ||
|
|
ee113d080e | ||
|
|
8bc69415a7 | ||
|
|
3979ee57ff | ||
|
|
c2ee169d16 | ||
|
|
56b51c85bb | ||
|
|
853ee5aac4 | ||
|
|
4cf406aefa | ||
|
|
1abb341cb6 | ||
|
|
44dce9598c | ||
|
|
aefc8685a1 | ||
|
|
26761342f5 | ||
|
|
ca777bcebb | ||
|
|
5742075ff2 | ||
|
|
c7a6ec9691 | ||
|
|
e0153cfa6a | ||
|
|
3fd5e1bae7 | ||
|
|
d9511a7edd | ||
|
|
040db055fd | ||
|
|
b9f8cfd10d | ||
|
|
ff46556927 | ||
|
|
d1815a3d6e | ||
|
|
fdfa4827ab | ||
|
|
5d579ccef9 | ||
|
|
4145d5578e | ||
|
|
43eb041bb8 | ||
|
|
0b8ac8fc47 | ||
|
|
de3ba5908f | ||
|
|
bbb6251aa9 | ||
|
|
6f71d92a7b | ||
|
|
a8b59cc567 | ||
|
|
f4d3660eac | ||
|
|
1f165835c6 | ||
|
|
9087f3487d | ||
|
|
4ca92ea22d | ||
|
|
e2682d633f | ||
|
|
e6cb1b5970 | ||
|
|
de2d548139 | ||
|
|
4d1a0b85e4 | ||
|
|
5cfbd8c3ad | ||
|
|
95f72be8eb | ||
|
|
95c4dfa52f | ||
|
|
b72a2f4a5f | ||
|
|
844f8595d1 | ||
|
|
1c9d9be667 | ||
|
|
2a02a743a4 | ||
|
|
968d9365d6 | ||
|
|
cdb16cc591 | ||
|
|
86bc41c15c | ||
|
|
ac05c7cfaa | ||
|
|
9c07fe5963 | ||
|
|
ed05f995b5 | ||
|
|
85491dca20 | ||
|
|
465399f803 | ||
|
|
3550c8a8f6 | ||
|
|
739c0f8f52 | ||
|
|
49aa79c612 | ||
|
|
cdde75b517 | ||
|
|
dde74af6b5 | ||
|
|
eff9c77c9a | ||
|
|
5ba633c8a1 | ||
|
|
ab67a38ca5 | ||
|
|
08f0c9d30a | ||
|
|
9d747a9f9b | ||
|
|
31ca121498 | ||
|
|
44b5f5acf1 | ||
|
|
eb4878dfc4 | ||
|
|
3dc7dc3d1a | ||
|
|
6a6133e5cd | ||
|
|
65c85d7f0b | ||
|
|
d519dfa5da | ||
|
|
73ea83bbdd | ||
|
|
235a5ec538 | ||
|
|
f81a3d03c0 | ||
|
|
6878b57fba | ||
|
|
0143e5641f | ||
|
|
5adc92c601 | ||
|
|
0b2fbddbc5 | ||
|
|
28035bf02b | ||
|
|
6c9a9de3f3 | ||
|
|
62139fc4eb | ||
|
|
88b9ed29ba | ||
|
|
d94c57afd6 | ||
|
|
b6421646ff | ||
|
|
611b6fc272 | ||
|
|
9cdbef9faf | ||
|
|
b34de70fc7 | ||
|
|
f5047fc0cc | ||
|
|
258c34e61d | ||
|
|
f365487fd6 | ||
|
|
8f65b7ee7c | ||
|
|
9397f1f39c | ||
|
|
f03121af5a | ||
|
|
239b9f8234 | ||
|
|
1620c602cf | ||
|
|
fa01303460 | ||
|
|
1e5bd916d9 | ||
|
|
62d89239fc | ||
|
|
91bee4e6c2 | ||
|
|
a465dde32f | ||
|
|
254dd5f70f | ||
|
|
1a70ed5121 | ||
|
|
16d5a550ce | ||
|
|
794cc43a41 | ||
|
|
8583bbf893 | ||
|
|
63d510f2ea | ||
|
|
2c6b26003b | ||
|
|
fdaf3bc30f | ||
|
|
816d9696b5 | ||
|
|
bdaa6a294a | ||
|
|
a6b15b9529 | ||
|
|
878529e8f8 | ||
|
|
2cb37b5bd8 | ||
|
|
de95cfc981 | ||
|
|
0477247cf2 | ||
|
|
e0319a4047 | ||
|
|
839f163ac5 | ||
|
|
ba9ad4c371 | ||
|
|
bdcf7c1828 | ||
|
|
821e9fc114 | ||
|
|
334d21897b | ||
|
|
b405c3ab32 | ||
|
|
3cabb1e02d | ||
|
|
ec7f8f5710 | ||
|
|
93f0627c5e | ||
|
|
3d8c2d689e | ||
|
|
e1572c09ff | ||
|
|
910af12fb9 | ||
|
|
acbd45341f | ||
|
|
901c89371c | ||
|
|
743534bdda | ||
|
|
98bb1a9f65 | ||
|
|
bfc602f22f | ||
|
|
dd2b61edf3 | ||
|
|
0fd58135fd | ||
|
|
4672252242 | ||
|
|
a0c61bf73a | ||
|
|
d6c19a8aff | ||
|
|
b6a933e264 | ||
|
|
0191eca9dc | ||
|
|
f962fd55bc | ||
|
|
33fa719e8d | ||
|
|
adb9352905 | ||
|
|
58574c67df | ||
|
|
04d3ba4c47 | ||
|
|
0f1d2b8685 | ||
|
|
c06a855113 | ||
|
|
e9bfd58ee1 | ||
|
|
5f02febb6c | ||
|
|
15db12fb21 | ||
|
|
c3fca6696d | ||
|
|
dd4c37cde3 | ||
|
|
aa7ffdabb0 | ||
|
|
85f6616185 | ||
|
|
c8955bdca7 | ||
|
|
669a8c9e64 | ||
|
|
b4d0eb0b99 | ||
|
|
035bebaab8 | ||
|
|
b20200318b | ||
|
|
9caf83cda9 | ||
|
|
493f9df4e2 | ||
|
|
cc61893bca | ||
|
|
3ba2fb76aa | ||
|
|
e024da277b | ||
|
|
bc1e793ce6 | ||
|
|
aa7eacc043 | ||
|
|
dd7b9f1790 | ||
|
|
d6b94345d9 | ||
|
|
aa1ac56ec3 | ||
|
|
fd969020af | ||
|
|
95f486870d | ||
|
|
f6d049da3c | ||
|
|
69f15824ca | ||
|
|
8c70d1ab79 | ||
|
|
58fd651a0b | ||
|
|
177b505cb7 | ||
|
|
8ac5dbe22a | ||
|
|
b744238fb8 | ||
|
|
57b7402753 | ||
|
|
dcaa390d24 | ||
|
|
59010f6949 | ||
|
|
6a91fad10a | ||
|
|
f03029417d | ||
|
|
f5aa342602 | ||
|
|
53582ba539 | ||
|
|
44c976948d | ||
|
|
41d5020467 | ||
|
|
1bd26005f2 | ||
|
|
b1840ce2ca | ||
|
|
769246c55d | ||
|
|
24394d4d00 | ||
|
|
cab5825b72 | ||
|
|
84beb2dfe5 | ||
|
|
f8ac39fb82 | ||
|
|
eb69d01067 | ||
|
|
1c4f255c7f | ||
|
|
4125f6ec06 | ||
|
|
5023e0d30f | ||
|
|
f65595c626 | ||
|
|
916d1eec96 | ||
|
|
c2d615315e | ||
|
|
aa96ce7134 | ||
|
|
01f83e8451 | ||
|
|
ed92e9afb9 | ||
|
|
064890c0a2 | ||
|
|
8617f48fc2 | ||
|
|
2269cf0f08 | ||
|
|
3d41eb1ab4 | ||
|
|
cace523aa8 | ||
|
|
002f55dc04 | ||
|
|
49b6951ac3 | ||
|
|
7aa17e5ad6 | ||
|
|
9db41270f3 | ||
|
|
e4852c74ab | ||
|
|
613f84aa3c | ||
|
|
2814876976 | ||
|
|
69bf3999aa | ||
|
|
2fa3b26119 | ||
|
|
5a027c552e | ||
|
|
9efecf9514 | ||
|
|
8b87c43869 | ||
|
|
f7fec834e6 | ||
|
|
de43a1215c | ||
|
|
8dc531bb7f | ||
|
|
da2584d7ee | ||
|
|
373924a959 | ||
|
|
f75032bd79 | ||
|
|
4203dc5d41 | ||
|
|
9fe1f8ff90 | ||
|
|
bc825f760d | ||
|
|
191199d9de | ||
|
|
883a66a792 | ||
|
|
411e71b9a2 | ||
|
|
5463505787 | ||
|
|
ec6fc076de | ||
|
|
5a245bf362 | ||
|
|
d95912322c | ||
|
|
9c58e73b39 | ||
|
|
3c4ccd7d57 | ||
|
|
d22943d755 | ||
|
|
974b6590d8 | ||
|
|
d414c00b74 | ||
|
|
7b79d183eb | ||
|
|
edd56cb407 | ||
|
|
c777204f50 | ||
|
|
55f12f20c1 | ||
|
|
1ac062653d | ||
|
|
b0a176a22c | ||
|
|
f4b07cb518 | ||
|
|
a0ff78a810 | ||
|
|
f22391855b | ||
|
|
1fa18447e1 | ||
|
|
2d8c8c63c9 | ||
|
|
31d5e3151b | ||
|
|
c77bfe9da2 | ||
|
|
9a7dfc55e3 | ||
|
|
63bc3bd46e | ||
|
|
b5e5a3b2da | ||
|
|
7abe32be5c | ||
|
|
f0279a6866 | ||
|
|
5ce942c9a0 | ||
|
|
59d6cc7625 | ||
|
|
4abf806837 | ||
|
|
6bfb8cf2fd | ||
|
|
1d7f1082ea | ||
|
|
1c4aec83cb | ||
|
|
e537396fec | ||
|
|
944748a0ac | ||
|
|
023925d741 | ||
|
|
93db1254ec | ||
|
|
e0725ff139 | ||
|
|
9e96aee89f | ||
|
|
db6b660270 | ||
|
|
1808e5bccf | ||
|
|
5f1601a2da | ||
|
|
60ce6658ad | ||
|
|
e664652cc5 | ||
|
|
ca48a42701 | ||
|
|
00ea8ac4e1 | ||
|
|
de5238e89a | ||
|
|
3bb1327a65 | ||
|
|
71f77dd8fb | ||
|
|
9101dae38a | ||
|
|
1f18bf2bd8 | ||
|
|
41445c3092 | ||
|
|
f040804d02 | ||
|
|
e27625556c | ||
|
|
b13711ddef | ||
|
|
6ce82be46b | ||
|
|
56a5864600 | ||
|
|
07cbaa3e70 | ||
|
|
2aecea88b0 | ||
|
|
582c43fb6c | ||
|
|
cc7c2cc707 | ||
|
|
8b717c4f4c | ||
|
|
aa309af015 | ||
|
|
1e041b6249 | ||
|
|
1f240b02f4 | ||
|
|
6317f376b7 | ||
|
|
5ecf9aeed8 | ||
|
|
db36a76c2c | ||
|
|
3df8fb5fe9 | ||
|
|
5ba2e8a7a1 | ||
|
|
07d4b126f5 | ||
|
|
6e14ec3227 | ||
|
|
028292a023 | ||
|
|
a38f2e36a2 | ||
|
|
5807fbf896 | ||
|
|
2c1dc6a18d | ||
|
|
a3c06ce6e0 | ||
|
|
0e1dfb8ccb | ||
|
|
8a4a81a008 | ||
|
|
ff083942e8 | ||
|
|
0eee5747af | ||
|
|
80f2b7a1db |
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
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.
|
||||
|
||||
**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 locaed in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`).
|
||||
|
||||
**Version information (please complete the following information):**
|
||||
- DCS Liberation [e.g. 2.3.1]:
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
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.
|
||||
|
||||
**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.
|
||||
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -18,15 +18,29 @@ jobs:
|
||||
|
||||
- name: Install environment
|
||||
run: |
|
||||
py -m venv ./venv
|
||||
python -m venv ./venv
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
pip install -r requirements.txt
|
||||
python -m pip install -r requirements.txt
|
||||
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
|
||||
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force
|
||||
|
||||
- name: mypy game
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy game
|
||||
|
||||
- name: mypy gen
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy gen
|
||||
|
||||
- name: update build number
|
||||
run: |
|
||||
[IO.File]::WriteAllLines($pwd.path + "\resources\buildnumber", $env:GITHUB_RUN_NUMBER)
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
|
||||
33
.github/workflows/release.yml
vendored
33
.github/workflows/release.yml
vendored
@@ -20,15 +20,29 @@ jobs:
|
||||
|
||||
- name: Install environment
|
||||
run: |
|
||||
py -m venv ./venv
|
||||
python -m venv ./venv
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
pip install -r requirements.txt
|
||||
python -m pip install -r requirements.txt
|
||||
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
|
||||
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force
|
||||
|
||||
- name: Finalize version
|
||||
run: |
|
||||
New-Item -ItemType file resources\final
|
||||
|
||||
- name: mypy game
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy game
|
||||
|
||||
- name: mypy gen
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy gen
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
@@ -39,11 +53,6 @@ jobs:
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref }}
|
||||
run: |
|
||||
$version = ($env:TAG_NAME -split "/") | Select-Object -Last 1
|
||||
(Get-Content .\installer\dcs_liberation.iss) -replace "{{version}}",$version | Out-File .\build\installer.iss
|
||||
cd .\installer
|
||||
iscc.exe ..\build\installer.iss
|
||||
cd ..
|
||||
Copy-Item .\changelog.md .\dist
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
@@ -86,15 +95,7 @@ jobs:
|
||||
body_path: releasenotes.md
|
||||
draft: false
|
||||
prerelease: ${{ steps.version.outputs.prerelease }}
|
||||
|
||||
- uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./dcs_liberation.exe
|
||||
asset_name: dcs_liberation.${{ steps.version.outputs.number }}.exe
|
||||
asset_content_type: application/exe
|
||||
|
||||
|
||||
- uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,10 +5,10 @@ resources/payloads/*.lua
|
||||
venv
|
||||
logs.txt
|
||||
.DS_Store
|
||||
.vscode/settings.json
|
||||
dist/**
|
||||
a.py
|
||||
resources/tools/a.miz
|
||||
tests/**
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
|
||||
@@ -16,9 +16,8 @@ tests/**
|
||||
/liberation_preferences.json
|
||||
/state.json
|
||||
|
||||
logs/liberation.log
|
||||
logs/
|
||||
|
||||
qt_ui/logs/liberation.log
|
||||
|
||||
*.psd
|
||||
resources/scripts/plugins/*
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"python.pythonPath": "g:\\python\\dcs_liberation\\venv\\Scripts\\python.exe",
|
||||
"vsintellicode.python.completionsEnabled": true
|
||||
}
|
||||
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/Khopa/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.
|
||||
20
README.md
20
README.md
@@ -12,15 +12,25 @@
|
||||

|
||||
|
||||
## About DCS Liberation
|
||||
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player semi dynamic campaign.
|
||||
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
|
||||
|
||||
To download preview builds of the next version of DCS Liberation, see https://github.com/Khopa/dcs_liberation/wiki/Preview-builds.
|
||||
|
||||
## 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/Khopa/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/Khopa/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/Khopa/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/)
|
||||
@@ -31,6 +41,10 @@ First, a big thanks to shdwp, for starting the original DCS Liberation project.
|
||||
|
||||
Then, DCS Liberation uses [pydcs](http://github.com/pydcs/dcs) for mission generation, and nothing would be possible without this.
|
||||
It also uses the popular [Mist](https://github.com/mrSkortch/MissionScriptingTools) lua framework for mission scripting.
|
||||
And for the JTAC feature, DCS Liberation embed Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
|
||||
|
||||
Excellent lua scripts DCS Liberation uses as plugins:
|
||||
|
||||
* For the JTAC feature, DCS Liberation embeds Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
|
||||
* Walder's [Skynet-IADS](https://github.com/walder/Skynet-IADS) is used for Integrated Air Defense System.
|
||||
|
||||
Please also show some support to these projects !
|
||||
|
||||
269
changelog.md
269
changelog.md
@@ -1,3 +1,266 @@
|
||||
# 2.4.2
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Factions]** Introduction dates and fallback weapons added for US, Russian, UK, and French weapons. Huge thanks to @TheCandianVendingMachine for the massive amount of data entry!
|
||||
* **[Campaigns]** Added 1995 start dates.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Economy]** Pending ground unit purchases will also be transferred when a connected base is captured.
|
||||
* **[UI]** Fixed rounding of budget in recruitment menu.
|
||||
|
||||
# 2.4.1
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Units]** Fixed syntax error with the SH-60B payload file.
|
||||
* **[Culling]** Missile sites generate reasonably sized non-cull zones rather than 100km ones.
|
||||
* **[UI]** Budget display is also now rounded to 2 decimal places.
|
||||
* **[UI]** Fixed some areas where the old, non-pretty name was displayed to users.
|
||||
|
||||
# 2.4.0
|
||||
|
||||
Saves from 2.3 are not compatible with 2.4.
|
||||
|
||||
## Highlights
|
||||
|
||||
* Improved flight plan generation to avoid loitering in or traveling through threatened areas when practical.
|
||||
* Improved AI aircraft purchasing behavior.
|
||||
* Era-restricted weapons (work in progress).
|
||||
* Tons of UI polish.
|
||||
* Rebalanced economy to keep opfor competitive over the course of the game.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Flight Planner]** Air-to-air and SEAD escorts will no longer be automatically planned for packages that are not in range of threats.
|
||||
* **[Flight Planner]** Non-custom flight plans will now navigate around threat areas en route to the target area when practical.
|
||||
* **[Flight Planner]** Flight plans along front lines now ensure that the race track start is closer to the departure airfield than the race track end.
|
||||
* **[Campaign AI]** Auto-purchase now prefers airfields that are not within range of the enemy.
|
||||
* **[Campaign AI]** Auto-purchase now prefers the best aircraft for the task, but will attempt to maintain some variety.
|
||||
* **[Campaign AI]** Opfor now sells off odd aircraft since they're unlikely to be used.
|
||||
* **[Campaign AI]** Multiple rounds of CAP will be planned (roughly 90 minutes of coverage). Default starting budget has increased to account for the increased need for aircraft.
|
||||
* **[Mission Generator]** Multiple groups are created for complex SAM sites (SAMs with additional point defense or SHORADS), improving Skynet behavior.
|
||||
* **[Mission Generator]** Default start type can now be chosen in the settings. This replaces the non-functional "AI Parking Start" option. **Selecting any type other than cold will break OCA/Aircraft missions.**
|
||||
* **[Cheat Menu]** Added ability to toggle base capture and frontline advance/retreat cheats.
|
||||
* **[Skynet]** Updated to 2.0.1.
|
||||
* **[Skynet]** Point defenses are now configured to remain on to protect the site they accompany.
|
||||
* **[Hercules]** Updated the Hercules Cargo list file.
|
||||
* **[Balance]** Opfor now gains income using the same rules as the player, significantly increasing their income relative to the player for most campaigns.
|
||||
* **[Balance]** Units now retreat from captured bases when able. Units with no retreat path will be captured and sold.
|
||||
* **[Economy]** FOBs generate only $10M per turn (previously $20M like airbases).
|
||||
* **[Economy]** Carriers and off-map spawns generate no income (previously $20M like airbases).
|
||||
* **[Economy]** Sales of aircraft and ground vehicles can now be cancelled before the next turn begins.
|
||||
* **[UI]** Multi-SAM objectives now show threat and detection rings per group.
|
||||
* **[UI]** New icon for AA sites with no active threat.
|
||||
* **[UI]** Unit names are now prettier and more accurate, and can now be set per-country for added historical flavour.
|
||||
* **[UI]** Default loadout is now shown for flights with no custom loadout selected.
|
||||
* **[UI]** Aircraft for a new flight are now only selectable if they match the task type for that flight.
|
||||
* **[UI]** WIP - There is now a unit info button for each unit in the recruitment list, that should help newer players learn what each unit does.
|
||||
* **[UI]** Docs for time-on-target and creating new theaters/factions/loadouts are now linked in the UI at the appropriate places.
|
||||
* **[UI]** ASAP is now a checkbox rather than a button. Enabling this will disable the TOT selector but changes to the package structure will automatically re-ASAP the package.
|
||||
* **[UI]** Arrival airfield is now shown in the flight list if it differs from the departure airfield.
|
||||
* **[UI]** Start type can now be selected when creating a flight.
|
||||
* **[UI]** Arrival and divert airfields can be edited after the flight is created.
|
||||
* **[Factions]** Added option for date-based loadout restriction. Active radar homing missiles are handled, patches welcome for the other thousand weapons.
|
||||
* **[Factions]** Added Poland 2010 faction.
|
||||
* **[Factions]** Added Greece 2005 faction.
|
||||
* **[Factions]** Added Iran 1988 faction.
|
||||
* **[Units]** Support for E-2 Hawkeye, SH-60B Seahawk, S-3B Viking (thanks to awinterquest) and SpGH Dana - these are now being used by appropriate factions.
|
||||
* **[Culling]** Missile sites are no longer culled.
|
||||
* **[Campaigns]** Added campaign "Black Sea Lite" by Starfire
|
||||
* **[Campaigns]** Added campaign "Exercise Vegas Nerve" by Starfire
|
||||
* **[New game Wizard]** The theater page is now the first page of the campaign wizard, recommended factions will be selected automatically on the faction selection page
|
||||
* **[New game Wizard]** Added information text about the selected campaign performance.
|
||||
* **[Mod Support]** Added support for High Digit SAMs mod 1.4.0
|
||||
* **[Mod Support]** Added SAMs sites generator : KS19Generator, SA10BGenerator, SA12Generator, SA17Generator, SA20Generator, SA20BGenerator, SA23Generator
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Hercules]** Updated the default Hercules radio frequency.
|
||||
* **[Economy]** Pending unit orders at captured bases will be refunded.
|
||||
* **[UI]** Carrier group SAM threat rings now move with the carrier.
|
||||
* **[UI]** Base intel menu no longer compresses text, and is now scrollable.
|
||||
* **[UI]** Edit Flight window is now dynamically sized to adapt to the width of waypoint names, so they no longer get truncated.
|
||||
* **[UI]** Budget income display is now rounded to 2 decimal places.
|
||||
* **[UI]** Fixed incorrect income per turn displayed for strike target tooltip.
|
||||
* **[Factions]** USA with C-130 faction now links to the required mod.
|
||||
* **[Campaign]** Fixed issue where destroyed buildings would sometimes not count as destroyed and thus respawn.
|
||||
* **[Campaign]** Fixed issue where destroyed runways were not registered.
|
||||
* **[Units]** J-11A is no longer spawned with empty loadout.
|
||||
* **[Units]** F-14B is no longer spawned with empty loadout for fighter sweep tasks.
|
||||
* **[Units]** Pyotr Velikiy cruiser has been removed for now as it's nearly unkillable.
|
||||
* **[Units]** Submarines have been removed for now as they aren't wholly functional.
|
||||
* **[Units]** Fixed "FACTION ERROR : Unable to find OliverHazardPerryGroupGenerator in pydcs" error at startup.
|
||||
* **[Mission Generator]** Fixed a bug where units set to Aggressive stance sometimes did not move.
|
||||
* **[Mission Generator]** Flyover points for OCA/Aircraft missions are now generated correctly.
|
||||
* **[Flight Planner]** Fixed not being able to create custom waypoints for buildings.
|
||||
* **[Flight Planner]** Strike missions will no longer be automatically planned against SAMs.
|
||||
* **[Flight Planner]** Strike missions will no longer be automatically planned against FOB structures.
|
||||
|
||||
# 2.3.4
|
||||
|
||||
## Fixes:
|
||||
[Mission Generator] Mission generator would crash when generating fire missions for destroyed SCUD sites - fixed
|
||||
|
||||
# 2.3.3
|
||||
|
||||
## Features/Improvements
|
||||
* **[Campaigns]** Reworked Golan Heights campaign on Syria, (Added FOB and preset locations for SAMS)
|
||||
* **[Campaigns]** Added a lite version of the Golan Heights campaign
|
||||
* **[Campaigns]** Reworked Syrian Civil War campaign (Added FOB and preset locations for SAMS)
|
||||
* **[Campaigns]** Reworked Emirates campaign
|
||||
* **[Campaigns]** AA units added to frontlines and updated all factions to include some frontline AA units.
|
||||
* **[Mission Generator]** Infantry will only be generated for APC and IFV groups
|
||||
* **[Mission Generator]** Infantry squads size is not randomized anymore
|
||||
* **[Mission Generator]** Infantry squads can have a mortar.
|
||||
* **[Mission Generator]** SCUD missiles sites will now fire on enemy controls points in range when possible
|
||||
* **[Factions]** Updated Nato Desert Storm to include F-14A
|
||||
* **[Factions]** Updated Iraq 1991 factions to include Zsu-57 and Mig-29A
|
||||
* **[Factions]** Germany 1944, added Stug III and Stug IV
|
||||
* **[Factions]** Added factions Insurgents (Hard) with better and more weapons
|
||||
* **[Plugins]** [The EWRS plugin](https://github.com/Bob7heBuilder/EWRS) is now included.
|
||||
* **[UI]** Added enemy intelligence summary and details window.
|
||||
|
||||
## Fixes:
|
||||
* **[Factions]** AI would never buy artillery units for the frontline - fixed
|
||||
* **[Factions]** Removed the F-111 unit from the NATO desert storm faction. (Recruiting it would cause crashes in DCS, since it is not a valid unit)
|
||||
* **[Campaign]** Automatic redeployment of ground units would sometimes fail - fixed
|
||||
* **[Mission Generator]** Artillery groups would retreat in the wrong direction - fixed
|
||||
* **[Units]** Fixed SPG_Stryker_M1128_MGS not being in db
|
||||
* **[UI]** Fixed and added many missing ground units icons
|
||||
* **[UI]** Ship groups could be replaced by SAM sites in the UI, which would lead to broken mission being generated - fixed
|
||||
* **[New Game Wizard]** Removed the "mid game" campaign generator option which is currently broken
|
||||
* **[Mission Generator]** Empty navy groups will no longer be generated
|
||||
* **[Mission Generator]** Fixed BAI, SEAD, and DEAD flights ocassionally being assigned the wrong targets.
|
||||
* **[Flight Planner]** Fixed not being able to plan packages against opfor carriers
|
||||
* **[UI]** Repaired SAMs no longer show as dead.
|
||||
* **[UI]** Fixed not being able to manage a disbanded site after disbanding and closing the base menu.
|
||||
|
||||
# 2.3.2
|
||||
|
||||
## Features/Improvements
|
||||
* **[Units]** Support for newly added BTR-82A, T-72B3
|
||||
* **[Units]** Added ZSU-57 AAA sites
|
||||
* **[Culling]** BARCAP missions no longer create culling exclusion zones.
|
||||
* **[Flight Planner]** Improved TOT planning. Negative start times no longer occur with TARCAPs and hold times no longer affect planning for flight plans without hold points.
|
||||
* **[Factions]** Added Iraq 1991 faction (thanks again to Hawkmoon!)
|
||||
|
||||
## Fixes:
|
||||
* **[Mission Generator]** Fix mission generation error when there are too many radio frequency to setup for the Mig-21
|
||||
* **[Mission Generator]** Fix ground units not moving forward
|
||||
* **[Mission Generator]** Fixed assigned radio channels overlapping with beacons.
|
||||
* **[Flight Planner]** Fix creation of custom waypoints.
|
||||
* **[Campaigns]** Fixed many cases of SAMs spawning on the runways/taxiways in Syria Full.
|
||||
|
||||
# 2.3.1
|
||||
|
||||
## Features/Improvements
|
||||
* **[UX]** Added a warning message when the player is attempting to buy more planes at an already full airbase.
|
||||
* **[Campaigns]** Migrated Syria full map to new format. (Thanks to Hawkmoon)
|
||||
* **[Faction]** Added NATO desert Storm faction (Thanks to Hawkmoon)
|
||||
|
||||
## Fixes:
|
||||
* **[AI]** CAP flights will engage enemies again.
|
||||
* **[Campaigns]** Fixed a missing path on the Caucasus Full Map campaign
|
||||
|
||||
# 2.3.0
|
||||
|
||||
## Features/Improvements
|
||||
* **[Campaign Map]** Overhauled the campaign model
|
||||
* **[Campaign Map]** Possible to add FOB as control points
|
||||
* **[Campaign Map]** Added off-map spawn locations
|
||||
* **[Campaign AI]** Overhauled AI recruiting behaviour
|
||||
* **[Campaign AI]** Added AI procurement for Blue
|
||||
* **[Campaign]** New Campaign: "Black Sea"
|
||||
* **[Mission Planner]** Possible to move carrier and tarawa on the campaign map
|
||||
* **[Mission Generator]** Infantry squads on frontline can have manpads
|
||||
* **[Mission Generator]** Unused aircraft now spawned to allow for OCA strikes
|
||||
* **[Mission Generator]** Opfor now obeys parking limits
|
||||
* **[Mission Generator]** Support for Anubis C-130 Hercules mod
|
||||
* **[Flight Planner]** Added fighter sweep missions.
|
||||
* **[Flight Planner]** Added BAI missions.
|
||||
* **[Flight Planner]** Added anti-ship missions.
|
||||
* **[Flight Planner]** Differentiated BARCAP and TARCAP. TARCAP is now for hostile areas and will arrive before the package.
|
||||
* **[Flight Planner]** Added OCA missions
|
||||
* **[Flight Planner]** Added Alternate/divert airfields
|
||||
* **[Culling]** Added possibility to include/exclude carriers from culling zones
|
||||
* **[QOL]** On liberation startup, your latest save game is loaded automatically
|
||||
* **[Units]** Reduced starting fuel load for C101
|
||||
* **[UI]** Inform the user of the weather
|
||||
* **[UI]** Added toolbar buttons to change map display settings
|
||||
* **[Game]** Added new Economy options for adjusting income multipliers and starting budgets.
|
||||
|
||||
## Fixes :
|
||||
* **[Map]** Missiles sites now have a proper icon and will not re-use the SAM sites icon
|
||||
* **[Mission Generator]** Ground unit waypoints improperly set to "On Road" - fixed
|
||||
* **[Mission Generator]** Target waypoints not at ground level - fixed
|
||||
* **[Mission Generator]** Selected skill not applied to Helicopters - fixed
|
||||
* **[Mission Generator]** Ground units do not always spawn - fixed
|
||||
* **[Kneeboard]** Briefing waypoints off by one - fixed
|
||||
* **[Game]** Destroyed buildings still granting budget - fixed
|
||||
|
||||
# 2.2.1
|
||||
|
||||
## Features/Improvements
|
||||
* **[Factions]** Added factions : Georgia 2008, USN 1985, France 2005 Frenchpack by HerrTom
|
||||
* **[Factions]** Added map Persian Gulf full by Plob
|
||||
* **[Flight Planner]** Player flights with start delays under ten minutes will spawn immediately.
|
||||
* **[UI]** Mission start screen now informs players about delayed flights.
|
||||
* **[Units]** Added support for F-14A-135-GR
|
||||
* **[Modding]** Possible to setup liveries overrides in factions definition files
|
||||
|
||||
## Fixes :
|
||||
* **[Flight Planner]** Hold, join, and split points are planned cautiously near enemy airfields. Ascend/descend points are no longer planned.
|
||||
* **[Flight Planner]** Custom waypoints are usable again. Not that in most cases custom flight plans will revert to the 2.1 flight planning behavior.
|
||||
* **[Flight Planner]** Fixed UI bug that made it possible to create empty flights which would throw an error.
|
||||
* **[Flight Planner]** Player flights from carriers will now be delayed correctly according to the player's settings.
|
||||
* **[Misc]** Spitfire variant with clipped wings was not seen as flyable by DCS Liberation (hence could not be setup as client/player slot)
|
||||
* **[Misc]** Updated Syria terrain parking slots database, the out-of-date database could end up generating aircraft in wrong slots (We are still experiencing issues with somes airbases, such as Khalkhalah though)
|
||||
|
||||
# 2.2.0
|
||||
|
||||
## Features/Improvements :
|
||||
* **[Campaign Generator]** Added early warning radar generation
|
||||
* **[Campaign Generator]** Added scud launcher sites
|
||||
* **[Cheat Menu]** Added ability to capture base from mission planner
|
||||
* **[Cheat Menu]** Added ability to show red ATO
|
||||
* **[Factions]** Added WW2 factions that do not depend on WW2 asset pack
|
||||
* **[Factions]** Cold War / Middle eastern factions will use Flak sites
|
||||
* **[Flight Planner]** Flight planner overhaul, with package and TOT system
|
||||
* **[Flight Planner]** Pick runways and ascent/descent based on headwind
|
||||
* **[Map]** Added polygon debug mode display
|
||||
* **[Map]** Highlight the selected flight path on the map
|
||||
* **[Map]** Improved SAM display settings
|
||||
* **[Map]** Improved flight plan display settings
|
||||
* **[Map]** Caucasus and The Channel map use a new system to generate SAM and strike target location to reduce probability of targets generated in the middle of a forests
|
||||
* **[Misc]** Flexible Dedicated Hosting Options for Mission Files via environment variables
|
||||
* **[Moddability]** Custom campaigns can be designed through json files
|
||||
* **[Moddability]** LUA plugins can now be injected into Liberation missions.
|
||||
* **[Moddability]** Optional Skynet IADS lua plugin now included
|
||||
* **[New Game]** Starting budget can be freely selected
|
||||
* **[New Game]** Exanded information for faction and campaign selection in the new game wizard
|
||||
* **[UI]** Add double and right click actions to many UI elements.
|
||||
* **[UI]** Add polygon drawing mode for map background
|
||||
* **[UI]** Added a warning if you press takeoff with no player enabled flights
|
||||
* **[UI]** Packages and flights now visible in the main window sidebar
|
||||
* **[Units/Factions]** Added bombers to some coalitions
|
||||
* **[Units/Factions]** Added support for SU-57 mod by Cubanace
|
||||
* **[Units]** Added Freya EWR sites to german WW2 factions
|
||||
* **[Units]** Added support for many bombers (B-52H, B-1B, Tu-22, Tu-142)
|
||||
* **[Units]** Added support for new P-47 variants
|
||||
|
||||
## Fixes :
|
||||
* **[Campaign Generator]** Big airbases could end up without any airbase defense.
|
||||
* **[Campaign generator]** Ship group and offshore buildings should not be generated on land anymore
|
||||
* **[Flight Planner]** Fix waypoint alitudes for helicopters
|
||||
* **[Flight Planner]** Fixed CAS aircraft wandering away from frontline
|
||||
* **[Maps]** Incirlik airbase was missing exclusions zones, so SAMS could end up being generated on the runway
|
||||
* **[Mission Generator]** Fixed player/client confusion when a flight had only one player slot.
|
||||
* **[Radios]** Fix A-10C radio
|
||||
* **[UI]** Many missing unit icons were added
|
||||
* **[UI]** Missing TER weapons in custom payload now selectable.
|
||||
|
||||
# 2.1.5
|
||||
|
||||
## Features/Improvements :
|
||||
@@ -10,9 +273,7 @@
|
||||
# 2.1.4
|
||||
|
||||
## Fixes :
|
||||
* **[UI]** Fixed an issue that prevent generating the mission (take off button no working) on old savegames.
|
||||
|
||||
# 2.1.3
|
||||
* **[UI]** Fixed an issue that prevented generating the mission (take off button no working) on old savegames.
|
||||
|
||||
## Features/Improvements :
|
||||
* **[Units/Factions]** Added A-10C_2 to USA 2005 and Bluefor modern factions
|
||||
@@ -276,4 +537,4 @@ Sorry :(
|
||||
* **[Mission Generator]** Planned flights will spawn even if their home base has been captured or is being contested by enemy ground units.
|
||||
* **[Campaign Generator]** Base defenses would not be generated on Normandy map and in some rare cases on others maps as well
|
||||
* **[Mission Planning]** CAS waypoints created from the "Predefined waypoint selector" would not be at the exact location of the frontline
|
||||
* **[Naming]** CAP mission flown from airbase are not named BARCAP anymore (CAP from carrier is still named BARCAP)
|
||||
* **[Naming]** CAP mission flown from airbase are not named BARCAP anymore (CAP from carrier is still named BARCAP)
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from .game import Game
|
||||
from . import db
|
||||
from . import db
|
||||
from .version import VERSION
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import inspect
|
||||
import dcs
|
||||
|
||||
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick', 'aa']
|
||||
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick']
|
||||
|
||||
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'aa']
|
||||
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'aa']
|
||||
WW2_FREE = ['fuel', 'factory', 'ware', 'fob']
|
||||
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'fob']
|
||||
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'fob']
|
||||
|
||||
FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2',
|
||||
'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5',
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
from dcs.planes import *
|
||||
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
|
||||
|
||||
"""
|
||||
@@ -23,6 +43,8 @@ GUNFIGHTERS = [
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
Bf_109K_4,
|
||||
|
||||
@@ -1,95 +1,133 @@
|
||||
from game.utils import nm_to_meter, feet_to_meter
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
|
||||
MODERN_DOCTRINE = {
|
||||
from game.utils import Distance, feet, nautical_miles
|
||||
|
||||
"GENERATORS": {
|
||||
"CAS": True,
|
||||
"CAP": True,
|
||||
"SEAD": True,
|
||||
"STRIKE": True,
|
||||
"ANTISHIP": True,
|
||||
},
|
||||
|
||||
"STRIKE_MAX_RANGE": 1500000,
|
||||
"SEAD_MAX_RANGE": 1500000,
|
||||
@dataclass(frozen=True)
|
||||
class Doctrine:
|
||||
cas: bool
|
||||
cap: bool
|
||||
sead: bool
|
||||
strike: bool
|
||||
antiship: bool
|
||||
|
||||
"CAP_EVERY_X_MINUTES": 20,
|
||||
"CAS_EVERY_X_MINUTES": 30,
|
||||
"SEAD_EVERY_X_MINUTES": 40,
|
||||
"STRIKE_EVERY_X_MINUTES": 40,
|
||||
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
|
||||
|
||||
"INGRESS_EGRESS_DISTANCE": nm_to_meter(45),
|
||||
"INGRESS_ALT": feet_to_meter(20000),
|
||||
"EGRESS_ALT": feet_to_meter(20000),
|
||||
"PATROL_ALT_RANGE": (feet_to_meter(15000), feet_to_meter(33000)),
|
||||
"PATTERN_ALTITUDE": feet_to_meter(5000),
|
||||
min_patrol_altitude: Distance
|
||||
max_patrol_altitude: Distance
|
||||
pattern_altitude: Distance
|
||||
|
||||
"CAP_PATTERN_LENGTH": (nm_to_meter(15), nm_to_meter(40)),
|
||||
"FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(6), nm_to_meter(15)),
|
||||
"CAP_DISTANCE_FROM_CP": (nm_to_meter(10), nm_to_meter(40)),
|
||||
#: The duration that CAP flights will remain on-station.
|
||||
cap_duration: timedelta
|
||||
|
||||
"MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
|
||||
}
|
||||
#: The minimum length of the CAP race track.
|
||||
cap_min_track_length: Distance
|
||||
|
||||
COLDWAR_DOCTRINE = {
|
||||
#: The maximum length of the CAP race track.
|
||||
cap_max_track_length: Distance
|
||||
|
||||
"GENERATORS": {
|
||||
"CAS": True,
|
||||
"CAP": True,
|
||||
"SEAD": True,
|
||||
"STRIKE": True,
|
||||
"ANTISHIP": True,
|
||||
},
|
||||
#: The minimum distance between the defended position and the *end* of the
|
||||
#: CAP race track.
|
||||
cap_min_distance_from_cp: Distance
|
||||
|
||||
"STRIKE_MAX_RANGE": 1500000,
|
||||
"SEAD_MAX_RANGE": 1500000,
|
||||
#: The maximum distance between the defended position and the *end* of the
|
||||
#: CAP race track.
|
||||
cap_max_distance_from_cp: Distance
|
||||
|
||||
"CAP_EVERY_X_MINUTES": 20,
|
||||
"CAS_EVERY_X_MINUTES": 30,
|
||||
"SEAD_EVERY_X_MINUTES": 40,
|
||||
"STRIKE_EVERY_X_MINUTES": 40,
|
||||
#: 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
|
||||
|
||||
"INGRESS_EGRESS_DISTANCE": nm_to_meter(30),
|
||||
"INGRESS_ALT": feet_to_meter(18000),
|
||||
"EGRESS_ALT": feet_to_meter(18000),
|
||||
"PATROL_ALT_RANGE": (feet_to_meter(10000), feet_to_meter(24000)),
|
||||
"PATTERN_ALTITUDE": feet_to_meter(5000),
|
||||
cas_duration: timedelta
|
||||
|
||||
"CAP_PATTERN_LENGTH": (nm_to_meter(12), nm_to_meter(24)),
|
||||
"FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(2), nm_to_meter(8)),
|
||||
"CAP_DISTANCE_FROM_CP": (nm_to_meter(8), nm_to_meter(25)),
|
||||
sweep_distance: Distance
|
||||
|
||||
"MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
|
||||
}
|
||||
|
||||
WWII_DOCTRINE = {
|
||||
MODERN_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
cas=True,
|
||||
sead=True,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
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=nautical_miles(15),
|
||||
cap_max_track_length=nautical_miles(40),
|
||||
cap_min_distance_from_cp=nautical_miles(10),
|
||||
cap_max_distance_from_cp=nautical_miles(40),
|
||||
cap_engagement_range=nautical_miles(50),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(60),
|
||||
)
|
||||
|
||||
"GENERATORS": {
|
||||
"CAS": True,
|
||||
"CAP": True,
|
||||
"SEAD": False,
|
||||
"STRIKE": True,
|
||||
"ANTISHIP": True,
|
||||
},
|
||||
COLDWAR_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
cas=True,
|
||||
sead=True,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
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=nautical_miles(12),
|
||||
cap_max_track_length=nautical_miles(24),
|
||||
cap_min_distance_from_cp=nautical_miles(8),
|
||||
cap_max_distance_from_cp=nautical_miles(25),
|
||||
cap_engagement_range=nautical_miles(35),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(40),
|
||||
)
|
||||
|
||||
"STRIKE_MAX_RANGE": 1500000,
|
||||
"SEAD_MAX_RANGE": 1500000,
|
||||
|
||||
"CAP_EVERY_X_MINUTES": 20,
|
||||
"CAS_EVERY_X_MINUTES": 30,
|
||||
"SEAD_EVERY_X_MINUTES": 40,
|
||||
"STRIKE_EVERY_X_MINUTES": 40,
|
||||
|
||||
"INGRESS_EGRESS_DISTANCE": nm_to_meter(7),
|
||||
"INGRESS_ALT": feet_to_meter(8000),
|
||||
"EGRESS_ALT": feet_to_meter(8000),
|
||||
"PATROL_ALT_RANGE": (feet_to_meter(4000), feet_to_meter(15000)),
|
||||
"PATTERN_ALTITUDE": feet_to_meter(5000),
|
||||
|
||||
"CAP_PATTERN_LENGTH": (nm_to_meter(8), nm_to_meter(18)),
|
||||
"FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(1), nm_to_meter(6)),
|
||||
"CAP_DISTANCE_FROM_CP": (nm_to_meter(0), nm_to_meter(5)),
|
||||
|
||||
"MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
|
||||
|
||||
}
|
||||
WWII_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
cas=True,
|
||||
sead=False,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
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=nautical_miles(8),
|
||||
cap_max_track_length=nautical_miles(18),
|
||||
cap_min_distance_from_cp=nautical_miles(0),
|
||||
cap_max_distance_from_cp=nautical_miles(5),
|
||||
cap_engagement_range=nautical_miles(20),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(10),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
from dcs.ships import (
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
CG_1164_Moskva,
|
||||
CVN_70_Carl_Vinson,
|
||||
CVN_71_Theodore_Roosevelt,
|
||||
CVN_72_Abraham_Lincoln,
|
||||
CVN_73_George_Washington,
|
||||
CVN_74_John_C__Stennis,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
CV_1143_5_Admiral_Kuznetsov_2017,
|
||||
FFG_11540_Neustrashimy,
|
||||
FFL_1124_4_Grisha,
|
||||
FF_1135M_Rezky,
|
||||
FSG_1241_1MP_Molniya,
|
||||
LHA_1_Tarawa,
|
||||
Oliver_Hazzard_Perry_class,
|
||||
Ticonderoga_class,
|
||||
Type_052B_Destroyer,
|
||||
Type_052C_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
)
|
||||
from dcs.vehicles import AirDefence
|
||||
from dcs.ships import *
|
||||
|
||||
UNITS_WITH_RADAR = [
|
||||
|
||||
|
||||
1031
game/data/weapons.py
Normal file
1031
game/data/weapons.py
Normal file
File diff suppressed because it is too large
Load Diff
923
game/db.py
923
game/db.py
File diff suppressed because it is too large
Load Diff
277
game/debriefing.py
Normal file
277
game/debriefing.py
Normal file
@@ -0,0 +1,277 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Type,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.theater import Airfield, ControlPoint
|
||||
from game.unitmap import Building, FrontLineUnit, GroundObjectUnit, UnitMap
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
DEBRIEFING_LOG_EXTENSION = "log"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirLosses:
|
||||
player: List[Flight]
|
||||
enemy: List[Flight]
|
||||
|
||||
@property
|
||||
def losses(self) -> Iterator[Flight]:
|
||||
return itertools.chain(self.player, self.enemy)
|
||||
|
||||
def by_type(self, player: bool) -> Dict[Type[FlyingType], int]:
|
||||
losses_by_type: Dict[Type[FlyingType], int] = defaultdict(int)
|
||||
losses = self.player if player else self.enemy
|
||||
for loss in losses:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def surviving_flight_members(self, flight: Flight) -> int:
|
||||
losses = 0
|
||||
for loss in self.losses:
|
||||
if loss == flight:
|
||||
losses += 1
|
||||
return flight.count - losses
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroundLosses:
|
||||
player_front_line: List[FrontLineUnit] = field(default_factory=list)
|
||||
enemy_front_line: List[FrontLineUnit] = field(default_factory=list)
|
||||
|
||||
player_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
|
||||
enemy_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
|
||||
|
||||
player_buildings: List[Building] = field(default_factory=list)
|
||||
enemy_buildings: List[Building] = field(default_factory=list)
|
||||
|
||||
player_airfields: List[Airfield] = field(default_factory=list)
|
||||
enemy_airfields: List[Airfield] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StateData:
|
||||
#: True if the mission ended. If False, the mission exited abnormally.
|
||||
mission_ended: bool
|
||||
|
||||
#: Names of aircraft units that were killed during the mission.
|
||||
killed_aircraft: List[str]
|
||||
|
||||
#: Names of vehicle (and ship) units that were killed during the mission.
|
||||
killed_ground_units: List[str]
|
||||
|
||||
#: Names of static units that were destroyed during the mission.
|
||||
destroyed_statics: List[str]
|
||||
|
||||
#: Mangled names of bases that were captured during the mission.
|
||||
base_capture_events: List[str]
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data: Dict[str, Any]) -> StateData:
|
||||
return cls(
|
||||
mission_ended=data["mission_ended"],
|
||||
killed_aircraft=data["killed_aircrafts"],
|
||||
# Airfields emit a new "dead" event every time a bomb is dropped on
|
||||
# them when they've already dead. Dedup.
|
||||
killed_ground_units=list(set(data["killed_ground_units"])),
|
||||
destroyed_statics=data["destroyed_objects_positions"],
|
||||
base_capture_events=data["base_capture_events"]
|
||||
)
|
||||
|
||||
|
||||
class Debriefing:
|
||||
def __init__(self, state_data: Dict[str, Any], game: Game,
|
||||
unit_map: UnitMap) -> None:
|
||||
self.state_data = StateData.from_json(state_data)
|
||||
self.unit_map = unit_map
|
||||
|
||||
self.player_country = game.player_country
|
||||
self.enemy_country = game.enemy_country
|
||||
self.player_country_id = db.country_id_from_name(game.player_country)
|
||||
self.enemy_country_id = db.country_id_from_name(game.enemy_country)
|
||||
|
||||
self.air_losses = self.dead_aircraft()
|
||||
self.ground_losses = self.dead_ground_units()
|
||||
|
||||
@property
|
||||
def front_line_losses(self) -> Iterator[FrontLineUnit]:
|
||||
yield from self.ground_losses.player_front_line
|
||||
yield from self.ground_losses.enemy_front_line
|
||||
|
||||
@property
|
||||
def ground_object_losses(self) -> Iterator[GroundObjectUnit]:
|
||||
yield from self.ground_losses.player_ground_objects
|
||||
yield from self.ground_losses.enemy_ground_objects
|
||||
|
||||
@property
|
||||
def building_losses(self) -> Iterator[Building]:
|
||||
yield from self.ground_losses.player_buildings
|
||||
yield from self.ground_losses.enemy_buildings
|
||||
|
||||
@property
|
||||
def damaged_runways(self) -> Iterator[Airfield]:
|
||||
yield from self.ground_losses.player_airfields
|
||||
yield from self.ground_losses.enemy_airfields
|
||||
|
||||
def casualty_count(self, control_point: ControlPoint) -> int:
|
||||
return len(
|
||||
[x for x in self.front_line_losses if x.origin == control_point]
|
||||
)
|
||||
|
||||
def front_line_losses_by_type(
|
||||
self, player: bool) -> Dict[Type[UnitType], int]:
|
||||
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
|
||||
if player:
|
||||
losses = self.ground_losses.player_front_line
|
||||
else:
|
||||
losses = self.ground_losses.enemy_front_line
|
||||
for loss in losses:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def building_losses_by_type(self, player: bool) -> Dict[str, int]:
|
||||
losses_by_type: Dict[str, int] = defaultdict(int)
|
||||
if player:
|
||||
losses = self.ground_losses.player_buildings
|
||||
else:
|
||||
losses = self.ground_losses.enemy_buildings
|
||||
for loss in losses:
|
||||
if loss.ground_object.control_point.captured != player:
|
||||
continue
|
||||
|
||||
losses_by_type[loss.ground_object.dcs_identifier] += 1
|
||||
return losses_by_type
|
||||
|
||||
def dead_aircraft(self) -> AirLosses:
|
||||
player_losses = []
|
||||
enemy_losses = []
|
||||
for unit_name in self.state_data.killed_aircraft:
|
||||
flight = self.unit_map.flight(unit_name)
|
||||
if flight is None:
|
||||
logging.error(f"Could not find Flight matching {unit_name}")
|
||||
continue
|
||||
if flight.departure.captured:
|
||||
player_losses.append(flight)
|
||||
else:
|
||||
enemy_losses.append(flight)
|
||||
return AirLosses(player_losses, enemy_losses)
|
||||
|
||||
def dead_ground_units(self) -> GroundLosses:
|
||||
losses = GroundLosses()
|
||||
for unit_name in self.state_data.killed_ground_units:
|
||||
front_line_unit = self.unit_map.front_line_unit(unit_name)
|
||||
if front_line_unit is not None:
|
||||
if front_line_unit.origin.captured:
|
||||
losses.player_front_line.append(front_line_unit)
|
||||
else:
|
||||
losses.enemy_front_line.append(front_line_unit)
|
||||
continue
|
||||
|
||||
ground_object_unit = self.unit_map.ground_object_unit(unit_name)
|
||||
if ground_object_unit is not None:
|
||||
if ground_object_unit.ground_object.control_point.captured:
|
||||
losses.player_ground_objects.append(ground_object_unit)
|
||||
else:
|
||||
losses.enemy_ground_objects.append(ground_object_unit)
|
||||
continue
|
||||
|
||||
building = self.unit_map.building_or_fortification(unit_name)
|
||||
# Try appending object to the name, because we do this for building statics.
|
||||
if building is None:
|
||||
building = self.unit_map.building_or_fortification(
|
||||
f"{unit_name} object"
|
||||
)
|
||||
if building is not None:
|
||||
if building.ground_object.control_point.captured:
|
||||
losses.player_buildings.append(building)
|
||||
else:
|
||||
losses.enemy_buildings.append(building)
|
||||
continue
|
||||
|
||||
airfield = self.unit_map.airfield(unit_name)
|
||||
if airfield is not None:
|
||||
if airfield.captured:
|
||||
losses.player_airfields.append(airfield)
|
||||
else:
|
||||
losses.enemy_airfields.append(airfield)
|
||||
continue
|
||||
|
||||
# Only logging as debug because we don't currently track infantry
|
||||
# deaths, so we expect to see quite a few unclaimed dead ground
|
||||
# units. We should start tracking those and covert this to a
|
||||
# warning.
|
||||
logging.debug(f"Death of untracked ground unit {unit_name} will "
|
||||
"have no effect. This may be normal behavior.")
|
||||
|
||||
return losses
|
||||
|
||||
@property
|
||||
def base_capture_events(self):
|
||||
"""Keeps only the last instance of a base capture event for each base ID."""
|
||||
reversed_captures = list(reversed(self.state_data.base_capture_events))
|
||||
last_base_cap_indexes = []
|
||||
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
|
||||
if base not in [x[1] for x in last_base_cap_indexes]:
|
||||
last_base_cap_indexes.append((idx, base))
|
||||
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
|
||||
|
||||
|
||||
class PollDebriefingFileThread(threading.Thread):
|
||||
"""Thread class with a stop() method. The thread itself has to check
|
||||
regularly for the stopped() condition."""
|
||||
|
||||
def __init__(self, callback: Callable[[Debriefing], None],
|
||||
game: Game, unit_map: UnitMap) -> None:
|
||||
super().__init__()
|
||||
self._stop_event = threading.Event()
|
||||
self.callback = callback
|
||||
self.game = game
|
||||
self.unit_map = unit_map
|
||||
|
||||
def stop(self):
|
||||
self._stop_event.set()
|
||||
|
||||
def stopped(self):
|
||||
return self._stop_event.is_set()
|
||||
|
||||
def run(self):
|
||||
if os.path.isfile("state.json"):
|
||||
last_modified = os.path.getmtime("state.json")
|
||||
else:
|
||||
last_modified = 0
|
||||
while not self.stopped():
|
||||
if os.path.isfile("state.json") and os.path.getmtime("state.json") > last_modified:
|
||||
with open("state.json", "r") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
debriefing = Debriefing(json_data, self.game, self.unit_map)
|
||||
self.callback(debriefing)
|
||||
break
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def wait_for_debriefing(callback: Callable[[Debriefing], None],
|
||||
game: Game, unit_map) -> PollDebriefingFileThread:
|
||||
thread = PollDebriefingFileThread(callback, game, unit_map)
|
||||
thread.start()
|
||||
return thread
|
||||
14
game/event/airwar.py
Normal file
14
game/event/airwar.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .event import Event
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater
|
||||
|
||||
|
||||
class AirWarEvent(Event):
|
||||
"""Event handler for the air battle"""
|
||||
|
||||
def __str__(self):
|
||||
return "AirWar"
|
||||
@@ -1,53 +1,44 @@
|
||||
import typing
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, Iterator, List, TYPE_CHECKING, Tuple, Type
|
||||
|
||||
from dcs.action import Coalition
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.task import *
|
||||
from dcs.vehicles import AirDefence
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import Task
|
||||
from dcs.unittype import UnitType, VehicleType
|
||||
|
||||
from game import *
|
||||
from game import persistency
|
||||
from game.debriefing import AirLosses, Debriefing
|
||||
from game.infos.information import Information
|
||||
from theater import *
|
||||
from gen.environmentgen import EnvironmentSettings
|
||||
from gen.conflictgen import Conflict
|
||||
from game.db import assigned_units_from, unitdict_from
|
||||
from theater.start_generator import generate_airbase_defense_group
|
||||
from game.operation.operation import Operation
|
||||
from game.theater import ControlPoint
|
||||
from gen import AirTaskingOrder
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from ..db import PRICES
|
||||
from ..unitmap import UnitMap
|
||||
|
||||
from userdata.debriefing import Debriefing
|
||||
from userdata import persistency
|
||||
|
||||
import game.db as db
|
||||
|
||||
DIFFICULTY_LOG_BASE = 1.1
|
||||
EVENT_DEPARTURE_MAX_DISTANCE = 340000
|
||||
if TYPE_CHECKING:
|
||||
from ..game import Game
|
||||
|
||||
|
||||
MINOR_DEFEAT_INFLUENCE = 0.1
|
||||
DEFEAT_INFLUENCE = 0.3
|
||||
STRONG_DEFEAT_INFLUENCE = 0.5
|
||||
|
||||
|
||||
class Event:
|
||||
silent = False
|
||||
informational = False
|
||||
is_awacs_enabled = False
|
||||
ca_slots = 0
|
||||
|
||||
game = None # type: Game
|
||||
location = None # type: Point
|
||||
from_cp = None # type: ControlPoint
|
||||
departure_cp = None # type: ControlPoint
|
||||
to_cp = None # type: ControlPoint
|
||||
|
||||
operation = None # type: Operation
|
||||
difficulty = 1 # type: int
|
||||
environment_settings = None # type: EnvironmentSettings
|
||||
BONUS_BASE = 5
|
||||
|
||||
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
|
||||
self.game = game
|
||||
self.departure_cp = None
|
||||
self.from_cp = from_cp
|
||||
self.to_cp = target_cp
|
||||
self.location = location
|
||||
@@ -59,138 +50,124 @@ class Event:
|
||||
return self.attacker_name == self.game.player_name
|
||||
|
||||
@property
|
||||
def enemy_cp(self) -> ControlPoint:
|
||||
if self.attacker_name == self.game.player_name:
|
||||
return self.to_cp
|
||||
else:
|
||||
return self.departure_cp
|
||||
|
||||
@property
|
||||
def tasks(self) -> typing.Collection[typing.Type[Task]]:
|
||||
def tasks(self) -> List[Type[Task]]:
|
||||
return []
|
||||
|
||||
@property
|
||||
def global_cp_available(self) -> bool:
|
||||
return False
|
||||
def generate(self) -> UnitMap:
|
||||
Operation.prepare(self.game)
|
||||
unit_map = Operation.generate()
|
||||
Operation.current_mission.save(
|
||||
persistency.mission_path_for("liberation_nextturn.miz"))
|
||||
return unit_map
|
||||
|
||||
def is_departure_available_from(self, cp: ControlPoint) -> bool:
|
||||
if not cp.captured:
|
||||
return False
|
||||
|
||||
if self.location.distance_to_point(cp.position) > EVENT_DEPARTURE_MAX_DISTANCE:
|
||||
return False
|
||||
|
||||
if cp.is_global and not self.global_cp_available:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def bonus(self) -> int:
|
||||
return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE)
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing) -> bool:
|
||||
return self.operation.is_successfull(debriefing)
|
||||
|
||||
def player_attacking(self, cp: ControlPoint, flights: db.TaskForceDict):
|
||||
if self.is_player_attacking:
|
||||
self.departure_cp = cp
|
||||
else:
|
||||
self.to_cp = cp
|
||||
|
||||
def player_defending(self, cp: ControlPoint, flights: db.TaskForceDict):
|
||||
if self.is_player_attacking:
|
||||
self.departure_cp = cp
|
||||
else:
|
||||
self.to_cp = cp
|
||||
|
||||
def generate(self):
|
||||
self.operation.is_awacs_enabled = self.is_awacs_enabled
|
||||
self.operation.ca_slots = self.ca_slots
|
||||
|
||||
self.operation.prepare(self.game.theater.terrain, is_quick=False)
|
||||
self.operation.generate()
|
||||
self.operation.current_mission.save(persistency.mission_path_for("liberation_nextturn.miz"))
|
||||
self.environment_settings = self.operation.environment_settings
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
|
||||
logging.info("Commiting mission results")
|
||||
|
||||
# ------------------------------
|
||||
# Destroyed aircrafts
|
||||
cp_map = {cp.id: cp for cp in self.game.theater.controlpoints}
|
||||
for destroyed_aircraft in debriefing.killed_aircrafts:
|
||||
try:
|
||||
cpid = int(destroyed_aircraft.split("|")[3])
|
||||
type = db.unit_type_from_name(destroyed_aircraft.split("|")[4])
|
||||
if cpid in cp_map.keys():
|
||||
cp = cp_map[cpid]
|
||||
if type in cp.base.aircraft.keys():
|
||||
logging.info("Aircraft destroyed : " + str(type))
|
||||
cp.base.aircraft[type] = max(0, cp.base.aircraft[type]-1)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# ------------------------------
|
||||
# Destroyed ground units
|
||||
killed_unit_count_by_cp = {cp.id: 0 for cp in self.game.theater.controlpoints}
|
||||
cp_map = {cp.id: cp for cp in self.game.theater.controlpoints}
|
||||
for killed_ground_unit in debriefing.killed_ground_units:
|
||||
try:
|
||||
cpid = int(killed_ground_unit.split("|")[3])
|
||||
type = db.unit_type_from_name(killed_ground_unit.split("|")[4])
|
||||
if cpid in cp_map.keys():
|
||||
killed_unit_count_by_cp[cpid] = killed_unit_count_by_cp[cpid] + 1
|
||||
cp = cp_map[cpid]
|
||||
if type in cp.base.armor.keys():
|
||||
logging.info("Ground unit destroyed : " + str(type))
|
||||
cp.base.armor[type] = max(0, cp.base.armor[type] - 1)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# ------------------------------
|
||||
# Static ground objects
|
||||
for destroyed_ground_unit_name in debriefing.killed_ground_units:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if not cp.ground_objects:
|
||||
@staticmethod
|
||||
def _transfer_aircraft(ato: AirTaskingOrder, losses: AirLosses,
|
||||
for_player: bool) -> None:
|
||||
for package in ato.packages:
|
||||
for flight in package.flights:
|
||||
# No need to transfer to the same location.
|
||||
if flight.departure == flight.arrival:
|
||||
continue
|
||||
|
||||
# -- Static ground objects
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
# Don't transfer to bases that were captured. Note that if the
|
||||
# airfield was back-filling transfers it may overflow. We could
|
||||
# attempt to be smarter in the future by performing transfers in
|
||||
# order up a graph to prevent transfers to full airports and
|
||||
# send overflow off-map, but overflow is fine for now.
|
||||
if flight.arrival.captured != for_player:
|
||||
logging.info(
|
||||
f"Not transferring {flight} because {flight.arrival} "
|
||||
"was captured")
|
||||
continue
|
||||
|
||||
if ground_object.matches_string_identifier(destroyed_ground_unit_name):
|
||||
logging.info("cp {} killing ground object {}".format(cp, ground_object.string_identifier))
|
||||
cp.ground_objects[i].is_dead = True
|
||||
transfer_count = losses.surviving_flight_members(flight)
|
||||
if transfer_count < 0:
|
||||
logging.error(f"{flight} had {flight.count} aircraft but "
|
||||
f"{transfer_count} losses were recorded.")
|
||||
continue
|
||||
|
||||
info = Information("Building destroyed",
|
||||
ground_object.dcs_identifier + " has been destroyed at location " + ground_object.obj_name,
|
||||
self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
aircraft = flight.unit_type
|
||||
available = flight.departure.base.total_units_of_type(aircraft)
|
||||
if available < transfer_count:
|
||||
logging.error(
|
||||
f"Found killed {aircraft} from {flight.departure} but "
|
||||
f"that airbase has only {available} available.")
|
||||
continue
|
||||
|
||||
flight.departure.base.aircraft[aircraft] -= transfer_count
|
||||
if aircraft not in flight.arrival.base.aircraft:
|
||||
# TODO: Should use defaultdict.
|
||||
flight.arrival.base.aircraft[aircraft] = 0
|
||||
flight.arrival.base.aircraft[aircraft] += transfer_count
|
||||
|
||||
# -- AA Site groups
|
||||
destroyed_units = 0
|
||||
info = Information("Units destroyed at " + ground_object.obj_name,
|
||||
"",
|
||||
self.game.turn)
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
if ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]:
|
||||
for g in ground_object.groups:
|
||||
if not hasattr(g, "units_losts"):
|
||||
g.units_losts = []
|
||||
for u in g.units:
|
||||
if u.name == destroyed_ground_unit_name:
|
||||
g.units.remove(u)
|
||||
g.units_losts.append(u)
|
||||
destroyed_units = destroyed_units + 1
|
||||
info.text = u.type
|
||||
ucount = sum([len(g.units) for g in ground_object.groups])
|
||||
if ucount == 0:
|
||||
ground_object.is_dead = True
|
||||
if destroyed_units > 0:
|
||||
self.game.informations.append(info)
|
||||
def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
|
||||
self._transfer_aircraft(self.game.blue_ato, debriefing.air_losses,
|
||||
for_player=True)
|
||||
self._transfer_aircraft(self.game.red_ato, debriefing.air_losses,
|
||||
for_player=False)
|
||||
|
||||
@staticmethod
|
||||
def commit_air_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.air_losses.losses:
|
||||
aircraft = loss.unit_type
|
||||
cp = loss.departure
|
||||
available = cp.base.total_units_of_type(aircraft)
|
||||
if available <= 0:
|
||||
logging.error(
|
||||
f"Found killed {aircraft} from {cp} but that airbase has "
|
||||
"none available.")
|
||||
continue
|
||||
|
||||
logging.info(f"{aircraft} destroyed from {cp}")
|
||||
cp.base.aircraft[aircraft] -= 1
|
||||
|
||||
@staticmethod
|
||||
def commit_front_line_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.front_line_losses:
|
||||
unit_type = loss.unit_type
|
||||
control_point = loss.origin
|
||||
available = control_point.base.total_units_of_type(unit_type)
|
||||
if available <= 0:
|
||||
logging.error(
|
||||
f"Found killed {unit_type} from {control_point} but that "
|
||||
"airbase has none available.")
|
||||
continue
|
||||
|
||||
logging.info(f"{unit_type} destroyed from {control_point}")
|
||||
control_point.base.armor[unit_type] -= 1
|
||||
|
||||
@staticmethod
|
||||
def commit_ground_object_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.ground_object_losses:
|
||||
# TODO: This should be stored in the TGO, not in the pydcs Group.
|
||||
if not hasattr(loss.group, "units_losts"):
|
||||
loss.group.units_losts = []
|
||||
|
||||
loss.group.units.remove(loss.unit)
|
||||
loss.group.units_losts.append(loss.unit)
|
||||
|
||||
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.building_losses:
|
||||
loss.ground_object.kill()
|
||||
self.game.informations.append(Information(
|
||||
"Building destroyed",
|
||||
f"{loss.ground_object.dcs_identifier} has been destroyed at "
|
||||
f"location {loss.ground_object.obj_name}", self.game.turn
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
def commit_damaged_runways(debriefing: Debriefing) -> None:
|
||||
for damaged_runway in debriefing.damaged_runways:
|
||||
damaged_runway.damage_runway()
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
logging.info("Committing mission results")
|
||||
|
||||
self.commit_air_losses(debriefing)
|
||||
self.commit_front_line_losses(debriefing)
|
||||
self.commit_ground_object_losses(debriefing)
|
||||
self.commit_building_losses(debriefing)
|
||||
self.commit_damaged_runways(debriefing)
|
||||
|
||||
# ------------------------------
|
||||
# Captured bases
|
||||
@@ -209,41 +186,31 @@ class Event:
|
||||
if cp.id == id:
|
||||
|
||||
if cp.captured and new_owner_coalition != coalition:
|
||||
cp.captured = False
|
||||
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)
|
||||
pname = self.game.enemy_name
|
||||
captured_cps.append(cp)
|
||||
elif not(cp.captured) and new_owner_coalition == coalition:
|
||||
cp.captured = True
|
||||
for_player = True
|
||||
info = Information(cp.name + " captured !", "We took control of " + cp.name + "! Great job !", self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
pname = self.game.player_name
|
||||
captured_cps.append(cp)
|
||||
else:
|
||||
continue
|
||||
|
||||
cp.base.aircraft = {}
|
||||
cp.base.armor = {}
|
||||
|
||||
airbase_def_id = 0
|
||||
for g in cp.ground_objects:
|
||||
g.groups = []
|
||||
if g.airbase_group and pname != "":
|
||||
generate_airbase_defense_group(airbase_def_id, g, pname, self.game, cp)
|
||||
airbase_def_id = airbase_def_id + 1
|
||||
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}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.complete_aircraft_transfers(debriefing)
|
||||
|
||||
# Destroyed units carcass
|
||||
# -------------------------
|
||||
for destroyed_unit in debriefing.destroyed_units:
|
||||
for destroyed_unit in debriefing.state_data.destroyed_statics:
|
||||
self.game.add_destroyed_units(destroyed_unit)
|
||||
|
||||
# -----------------------------------
|
||||
@@ -253,10 +220,10 @@ class Event:
|
||||
for enemy_cp in enemy_cps:
|
||||
print("Compute frontline progression for : " + cp.name + " to " + enemy_cp.name)
|
||||
|
||||
delta = 0
|
||||
delta = 0.0
|
||||
player_won = True
|
||||
ally_casualties = killed_unit_count_by_cp[cp.id]
|
||||
enemy_casualties = killed_unit_count_by_cp[enemy_cp.id]
|
||||
ally_casualties = debriefing.casualty_count(cp)
|
||||
enemy_casualties = debriefing.casualty_count(enemy_cp)
|
||||
ally_units_alive = cp.base.total_armor
|
||||
enemy_units_alive = enemy_cp.base.total_armor
|
||||
|
||||
@@ -329,76 +296,128 @@ class Event:
|
||||
self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
|
||||
def skip(self):
|
||||
pass
|
||||
|
||||
def redeploy_units(self, cp):
|
||||
def redeploy_units(self, cp: ControlPoint) -> None:
|
||||
""""
|
||||
Auto redeploy units to newly captured base
|
||||
"""
|
||||
|
||||
ally_connected_cps = [ocp for ocp in cp.connected_points if cp.captured == ocp.captured]
|
||||
enemy_connected_cps = [ocp for ocp in cp.connected_points if cp.captured != ocp.captured]
|
||||
ally_connected_cps = [ocp for ocp in cp.connected_points if
|
||||
cp.captured == ocp.captured]
|
||||
enemy_connected_cps = [ocp for ocp in cp.connected_points if
|
||||
cp.captured != ocp.captured]
|
||||
|
||||
# If the newly captured cp does not have enemy connected cp,
|
||||
# then it is not necessary to redeploy frontline units there.
|
||||
if len(enemy_connected_cps) == 0:
|
||||
return
|
||||
|
||||
# From each ally cp, send reinforcements
|
||||
for ally_cp in ally_connected_cps:
|
||||
self.redeploy_between(cp, ally_cp)
|
||||
|
||||
def redeploy_between(self, destination: ControlPoint,
|
||||
source: ControlPoint) -> None:
|
||||
total_units_redeployed = 0
|
||||
moved_units = {}
|
||||
|
||||
if source.has_active_frontline or not destination.captured:
|
||||
# If there are still active front lines to defend at the
|
||||
# transferring CP we should not transfer all units.
|
||||
#
|
||||
# Opfor also does not transfer all of their units.
|
||||
# TODO: Balance the CPs rather than moving half from everywhere.
|
||||
move_factor = 0.5
|
||||
else:
|
||||
# From each ally cp, send reinforcements
|
||||
for ally_cp in ally_connected_cps:
|
||||
total_units_redeployed = 0
|
||||
own_enemy_cp = [ocp for ocp in ally_cp.connected_points if ally_cp.captured != ocp.captured]
|
||||
# Otherwise we can move everything.
|
||||
move_factor = 1
|
||||
|
||||
moved_units = {}
|
||||
for frontline_unit, count in source.base.armor.items():
|
||||
moved_units[frontline_unit] = int(count * move_factor)
|
||||
total_units_redeployed = total_units_redeployed + int(
|
||||
count * move_factor)
|
||||
|
||||
# If the connected base, does not have any more enemy cp connected.
|
||||
# Or if it is not the opponent redeploying forces there (enemy AI will never redeploy all their forces at once)
|
||||
if len(own_enemy_cp) > 0 or not cp.captured:
|
||||
for frontline_unit, count in ally_cp.base.armor.items():
|
||||
moved_units[frontline_unit] = int(count/2)
|
||||
total_units_redeployed = total_units_redeployed + int(count/2)
|
||||
else: # So if the old base, does not have any more enemy cp connected, or if it is an enemy base
|
||||
for frontline_unit, count in ally_cp.base.armor.items():
|
||||
moved_units[frontline_unit] = count
|
||||
total_units_redeployed = total_units_redeployed + count
|
||||
destination.base.commision_units(moved_units)
|
||||
source.base.commit_losses(moved_units)
|
||||
|
||||
cp.base.commision_units(moved_units)
|
||||
ally_cp.base.commit_losses(moved_units)
|
||||
# Also transfer pending deliveries.
|
||||
for unit_type, count in source.pending_unit_deliveries.units.items():
|
||||
if not issubclass(unit_type, VehicleType):
|
||||
continue
|
||||
if count <= 0:
|
||||
# Don't transfer *sales*...
|
||||
continue
|
||||
move_count = int(count * move_factor)
|
||||
source.pending_unit_deliveries.sell({unit_type: move_count})
|
||||
destination.pending_unit_deliveries.order({unit_type: move_count})
|
||||
total_units_redeployed += move_count
|
||||
|
||||
if total_units_redeployed > 0:
|
||||
info = Information("Units redeployed", "", self.game.turn)
|
||||
info.text = str(total_units_redeployed) + " units have been redeployed from " + ally_cp.name + " to " + cp.name
|
||||
self.game.informations.append(info)
|
||||
logging.info(info.text)
|
||||
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)
|
||||
|
||||
|
||||
class UnitsDeliveryEvent:
|
||||
|
||||
class UnitsDeliveryEvent(Event):
|
||||
informational = True
|
||||
units = None # type: typing.Dict[UnitType, int]
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
self.to_cp = control_point
|
||||
self.units: Dict[Type[UnitType], int] = {}
|
||||
|
||||
def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game):
|
||||
super(UnitsDeliveryEvent, self).__init__(game=game,
|
||||
location=to_cp.position,
|
||||
from_cp=from_cp,
|
||||
target_cp=to_cp,
|
||||
attacker_name=attacker_name,
|
||||
defender_name=defender_name)
|
||||
|
||||
self.units = {}
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return "Pending delivery to {}".format(self.to_cp)
|
||||
|
||||
def deliver(self, units: typing.Dict[UnitType, int]):
|
||||
def order(self, units: Dict[Type[UnitType], int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] = self.units.get(k, 0) + v
|
||||
|
||||
def skip(self):
|
||||
def sell(self, units: Dict[Type[UnitType], int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] = self.units.get(k, 0) - v
|
||||
|
||||
for k, v in self.units.items():
|
||||
info = Information("Ally Reinforcement", str(k.id) + " x " + str(v) + " at " + self.to_cp.name, self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
def consume_each_order(self) -> Iterator[Tuple[Type[UnitType], int]]:
|
||||
while self.units:
|
||||
yield self.units.popitem()
|
||||
|
||||
self.to_cp.base.commision_units(self.units)
|
||||
def refund_all(self, game: Game) -> None:
|
||||
for unit_type, count in self.consume_each_order():
|
||||
try:
|
||||
price = PRICES[unit_type]
|
||||
except KeyError:
|
||||
logging.error(f"Could not refund {unit_type.id}, price unknown")
|
||||
continue
|
||||
|
||||
logging.info(
|
||||
f"Refunding {count} {unit_type.id} at {self.to_cp.name}")
|
||||
game.adjust_budget(price * count, player=self.to_cp.captured)
|
||||
|
||||
def available_next_turn(self, unit_type: Type[UnitType]) -> int:
|
||||
pending_units = self.units.get(unit_type)
|
||||
if pending_units is None:
|
||||
pending_units = 0
|
||||
current_units = self.to_cp.base.total_units_of_type(unit_type)
|
||||
return pending_units + current_units
|
||||
|
||||
def process(self, game: Game) -> None:
|
||||
bought_units: Dict[Type[UnitType], int] = {}
|
||||
sold_units: Dict[Type[UnitType], int] = {}
|
||||
for unit_type, count in self.units.items():
|
||||
coalition = "Ally" if self.to_cp.captured else "Enemy"
|
||||
aircraft = unit_type.id
|
||||
name = self.to_cp.name
|
||||
if count >= 0:
|
||||
bought_units[unit_type] = count
|
||||
game.message(
|
||||
f"{coalition} reinforcements: {aircraft} x {count} at {name}")
|
||||
else:
|
||||
sold_units[unit_type] = -count
|
||||
game.message(
|
||||
f"{coalition} sold: {aircraft} x {-count} at {name}")
|
||||
self.to_cp.base.commision_units(bought_units)
|
||||
self.to_cp.base.commit_losses(sold_units)
|
||||
self.units = {}
|
||||
bought_units = {}
|
||||
sold_units = {}
|
||||
|
||||
@@ -1,53 +1,11 @@
|
||||
from game.event import *
|
||||
from game.operation.frontlineattack import FrontlineAttackOperation
|
||||
from userdata.debriefing import Debriefing
|
||||
from .event import Event
|
||||
|
||||
|
||||
class FrontlineAttackEvent(Event):
|
||||
|
||||
@property
|
||||
def tasks(self) -> typing.Collection[typing.Type[Task]]:
|
||||
if self.is_player_attacking:
|
||||
return [CAS, CAP]
|
||||
else:
|
||||
return [CAP]
|
||||
|
||||
@property
|
||||
def global_cp_available(self) -> bool:
|
||||
return True
|
||||
|
||||
"""
|
||||
An event centered on a FrontLine Conflict.
|
||||
Currently the same as its parent, but here for legacy compatibility as well as to allow for
|
||||
future unique Event handling
|
||||
"""
|
||||
def __str__(self):
|
||||
return "Frontline attack"
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing):
|
||||
attackers_success = True
|
||||
if self.from_cp.captured:
|
||||
return attackers_success
|
||||
else:
|
||||
return not attackers_success
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
super(FrontlineAttackEvent, self).commit(debriefing)
|
||||
|
||||
def skip(self):
|
||||
if self.to_cp.captured:
|
||||
self.to_cp.base.affect_strength(-0.1)
|
||||
|
||||
def player_attacking(self, flights: db.TaskForceDict):
|
||||
op = FrontlineAttackOperation(game=self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
from_cp=self.from_cp,
|
||||
departure_cp=self.departure_cp,
|
||||
to_cp=self.to_cp)
|
||||
self.operation = op
|
||||
|
||||
def player_defending(self, flights: db.TaskForceDict):
|
||||
op = FrontlineAttackOperation(game=self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
from_cp=self.from_cp,
|
||||
departure_cp=self.departure_cp,
|
||||
to_cp=self.to_cp)
|
||||
self.operation = op
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Australia_2005 = {
|
||||
"country": "Australia",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
FA_18C_hornet,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.APC_M113,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.IFV_MCV_80,
|
||||
|
||||
UH_1H,
|
||||
AH_1W, # Standing as EC Tiger
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.Rapier_FSA_Launcher,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.Rapier_FSA_Launcher,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "destroyer": [
|
||||
USS_Arleigh_Burke_IIa,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "lhanames": [
|
||||
"HMAS Canberra",
|
||||
"HMAS Adelaide"
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
BLUEFOR_COLDWAR = {
|
||||
"country": "Combined Joint Task Forces Blue",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
|
||||
F_14B,
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
A_10A,
|
||||
AJS37,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
SA342M,
|
||||
SA342L,
|
||||
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.APC_M113,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "carrier_names": [
|
||||
"CVN-71 Theodore Roosevelt",
|
||||
"CVN-72 Abraham Lincoln",
|
||||
"CVN-73 George Washington",
|
||||
"CVN-74 John C. Stennis",
|
||||
], "lhanames": [
|
||||
"LHA-1 Tarawa",
|
||||
"LHA-2 Saipan",
|
||||
"LHA-3 Belleau Wood",
|
||||
"LHA-4 Nassau",
|
||||
"LHA-5 Peleliu"
|
||||
], "boat": [
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
|
||||
BLUEFOR_COLDWAR_A4 = {
|
||||
"country": "Combined Joint Task Forces Blue",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
|
||||
F_14B,
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
A_10A,
|
||||
AJS37,
|
||||
A_4E_C,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
SA342M,
|
||||
SA342L,
|
||||
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.APC_M113,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "carrier_names": [
|
||||
"CVN-71 Theodore Roosevelt",
|
||||
"CVN-72 Abraham Lincoln",
|
||||
"CVN-73 George Washington",
|
||||
"CVN-74 John C. Stennis",
|
||||
], "lhanames": [
|
||||
"LHA-1 Tarawa",
|
||||
"LHA-2 Saipan",
|
||||
"LHA-3 Belleau Wood",
|
||||
"LHA-4 Nassau",
|
||||
"LHA-5 Peleliu"
|
||||
], "boat": [
|
||||
], "requirements": {
|
||||
"Community A-4E": "https://heclak.github.io/community-a4e-c/",
|
||||
}, "has_jtac": True
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
|
||||
BLUEFOR_COLDWAR_MODS = {
|
||||
"country": "USA",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
|
||||
F_14B,
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
A_10A,
|
||||
AJS37,
|
||||
A_4E_C,
|
||||
MB_339PAN,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
SA342M,
|
||||
SA342L,
|
||||
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.APC_M113,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "carrier_names": [
|
||||
"CVN-71 Theodore Roosevelt",
|
||||
"CVN-72 Abraham Lincoln",
|
||||
"CVN-73 George Washington",
|
||||
"CVN-74 John C. Stennis",
|
||||
], "lhanames": [
|
||||
"LHA-1 Tarawa",
|
||||
"LHA-2 Saipan",
|
||||
"LHA-3 Belleau Wood",
|
||||
"LHA-4 Nassau",
|
||||
"LHA-5 Peleliu"
|
||||
], "boat": [
|
||||
], "requirements": {
|
||||
"MB-339A": "http://www.freccetricolorivirtuali.net/",
|
||||
"Community A-4E": "https://heclak.github.io/community-a4e-c/",
|
||||
}, "has_jtac": True
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
BLUEFOR_MODERN = {
|
||||
"country": "Combined Joint Task Forces Blue",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
|
||||
F_15C,
|
||||
F_14B,
|
||||
FA_18C_hornet,
|
||||
F_16C_50,
|
||||
JF_17,
|
||||
M_2000C,
|
||||
F_5E_3,
|
||||
Su_27,
|
||||
|
||||
Su_25T,
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
AJS37,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
AH_64D,
|
||||
Ka_50,
|
||||
SA342M,
|
||||
SA342L,
|
||||
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_Leopard_2,
|
||||
Armor.ATGM_M1134_Stryker,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.IFV_Marder,
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
|
||||
Artillery.MLRS_M270,
|
||||
Artillery.SPH_M109_Paladin,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Patriot_EPP_III,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "destroyer": [
|
||||
Oliver_Hazzard_Perry_class,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "carrier_names": [
|
||||
"CVN-71 Theodore Roosevelt",
|
||||
"CVN-72 Abraham Lincoln",
|
||||
"CVN-73 George Washington",
|
||||
"CVN-74 John C. Stennis",
|
||||
], "lhanames": [
|
||||
"LHA-1 Tarawa",
|
||||
"LHA-2 Saipan",
|
||||
"LHA-3 Belleau Wood",
|
||||
"LHA-4 Nassau",
|
||||
"LHA-5 Peleliu"
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Canada_2005 = {
|
||||
"country": "Canada",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
FA_18C_hornet,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.MBT_Leopard_2,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.APC_M113,
|
||||
Armor.IFV_MCV_80,
|
||||
|
||||
UH_1H,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
], "destroyer": [
|
||||
USS_Arleigh_Burke_IIa,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
China_2010 = {
|
||||
"country": "China",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
MiG_21Bis, # Standing as J-7
|
||||
Su_30,
|
||||
Su_33,
|
||||
J_11A,
|
||||
JF_17,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
|
||||
KJ_2000,
|
||||
|
||||
Mi_8MT,
|
||||
Mi_28N,
|
||||
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85C, # Standing as HQ-9+
|
||||
AirDefence.SAM_SA_6_Kub_LN_2P25,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
|
||||
Armor.ZTZ_96B,
|
||||
Armor.MBT_T_55,
|
||||
Armor.ZBD_04A,
|
||||
Armor.IFV_BMP_1,
|
||||
Artillery.MLRS_9A52_Smerch,
|
||||
Artillery.SPH_2S9_Nona,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Bulk_cargo_ship_Yakushev,
|
||||
Dry_cargo_ship_Ivanov,
|
||||
Tanker_Elnya_160
|
||||
],
|
||||
"shorad":[
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
AirDefence.Rapier_FSA_Launcher, # Standing as PL-9C Shorad
|
||||
AirDefence.HQ_7_Self_Propelled_LN
|
||||
], "aircraft_carrier": [
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
], "destroyer": [
|
||||
Type_052B_Destroyer,
|
||||
Type_052C_Destroyer
|
||||
], "cruiser": [
|
||||
Type_054A_Frigate,
|
||||
], "helicopter_carrier": [
|
||||
Type_071_Amphibious_Transport_Dock,
|
||||
], "lhanames": [
|
||||
"Kunlun Shan",
|
||||
"Jinggang Shan",
|
||||
"Changbai Shan",
|
||||
"Yimeng Shan",
|
||||
"Longhu Shan",
|
||||
"Wuzhi Shan",
|
||||
"Wudang Shan"
|
||||
], "carrier_names": [
|
||||
"001 Liaoning",
|
||||
"002 Shandong",
|
||||
], "boat":[
|
||||
"Type54GroupGenerator"
|
||||
],
|
||||
"has_jtac": True,
|
||||
"jtac_unit": WingLoong_I
|
||||
}
|
||||
284
game/factions/faction.py
Normal file
284
game/factions/faction.py
Normal file
@@ -0,0 +1,284 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict, Type, List, Any, cast
|
||||
|
||||
import dcs
|
||||
from dcs.countries import country_dict
|
||||
from dcs.planes import plane_map
|
||||
from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
|
||||
from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
|
||||
|
||||
from game.data.building_data import WW2_ALLIES_BUILDINGS, DEFAULT_AVAILABLE_BUILDINGS, WW2_GERMANY_BUILDINGS, WW2_FREE
|
||||
from game.data.doctrine import Doctrine, MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
|
||||
from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
|
||||
|
||||
|
||||
@dataclass
|
||||
class Faction:
|
||||
|
||||
# Country used by this faction
|
||||
country: str = field(default="")
|
||||
|
||||
# Nice name of the faction
|
||||
name: str = field(default="")
|
||||
|
||||
# List of faction file authors
|
||||
authors: str = field(default="")
|
||||
|
||||
# A description of the faction
|
||||
description: str = field(default="")
|
||||
|
||||
# Available aircraft
|
||||
aircrafts: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available awacs aircraft
|
||||
awacs: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available tanker aircraft
|
||||
tankers: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available frontline units
|
||||
frontline_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Available artillery units
|
||||
artillery_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Infantry units used
|
||||
infantry_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Logistics units used
|
||||
logistics_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Possible SAMS site generators for this faction
|
||||
air_defenses: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible EWR generators for this faction.
|
||||
ewrs: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible Missile site generators for this faction
|
||||
missiles: List[str] = field(default_factory=list)
|
||||
|
||||
# Required mods or asset packs
|
||||
requirements: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
# possible aircraft carrier units
|
||||
aircraft_carrier: List[Type[UnitType]] = field(default_factory=list)
|
||||
|
||||
# possible helicopter carrier units
|
||||
helicopter_carrier: List[Type[UnitType]] = field(default_factory=list)
|
||||
|
||||
# Possible carrier names
|
||||
carrier_names: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible helicopter carrier names
|
||||
helicopter_carrier_names: List[str] = field(default_factory=list)
|
||||
|
||||
# Navy group generators
|
||||
navy_generators: List[str] = field(default_factory=list)
|
||||
|
||||
# Available destroyers
|
||||
destroyers: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# Available cruisers
|
||||
cruisers: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# How many navy group should we try to generate per CP on startup for this faction
|
||||
navy_group_count: int = field(default=1)
|
||||
|
||||
# How many missiles group should we try to generate per CP on startup for this faction
|
||||
missiles_group_count: int = field(default=1)
|
||||
|
||||
# Whether this faction has JTAC access
|
||||
has_jtac: bool = field(default=False)
|
||||
|
||||
# Unit to use as JTAC for this faction
|
||||
jtac_unit: Optional[Type[FlyingType]] = field(default=None)
|
||||
|
||||
# doctrine
|
||||
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
|
||||
|
||||
# List of available buildings for this faction
|
||||
building_set: List[str] = field(default_factory=list)
|
||||
|
||||
# List of default livery overrides
|
||||
liveries_overrides: Dict[Type[UnitType], List[str]] = field(
|
||||
default_factory=dict)
|
||||
|
||||
#: Set to True if the faction should force the "Unrestricted satnav" option
|
||||
#: for the mission. This option enables GPS for capable aircraft regardless
|
||||
#: of the time period or operator. For example, the CJTF "countries" don't
|
||||
#: appear to have GPS capability, so they need this.
|
||||
#:
|
||||
#: Note that this option cannot be set per-side. If either faction needs it,
|
||||
#: both will use it.
|
||||
unrestricted_satnav: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
|
||||
faction = Faction()
|
||||
|
||||
faction.country = json.get("country", "/")
|
||||
if faction.country not in [c.name for c in country_dict.values()]:
|
||||
raise AssertionError("Faction's country (\"{}\") is not a valid DCS country ID".format(faction.country))
|
||||
|
||||
faction.name = json.get("name", "")
|
||||
if not faction.name:
|
||||
raise AssertionError("Faction has no valid name")
|
||||
|
||||
faction.authors = json.get("authors", "")
|
||||
faction.description = json.get("description", "")
|
||||
|
||||
faction.aircrafts = load_all_aircraft(json.get("aircrafts", []))
|
||||
faction.awacs = load_all_aircraft(json.get("awacs", []))
|
||||
faction.tankers = load_all_aircraft(json.get("tankers", []))
|
||||
|
||||
faction.frontline_units = load_all_vehicles(
|
||||
json.get("frontline_units", []))
|
||||
faction.artillery_units = load_all_vehicles(
|
||||
json.get("artillery_units", []))
|
||||
faction.infantry_units = load_all_vehicles(
|
||||
json.get("infantry_units", []))
|
||||
faction.logistics_units = load_all_vehicles(
|
||||
json.get("logistics_units", []))
|
||||
|
||||
faction.ewrs = json.get("ewrs", [])
|
||||
|
||||
faction.air_defenses = json.get("air_defenses", [])
|
||||
# Compatibility for older factions. All air defenses now belong to a
|
||||
# single group and the generator decides what belongs where.
|
||||
faction.air_defenses.extend(json.get("sams", []))
|
||||
faction.air_defenses.extend(json.get("shorads", []))
|
||||
|
||||
faction.missiles = json.get("missiles", [])
|
||||
faction.requirements = json.get("requirements", {})
|
||||
|
||||
faction.carrier_names = json.get("carrier_names", [])
|
||||
faction.helicopter_carrier_names = json.get(
|
||||
"helicopter_carrier_names", [])
|
||||
faction.navy_generators = json.get("navy_generators", [])
|
||||
faction.aircraft_carrier = load_all_ships(
|
||||
json.get("aircraft_carrier", []))
|
||||
faction.helicopter_carrier = load_all_ships(
|
||||
json.get("helicopter_carrier", []))
|
||||
faction.destroyers = load_all_ships(json.get("destroyers", []))
|
||||
faction.cruisers = load_all_ships(json.get("cruisers", []))
|
||||
faction.has_jtac = json.get("has_jtac", False)
|
||||
jtac_name = json.get("jtac_unit", None)
|
||||
if jtac_name is not None:
|
||||
faction.jtac_unit = load_aircraft(jtac_name)
|
||||
else:
|
||||
faction.jtac_unit = None
|
||||
faction.navy_group_count = int(json.get("navy_group_count", 1))
|
||||
faction.missiles_group_count = int(json.get("missiles_group_count", 0))
|
||||
|
||||
# Load doctrine
|
||||
doctrine = json.get("doctrine", "modern")
|
||||
if doctrine == "modern":
|
||||
faction.doctrine = MODERN_DOCTRINE
|
||||
elif doctrine == "coldwar":
|
||||
faction.doctrine = COLDWAR_DOCTRINE
|
||||
elif doctrine == "ww2":
|
||||
faction.doctrine = WWII_DOCTRINE
|
||||
else:
|
||||
faction.doctrine = MODERN_DOCTRINE
|
||||
|
||||
# Load the building set
|
||||
building_set = json.get("building_set", "default")
|
||||
if building_set == "default":
|
||||
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
|
||||
elif building_set == "ww2free":
|
||||
faction.building_set = WW2_FREE
|
||||
elif building_set == "ww2ally":
|
||||
faction.building_set = WW2_ALLIES_BUILDINGS
|
||||
elif building_set == "ww2germany":
|
||||
faction.building_set = WW2_GERMANY_BUILDINGS
|
||||
else:
|
||||
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
|
||||
|
||||
# Load liveries override
|
||||
faction.liveries_overrides = {}
|
||||
liveries_overrides = json.get("liveries_overrides", {})
|
||||
for k, v in liveries_overrides.items():
|
||||
k = load_aircraft(k)
|
||||
if k is not None:
|
||||
faction.liveries_overrides[k] = [s.lower() for s in v]
|
||||
|
||||
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
|
||||
|
||||
return faction
|
||||
|
||||
@property
|
||||
def units(self) -> List[Type[UnitType]]:
|
||||
return (self.infantry_units + self.aircrafts + self.awacs +
|
||||
self.artillery_units + self.frontline_units +
|
||||
self.tankers + self.logistics_units)
|
||||
|
||||
|
||||
def unit_loader(
|
||||
unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
|
||||
"""
|
||||
Find unit by name
|
||||
:param unit: Unit name as string
|
||||
:param class_repository: Repository of classes (Either a module, a class, or a list of classes)
|
||||
:return: The unit as a PyDCS type
|
||||
"""
|
||||
if unit is None:
|
||||
return None
|
||||
elif unit in plane_map.keys():
|
||||
return plane_map[unit]
|
||||
else:
|
||||
for mother_class in class_repository:
|
||||
if getattr(mother_class, unit, None) is not None:
|
||||
return getattr(mother_class, unit)
|
||||
if type(mother_class) is list:
|
||||
for m in mother_class:
|
||||
if m.__name__ == unit:
|
||||
return m
|
||||
logging.error(f"FACTION ERROR : Unable to find {unit} in pydcs")
|
||||
return None
|
||||
|
||||
|
||||
def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(
|
||||
name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]
|
||||
))
|
||||
|
||||
|
||||
def load_all_aircraft(data) -> List[Type[FlyingType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_aircraft(name)
|
||||
if item is not None:
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(
|
||||
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
|
||||
))
|
||||
|
||||
|
||||
def load_all_vehicles(data) -> List[Type[VehicleType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_vehicle(name)
|
||||
if item is not None:
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def load_ship(name: str) -> Optional[Type[ShipType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(name, [dcs.ships]))
|
||||
|
||||
|
||||
def load_all_ships(data) -> List[Type[ShipType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_ship(name)
|
||||
if item is not None:
|
||||
items.append(item)
|
||||
return items
|
||||
46
game/factions/faction_loader.py
Normal file
46
game/factions/faction_loader.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterator, Optional, Type
|
||||
|
||||
from game.factions.faction import Faction
|
||||
|
||||
FACTION_DIRECTORY = Path("./resources/factions/")
|
||||
|
||||
|
||||
class FactionLoader:
|
||||
def __init__(self) -> None:
|
||||
self._factions: Optional[Dict[str, Faction]] = None
|
||||
|
||||
@property
|
||||
def factions(self) -> Dict[str, Faction]:
|
||||
self.initialize()
|
||||
assert self._factions is not None
|
||||
return self._factions
|
||||
|
||||
def initialize(self) -> None:
|
||||
if self._factions is None:
|
||||
self._factions = self.load_factions()
|
||||
|
||||
@classmethod
|
||||
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
|
||||
files = [f for f in FACTION_DIRECTORY.glob("*.json") if f.is_file()]
|
||||
factions = {}
|
||||
|
||||
for f in files:
|
||||
try:
|
||||
with f.open("r", encoding="utf-8") as fdata:
|
||||
data = json.load(fdata, encoding="utf-8")
|
||||
factions[data["name"]] = Faction.from_json(data)
|
||||
logging.info("Loaded faction : " + str(f))
|
||||
except Exception:
|
||||
logging.exception(f"Unable to load faction : {f}")
|
||||
|
||||
return factions
|
||||
|
||||
def __getitem__(self, name: str) -> Faction:
|
||||
return self.factions[name]
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self.factions.keys())
|
||||
@@ -1,47 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
France_1995 = {
|
||||
"country": "France",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
SA342M,
|
||||
SA342L,
|
||||
SA342Mistral,
|
||||
|
||||
Armor.MBT_Leclerc,
|
||||
Armor.TPz_Fuchs, # Standing as VAB
|
||||
Armor.APC_Cobra, # Standing as VBL
|
||||
Armor.ATGM_M1134_Stryker, # Standing as VAB Mephisto
|
||||
Artillery.SPH_M109_Paladin, # Standing as AMX30 AuF1
|
||||
Artillery.MLRS_M270,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.HQ_7_Self_Propelled_LN, # Standing as Crotale
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
|
||||
], "shorad": [
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.SAM_Roland_ADS
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
France_2005 = {
|
||||
"country": "France",
|
||||
"side": "blue",
|
||||
"units":[
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
FA_18C_hornet, # Standing as Rafale M
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
SA342M,
|
||||
SA342L,
|
||||
SA342Mistral,
|
||||
|
||||
Armor.MBT_Leclerc,
|
||||
Armor.TPz_Fuchs, # Standing as VAB
|
||||
Armor.APC_Cobra, # Standing as VBL
|
||||
Armor.ATGM_M1134_Stryker, # Standing as VAB Mephisto
|
||||
Artillery.SPH_M109_Paladin, # Standing as AMX30 AuF1
|
||||
Artillery.MLRS_M270,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.HQ_7_Self_Propelled_LN, # Standing as Crotale
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
|
||||
], "shorad":[
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.SAM_Roland_ADS
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis, # Standing as CDG Aircraft Carrier
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa, # Standing as Mistral Class
|
||||
], "destroyer": [
|
||||
Oliver_Hazzard_Perry_class,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "carrier_names": [
|
||||
"PA Charles de Gaulle",
|
||||
], "lhanames": [
|
||||
"L9013 Mistral",
|
||||
"L9014 Tonerre",
|
||||
"L9015 Dixmude"
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
import pydcs_extensions.frenchpack.frenchpack as frenchpack
|
||||
from pydcs_extensions.rafale.rafale import Rafale_M, Rafale_A_S
|
||||
|
||||
France_2005_Modded = {
|
||||
"country": "France",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
Rafale_M,
|
||||
Rafale_A_S,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
SA342M,
|
||||
SA342L,
|
||||
SA342Mistral,
|
||||
|
||||
Armor.MBT_Leclerc,
|
||||
Artillery.SPH_M109_Paladin, # Standing as AMX30 AuF1
|
||||
Artillery.MLRS_M270,
|
||||
|
||||
frenchpack.AMX_10RCR,
|
||||
frenchpack.AMX_10RCR_SEPAR,
|
||||
frenchpack.ERC_90,
|
||||
frenchpack.TRM_2000_PAMELA,
|
||||
frenchpack.VAB__50,
|
||||
frenchpack.VAB_MEPHISTO,
|
||||
frenchpack.VAB_T20_13,
|
||||
frenchpack.VBL__50,
|
||||
frenchpack.VBL_AANF1,
|
||||
frenchpack.VBAE_CRAB,
|
||||
frenchpack.VBAE_CRAB_MMP,
|
||||
frenchpack.AMX_30B2,
|
||||
frenchpack.Leclerc_Serie_XXI,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.HQ_7_Self_Propelled_LN, # Standing as Crotale
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
|
||||
], "shorad": [
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.SAM_Roland_ADS
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis, # Standing as CDG Aircraft Carrier
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa, # Standing as Mistral Class
|
||||
], "destroyer": [
|
||||
Oliver_Hazzard_Perry_class,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "carrier_names": [
|
||||
"PA Charles de Gaulle",
|
||||
], "lhanames": [
|
||||
"L9013 Mistral",
|
||||
"L9014 Tonerre",
|
||||
"L9015 Dixmude"
|
||||
], "boat": [
|
||||
"ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
|
||||
], "requirements": {
|
||||
"frenchpack V3.5": "https://forums.eagle.ru/showthread.php?t=279974",
|
||||
"RAFALE 2.5.5": "https://www.digitalcombatsimulator.com/fr/files/3307478/",
|
||||
}, "has_jtac": True
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
from dcs.planes import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from game.data.building_data import WW2_GERMANY_BUILDINGS
|
||||
from game.data.doctrine import WWII_DOCTRINE
|
||||
|
||||
Germany_1944 = {
|
||||
"country": "Third Reich",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
Bf_109K_4,
|
||||
Ju_88A4,
|
||||
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||
Armor.MT_Pz_Kpfw_IV_Ausf_H,
|
||||
Armor.HT_Pz_Kpfw_VI_Tiger_I,
|
||||
Armor.HT_Pz_Kpfw_VI_Ausf__B__Tiger_II,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.Sd_Kfz_184_Elefant,
|
||||
Armor.TD_Jagdpanther_G1,
|
||||
Armor.TD_Jagdpanzer_IV,
|
||||
|
||||
Artillery.Sturmpanzer_IV_Brummbär,
|
||||
|
||||
Unarmed.Sd_Kfz_2,
|
||||
Unarmed.Sd_Kfz_7,
|
||||
Unarmed.Kübelwagen_82,
|
||||
|
||||
Infantry.Infantry_Mauser_98,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
],"requirements":{
|
||||
"WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
|
||||
},
|
||||
"shorad": [
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
],
|
||||
"objects": WW2_GERMANY_BUILDINGS,
|
||||
"doctrine": WWII_DOCTRINE,
|
||||
"boat": ["UBoatGroupGenerator", "SchnellbootGroupGenerator"],
|
||||
"boat_count": 2,
|
||||
"missiles": ["V1GroupGenerator"],
|
||||
"missiles_count": 1
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
from dcs.planes import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from game.data.building_data import WW2_GERMANY_BUILDINGS
|
||||
from game.data.doctrine import WWII_DOCTRINE
|
||||
|
||||
Germany_1944_Easy = {
|
||||
"country": "Third Reich",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
Bf_109K_4,
|
||||
Ju_88A4,
|
||||
|
||||
Armor.MT_Pz_Kpfw_IV_Ausf_H,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Artillery.Sturmpanzer_IV_Brummbär,
|
||||
|
||||
Unarmed.Sd_Kfz_2,
|
||||
Unarmed.Sd_Kfz_7,
|
||||
Unarmed.Kübelwagen_82,
|
||||
|
||||
Infantry.Infantry_Mauser_98,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
],"requirements":{
|
||||
"WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
|
||||
},
|
||||
"shorad":[
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
],
|
||||
"objects": WW2_GERMANY_BUILDINGS,
|
||||
"doctrine": WWII_DOCTRINE,
|
||||
"boat": ["UBoatGroupGenerator", "SchnellbootGroupGenerator"],
|
||||
"boat_count": 1,
|
||||
"missiles": ["V1GroupGenerator"],
|
||||
"missiles_count": 1
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Germany_1990 = {
|
||||
"country": "Germany",
|
||||
"side": "blue",
|
||||
"units":[
|
||||
MiG_29G,
|
||||
Tornado_IDS,
|
||||
F_4E,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
SA342M,
|
||||
SA342L,
|
||||
|
||||
Armor.TPz_Fuchs,
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.MBT_Leopard_2,
|
||||
Armor.IFV_Marder,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
],
|
||||
"shorad":[
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
], "boat":[
|
||||
"OliverHazardPerryGroupGenerator"
|
||||
]
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
India_2010 = {
|
||||
"country": "India",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
Mirage_2000_5,
|
||||
M_2000C,
|
||||
MiG_27K,
|
||||
MiG_21Bis,
|
||||
MiG_29S,
|
||||
Su_30,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
AH_64A,
|
||||
Mi_8MT,
|
||||
|
||||
Armor.MBT_T_90,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.IFV_BMP_2,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
|
||||
AirDefence.SAM_SA_6_Kub_LN_2P25,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
],
|
||||
"shorad":[
|
||||
AirDefence.SAM_SA_8_Osa_9A33,
|
||||
AirDefence.AAA_ZU_23_Emplacement,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
|
||||
AirDefence.SAM_SA_8_Osa_9A33,
|
||||
AirDefence.SAM_SA_19_Tunguska_2S6
|
||||
], "aircraft_carrier": [
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
], "destroyer": [
|
||||
FSG_1241_1MP_Molniya,
|
||||
], "carrier_names": [
|
||||
"INS Vikramaditya"
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator", "MolniyaGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
from dcs.vehicles import *
|
||||
from dcs.ships import *
|
||||
from dcs.planes import *
|
||||
from dcs.helicopters import *
|
||||
|
||||
Insurgent = {
|
||||
"country": "Insurgents",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
AirDefence.AAA_ZU_23_Insurgent_Closed,
|
||||
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
|
||||
|
||||
Armor.APC_Cobra,
|
||||
Armor.APC_MTLB,
|
||||
Armor.ARV_BRDM_2,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
Infantry.Infantry_Soldier_Insurgents,
|
||||
Infantry.Soldier_RPG,
|
||||
|
||||
Bulk_cargo_ship_Yakushev,
|
||||
Dry_cargo_ship_Ivanov,
|
||||
Tanker_Elnya_160
|
||||
]
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from pydcs_extensions.frenchpack.frenchpack import DIM__TOYOTA_BLUE, DIM__TOYOTA_DESERT, DIM__TOYOTA_GREEN, \
|
||||
DIM__KAMIKAZE
|
||||
|
||||
Insurgent_modded = {
|
||||
"country": "Insurgents",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
AirDefence.AAA_ZU_23_Insurgent_Closed,
|
||||
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
|
||||
|
||||
DIM__TOYOTA_BLUE,
|
||||
DIM__TOYOTA_DESERT,
|
||||
DIM__TOYOTA_GREEN,
|
||||
DIM__KAMIKAZE,
|
||||
Armor.ARV_BRDM_2,
|
||||
Armor.APC_Cobra,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
Infantry.Soldier_AK,
|
||||
Infantry.Infantry_Soldier_Insurgents,
|
||||
|
||||
Bulk_cargo_ship_Yakushev,
|
||||
Dry_cargo_ship_Ivanov,
|
||||
Tanker_Elnya_160
|
||||
], "requirements": {
|
||||
"frenchpack V3.5": "https://forums.eagle.ru/showthread.php?t=279974",
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Iran_2015 = {
|
||||
"country": "Iran",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
MiG_29A,
|
||||
F_4E,
|
||||
F_14B,
|
||||
F_5E_3,
|
||||
|
||||
MiG_21Bis,
|
||||
Su_24M,
|
||||
Su_25,
|
||||
Su_17M4,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
|
||||
A_50,
|
||||
|
||||
Mi_28N,
|
||||
Mi_24V,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_SA_2_LN_SM_90,
|
||||
AirDefence.SAM_SA_6_Kub_LN_2P25,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.SAM_SA_11_Buk_LN_9A310M1,
|
||||
|
||||
Armor.APC_M113,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.MBT_T_72B,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
Infantry.Soldier_AK,
|
||||
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Bulk_cargo_ship_Yakushev,
|
||||
Dry_cargo_ship_Ivanov,
|
||||
Tanker_Elnya_160
|
||||
],
|
||||
"shorad":[
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.AAA_ZU_23_Insurgent_Closed
|
||||
], "boat":[
|
||||
"GrishaGroupGenerator", "MolniyaGroupGenerator", "KiloSubGroupGenerator"
|
||||
]
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Israel_1948 = {
|
||||
"country": "Israel",
|
||||
"side": "blue",
|
||||
"units":[
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
Bf_109K_4, # Standing as Avia S-199
|
||||
B_17G,
|
||||
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
Armor.APC_M2A1,
|
||||
Armor.MT_M4_Sherman,
|
||||
Armor.LAC_M8_Greyhound,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
Armed_speedboat,
|
||||
],"requirements":{
|
||||
"WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
|
||||
},
|
||||
"shorad": [
|
||||
AirDefence.AAA_Bofors_40mm
|
||||
], "boat": [
|
||||
], "has_jtac": False
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
|
||||
Israel_1973 = {
|
||||
"country": "Israel",
|
||||
"side": "blue",
|
||||
"units":[
|
||||
F_4E,
|
||||
A_4E_C,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
Armor.APC_M2A1,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.APC_M113,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
|
||||
Armed_speedboat,
|
||||
], "requirements": {
|
||||
"Community A-4E": "https://heclak.github.io/community-a4e-c/",
|
||||
"WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
|
||||
}, "shorad": [
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
AirDefence.AAA_Bofors_40mm
|
||||
], "boat": [
|
||||
], "has_jtac": True
|
||||
}
|
||||
|
||||
Israel_1973_NO_WW2_UNITS = {
|
||||
"country": "Israel",
|
||||
"side": "blue",
|
||||
"units":[
|
||||
F_4E,
|
||||
A_4E_C,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.APC_M113,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
|
||||
Armed_speedboat,
|
||||
], "requirements": {
|
||||
"Community A-4E": "https://heclak.github.io/community-a4e-c/",
|
||||
}, "shorad": [
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
], "boat": [
|
||||
], "has_jtac": True
|
||||
}
|
||||
|
||||
Israel_1982 = {
|
||||
"country": "Israel",
|
||||
"side": "blue",
|
||||
"units":[
|
||||
F_4E,
|
||||
A_4E_C,
|
||||
F_15C,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
AH_1W,
|
||||
|
||||
Armor.APC_M113,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.MBT_Merkava_Mk__4,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
|
||||
Armed_speedboat,
|
||||
], "requirements": {
|
||||
"Community A-4E": "https://heclak.github.io/community-a4e-c/",
|
||||
}, "shorad": [
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
], "boat": [
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Israel_2000 = {
|
||||
"country": "Israel",
|
||||
"side": "blue",
|
||||
"units":[
|
||||
F_16C_50,
|
||||
F_15C,
|
||||
F_15E,
|
||||
F_4E,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
AH_1W,
|
||||
AH_64D,
|
||||
|
||||
Armor.MBT_Merkava_Mk__4,
|
||||
Armor.APC_M113,
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
Armor.ATGM_M1045_HMMWV_TOW,
|
||||
Artillery.SPH_M109_Paladin,
|
||||
Artillery.MLRS_M270,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
|
||||
AirDefence.SAM_Patriot_EPP_III,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
],
|
||||
"shorad": [
|
||||
AirDefence.SAM_Avenger_M1097
|
||||
], "boat": [
|
||||
"ArleighBurkeGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Italy_1990 = {
|
||||
"country": "Italy",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
Tornado_IDS,
|
||||
AV8BNA,
|
||||
# MB339,
|
||||
|
||||
KC_135,
|
||||
S_3B_Tanker,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
AH_1W,
|
||||
UH_1H,
|
||||
|
||||
Armor.MBT_Leopard_1A3, # OF-40 MBT
|
||||
Armor.APC_M113,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad":[
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "destroyer": [
|
||||
Oliver_Hazzard_Perry_class,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "lhanames": [
|
||||
"Giuseppe Garibaldi",
|
||||
"Cavour",
|
||||
], "boat":[
|
||||
"OliverHazardPerryGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
|
||||
Italy_1990_MB339 = {
|
||||
"country": "Italy",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
Tornado_IDS,
|
||||
AV8BNA,
|
||||
MB_339PAN,
|
||||
|
||||
KC_135,
|
||||
S_3B_Tanker,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
AH_1W,
|
||||
UH_1H,
|
||||
|
||||
Armor.MBT_Leopard_1A3, # OF-40 MBT
|
||||
Armor.APC_M113,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad":[
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "destroyer": [
|
||||
Oliver_Hazzard_Perry_class,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "lhanames": [
|
||||
"Giuseppe Garibaldi",
|
||||
"Cavour",
|
||||
], "boat": [
|
||||
"OliverHazardPerryGroupGenerator"
|
||||
], "requirements": {
|
||||
"MB-339A": "http://www.freccetricolorivirtuali.net/",
|
||||
}, "has_jtac": True
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Japan_2005 = {
|
||||
"country": "Japan",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
F_15C, # F-15J/DJ
|
||||
F_16C_50, # F-2A/B
|
||||
F_4E, # F-4EJ
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
AH_1W,
|
||||
AH_64D,
|
||||
|
||||
Armor.MBT_Merkava_Mk__4, # Standing as Type 10 MBT
|
||||
Armor.MBT_M1A2_Abrams, # Standing as Type 90 MBT
|
||||
Armor.IFV_Marder, # Standing as Type 89 IFV
|
||||
Armor.TPz_Fuchs, # Standing as Type 96 APC
|
||||
Armor.IFV_LAV_25, # Standing as Type 16 or Type 87
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
|
||||
Artillery.MLRS_M270,
|
||||
Artillery.SPH_M109_Paladin, # Standing as Type 99 SPH
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Patriot_EPP_III,
|
||||
|
||||
LHA_1_Tarawa,
|
||||
], "shorad": [
|
||||
AirDefence.SPAAA_Gepard, # Type 87 SPAG
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa, # Standing as Hyuga-class helicopter carrier
|
||||
], "destroyer": [
|
||||
USS_Arleigh_Burke_IIa,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "lhanames": [
|
||||
"Hyuga",
|
||||
"Ise",
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Libya_2011 = {
|
||||
"country": "Libya",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
Mi_24V,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
A_50,
|
||||
|
||||
AirDefence.SAM_SA_8_Osa_9A33,
|
||||
AirDefence.SAM_SA_2_LN_SM_90,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73,
|
||||
AirDefence.SAM_SA_6_Kub_LN_2P25,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.FDDM_Grad,
|
||||
Armor.ARV_BRDM_2,
|
||||
Armor.MBT_T_55,
|
||||
Armor.MBT_T_72B,
|
||||
Artillery.MLRS_BM_21_Grad,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Infantry_Soldier_Insurgents
|
||||
|
||||
],
|
||||
"shorad":[
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.SAM_SA_8_Osa_9A33,
|
||||
], "boat": [
|
||||
"GrishaGroupGenerator", "MolniyaGroupGenerator"
|
||||
]
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Netherlands_1990 = {
|
||||
"country": "The Netherlands",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
F_16C_50,
|
||||
F_5E_3,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
AH_64A,
|
||||
|
||||
Armor.APC_M113,
|
||||
Armor.MBT_Leopard_1A3,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
],
|
||||
"shorad": [
|
||||
AirDefence.SAM_Avenger_M1097
|
||||
], "boat": [
|
||||
"OliverHazardPerryGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
NorthKorea_2000 = {
|
||||
"country": "North Korea",
|
||||
"side": "red",
|
||||
"units":[
|
||||
MiG_29A,
|
||||
Su_25,
|
||||
MiG_15bis,
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
MiG_19P,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
|
||||
A_50,
|
||||
|
||||
Mi_8MT,
|
||||
Mi_24V,
|
||||
|
||||
Armor.MBT_T_55,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.MBT_T_80U,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.ARV_BRDM_2,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Soldier_AK,
|
||||
|
||||
AirDefence.SAM_SA_2_LN_SM_90,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73,
|
||||
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Bulk_cargo_ship_Yakushev,
|
||||
Dry_cargo_ship_Ivanov,
|
||||
Tanker_Elnya_160
|
||||
],
|
||||
"shorad":[
|
||||
AirDefence.AAA_ZU_23_Emplacement,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka
|
||||
],
|
||||
"boat": [
|
||||
"GrishaGroupGenerator", "MolniyaGroupGenerator"
|
||||
]
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Pakistan_2015 = {
|
||||
"country": "Pakistan",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
JF_17,
|
||||
F_16C_50,
|
||||
MiG_21Bis, # Standing as J-7
|
||||
MiG_19P, # Standing as J-6
|
||||
IL_78M,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
AH_1W,
|
||||
|
||||
Armor.MBT_T_80U,
|
||||
Armor.MBT_T_55, # Standing as Al-Zarrar / Type 59 MBT
|
||||
Armor.ZBD_04A,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.APC_M113,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
|
||||
AirDefence.SAM_SA_2_LN_SM_90, # Standing as HQ-2
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85C, # Standing as HQ-9
|
||||
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Closed
|
||||
], "boat": [
|
||||
"Type54GroupGenerator", "OliverHazardPerryGroupGenerator"
|
||||
],
|
||||
"has_jtac": True,
|
||||
"jtac_unit": WingLoong_I
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
|
||||
PMC_WESTERN_A = {
|
||||
"country": "USA",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
C_101CC,
|
||||
|
||||
UH_1H,
|
||||
Mi_8MT,
|
||||
OH_58D,
|
||||
SA342M,
|
||||
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
Armor.IFV_MCV_80,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
|
||||
Armed_speedboat,
|
||||
], "shorad":[
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
], "has_jtac": True
|
||||
}
|
||||
|
||||
PMC_WESTERN_B = {
|
||||
"country": "USA",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
MB_339PAN,
|
||||
C_101CC,
|
||||
|
||||
UH_1H,
|
||||
Mi_8MT,
|
||||
OH_58D,
|
||||
SA342M,
|
||||
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
Armor.IFV_MCV_80,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
|
||||
Armed_speedboat,
|
||||
], "shorad":[
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
], "has_jtac": True,
|
||||
"requirements": {
|
||||
"MB-339A": "http://www.freccetricolorivirtuali.net/",
|
||||
}
|
||||
}
|
||||
|
||||
PMC_RUSSIAN = {
|
||||
"country": "Russia",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
L_39C,
|
||||
L_39ZA,
|
||||
|
||||
Mi_8MT,
|
||||
Mi_24V,
|
||||
Ka_50,
|
||||
|
||||
Armor.APC_Cobra,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.ARV_BRDM_2,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
|
||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
|
||||
Armed_speedboat,
|
||||
], "shorad":[
|
||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Closed,
|
||||
], "has_jtac": True
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
from dcs.planes import MiG_15bis, IL_76MD, IL_78M, An_26B, An_30M, Yak_40
|
||||
from dcs.ships import CV_1143_5_Admiral_Kuznetsov, Bulk_cargo_ship_Yakushev, Dry_cargo_ship_Ivanov, Tanker_Elnya_160
|
||||
from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery
|
||||
|
||||
Russia_1955 = {
|
||||
"country": "Russia",
|
||||
"side": "red",
|
||||
"units": [
|
||||
MiG_15bis,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
|
||||
AirDefence.AAA_ZU_23_Closed,
|
||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
Armor.ARV_BRDM_2,
|
||||
Armor.FDDM_Grad,
|
||||
Armor.APC_MTLB,
|
||||
Armor.MBT_T_55,
|
||||
Artillery.MLRS_BM_21_Grad,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Bulk_cargo_ship_Yakushev,
|
||||
Dry_cargo_ship_Ivanov,
|
||||
Tanker_Elnya_160,
|
||||
|
||||
# Infantry squad
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
]
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
from dcs.helicopters import Mi_8MT
|
||||
from dcs.planes import MiG_15bis, MiG_19P, MiG_21Bis, IL_76MD, IL_78M, An_26B, An_30M, Yak_40, A_50
|
||||
from dcs.ships import CV_1143_5_Admiral_Kuznetsov, Bulk_cargo_ship_Yakushev, Dry_cargo_ship_Ivanov, Tanker_Elnya_160
|
||||
from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery
|
||||
|
||||
Russia_1965 = {
|
||||
"country": "Russia",
|
||||
"side": "red",
|
||||
"units": [
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
|
||||
A_50,
|
||||
|
||||
Mi_8MT,
|
||||
|
||||
AirDefence.SAM_SA_6_Kub_LN_2P25,
|
||||
AirDefence.SAM_SA_2_LN_SM_90,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73,
|
||||
|
||||
Armor.ARV_BRDM_2,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.ARV_BTR_RD,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.MBT_T_55,
|
||||
Artillery.MLRS_BM_21_Grad,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Bulk_cargo_ship_Yakushev,
|
||||
Dry_cargo_ship_Ivanov,
|
||||
Tanker_Elnya_160,
|
||||
|
||||
# Infantry squad
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
|
||||
],
|
||||
"shorad":[
|
||||
AirDefence.AAA_ZU_23_Closed
|
||||
], "boat": [
|
||||
"GrishaGroupGenerator"
|
||||
]
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
from dcs.helicopters import Mi_8MT, Mi_24V
|
||||
from dcs.planes import MiG_21Bis, MiG_23MLD, MiG_25PD, MiG_29A, Su_17M4, Su_24M, Su_25, IL_76MD, IL_78M, An_26B, An_30M, \
|
||||
Yak_40, A_50
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery
|
||||
|
||||
Russia_1975 = {
|
||||
"country": "Russia",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
MiG_25PD,
|
||||
MiG_29A,
|
||||
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_25,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
|
||||
A_50,
|
||||
|
||||
Mi_8MT,
|
||||
Mi_24V,
|
||||
|
||||
AirDefence.AAA_ZU_23_Closed,
|
||||
AirDefence.SAM_SA_6_Kub_LN_2P25,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73,
|
||||
|
||||
Armor.ARV_BRDM_2,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.MBT_T_55,
|
||||
|
||||
Artillery.SPH_2S9_Nona,
|
||||
Artillery.SPH_2S1_Gvozdika,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Bulk_cargo_ship_Yakushev,
|
||||
Dry_cargo_ship_Ivanov,
|
||||
Tanker_Elnya_160,
|
||||
|
||||
# Infantry squad
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
|
||||
],
|
||||
"shorad": [
|
||||
AirDefence.AAA_ZU_23_Emplacement,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka
|
||||
], "aircraft_carrier": [
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
], "destroyer": [
|
||||
FF_1135M_Rezky,
|
||||
], "cruiser": [
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
], "boat": [
|
||||
"RussianNavyGroupGenerator", "KiloSubGroupGenerator", "MolniyaGroupGenerator"
|
||||
]
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Russia_1990 = {
|
||||
"country": "Russia",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
MiG_23MLD,
|
||||
MiG_25PD,
|
||||
MiG_29A,
|
||||
MiG_29S,
|
||||
MiG_31,
|
||||
Su_27,
|
||||
|
||||
Su_24M,
|
||||
Su_25,
|
||||
Ka_50,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
|
||||
A_50,
|
||||
|
||||
Mi_8MT,
|
||||
Mi_24V,
|
||||
|
||||
AirDefence.AAA_ZU_23_Closed,
|
||||
AirDefence.SAM_SA_6_Kub_LN_2P25,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73,
|
||||
|
||||
Armor.ARV_BRDM_2,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.MBT_T_55,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27,
|
||||
Artillery.SPH_2S19_Msta,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Bulk_cargo_ship_Yakushev,
|
||||
Dry_cargo_ship_Ivanov,
|
||||
Tanker_Elnya_160,
|
||||
|
||||
# Infantry squad
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
],
|
||||
"shorad":[
|
||||
AirDefence.SAM_SA_9_Strela_1_9P31,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka
|
||||
], "carrier_names": [
|
||||
"Admiral Kuznetov",
|
||||
"Admiral Gorshkov"
|
||||
], "aircraft_carrier": [
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
], "destroyer": [
|
||||
FF_1135M_Rezky,
|
||||
], "cruiser": [
|
||||
FSG_1241_1MP_Molniya,
|
||||
], "boat":[
|
||||
"RussianNavyGroupGenerator", "KiloSubGroupGenerator"
|
||||
]
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Russia_2010 = {
|
||||
"country": "Russia",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
Su_27,
|
||||
Su_30,
|
||||
Su_33,
|
||||
MiG_29S,
|
||||
MiG_31,
|
||||
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_34,
|
||||
Su_24M,
|
||||
L_39ZA,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
A_50,
|
||||
|
||||
Ka_50,
|
||||
Mi_8MT,
|
||||
Mi_24V,
|
||||
Mi_28N,
|
||||
|
||||
AirDefence.SAM_SA_19_Tunguska_2S6,
|
||||
AirDefence.SAM_SA_11_Buk_LN_9A310M1,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85C,
|
||||
|
||||
Armor.APC_BTR_80,
|
||||
Armor.MBT_T_90,
|
||||
Armor.MBT_T_80U,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.IFV_BMP_2,
|
||||
Armor.IFV_BMP_3,
|
||||
|
||||
Artillery.MLRS_9K57_Uragan_BM_27,
|
||||
Artillery.SPH_2S19_Msta,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Bulk_cargo_ship_Yakushev,
|
||||
Dry_cargo_ship_Ivanov,
|
||||
Tanker_Elnya_160,
|
||||
|
||||
# Infantry squad
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
],
|
||||
"shorad":[
|
||||
AirDefence.SAM_SA_19_Tunguska_2S6,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_9A35M3
|
||||
], "aircraft_carrier": [
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
], "carrier_names": [
|
||||
"Admiral Kuznetov"
|
||||
], "destroyer": [
|
||||
FF_1135M_Rezky,
|
||||
], "cruiser": [
|
||||
FSG_1241_1MP_Molniya,
|
||||
], "boat": [
|
||||
"RussianNavyGroupGenerator", "KiloSubGroupGenerator"
|
||||
]
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Spain_1990 = {
|
||||
"country": "Spain",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
FA_18C_hornet,
|
||||
AV8BNA,
|
||||
F_5E_3,
|
||||
C_101CC,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.MBT_Leopard_2,
|
||||
Armor.APC_M113,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad":[
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis, # Standing as Principe de Asturias
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa, # Standing as Juan Carlos
|
||||
], "destroyer": [
|
||||
Oliver_Hazzard_Perry_class,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "carrier_names": [
|
||||
"Principe de Asturias",
|
||||
], "lhanames": [
|
||||
"Juan Carlos I",
|
||||
], "boat":[
|
||||
"OliverHazardPerryGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
from dcs.vehicles import *
|
||||
from dcs.ships import *
|
||||
from dcs.planes import *
|
||||
from dcs.helicopters import *
|
||||
|
||||
Sweden_1990 = {
|
||||
"country": "Sweden",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
AJS37,
|
||||
|
||||
UH_1H,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
|
||||
Armor.IFV_MCV_80, # Standing as Strf 90
|
||||
Armor.MBT_Leopard_2,
|
||||
Armor.APC_M1126_Stryker_ICV, # Closest thing available
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
Infantry.Soldier_AK,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Bulk_cargo_ship_Yakushev,
|
||||
Dry_cargo_ship_Ivanov,
|
||||
Tanker_Elnya_160,
|
||||
],
|
||||
"shorad": [
|
||||
AirDefence.SAM_Avenger_M1097
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Syria_2011 = {
|
||||
"country": "Syria",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
MiG_25PD,
|
||||
MiG_29S,
|
||||
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
|
||||
L_39ZA,
|
||||
|
||||
Mi_24V,
|
||||
Mi_8MT,
|
||||
SA342M,
|
||||
SA342L,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
A_50,
|
||||
|
||||
AirDefence.SAM_SA_6_Kub_LN_2P25,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73,
|
||||
AirDefence.SAM_SA_2_LN_SM_90,
|
||||
AirDefence.SAM_SA_8_Osa_9A33,
|
||||
AirDefence.SAM_SA_11_Buk_LN_9A310M1,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85C,
|
||||
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.IFV_BMP_2,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.ARV_BRDM_2,
|
||||
Armor.APC_MTLB,
|
||||
Armor.APC_Cobra,
|
||||
Armor.MBT_T_55,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.MBT_T_90,
|
||||
Artillery.MLRS_BM_21_Grad,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27,
|
||||
Artillery.SPH_2S1_Gvozdika,
|
||||
Artillery.SPH_2S9_Nona,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Soldier_AK
|
||||
|
||||
],
|
||||
"shorad": [
|
||||
AirDefence.SAM_SA_8_Osa_9A33,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
|
||||
AirDefence.SAM_SA_9_Strela_1_9P31,
|
||||
AirDefence.SAM_SA_19_Tunguska_2S6,
|
||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
], "boat": [
|
||||
"GrishaGroupGenerator", "MolniyaGroupGenerator"
|
||||
]
|
||||
}
|
||||
|
||||
Syria_1973 = {
|
||||
"country": "Syria",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
MiG_21Bis,
|
||||
MiG_19P,
|
||||
MiG_15bis, # Standing as Mig-17
|
||||
|
||||
Su_17M4, # Standing as Su-7
|
||||
Mi_8MT,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
|
||||
AirDefence.SAM_SA_6_Kub_LN_2P25,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73,
|
||||
AirDefence.SAM_SA_2_LN_SM_90,
|
||||
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.APC_MTLB,
|
||||
Armor.MBT_T_55,
|
||||
Artillery.MLRS_BM_21_Grad,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Soldier_AK
|
||||
|
||||
],
|
||||
"shorad": [
|
||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
], "boat": [
|
||||
"GrishaGroupGenerator"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Syria_1982 = {
|
||||
"country": "Syria",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
MiG_25PD,
|
||||
MiG_19P,
|
||||
|
||||
Su_17M4, # Standing as Su-7
|
||||
Mi_8MT,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
|
||||
AirDefence.SAM_SA_6_Kub_LN_2P25,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73,
|
||||
AirDefence.SAM_SA_2_LN_SM_90,
|
||||
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.APC_MTLB,
|
||||
Armor.MBT_T_55,
|
||||
Armor.MBT_T_72B,
|
||||
Artillery.MLRS_BM_21_Grad,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Soldier_AK
|
||||
|
||||
],
|
||||
"shorad": [
|
||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
], "boat": [
|
||||
"GrishaGroupGenerator"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Syria_1967 = {
|
||||
"country": "Syria",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
MiG_21Bis,
|
||||
MiG_19P,
|
||||
MiG_15bis, # Standing as Mig-17
|
||||
|
||||
Su_17M4, # Standing as Su-7
|
||||
Mi_8MT,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
|
||||
AirDefence.SAM_SA_2_LN_SM_90,
|
||||
|
||||
Armor.ARV_BRDM_2,
|
||||
Armor.MBT_T_55,
|
||||
Artillery.MLRS_BM_21_Grad,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Soldier_AK
|
||||
|
||||
],
|
||||
"shorad": [
|
||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
], "boat": [
|
||||
"GrishaGroupGenerator"
|
||||
]
|
||||
}
|
||||
|
||||
Syria_1967_WW2_Weapons = {
|
||||
"country": "Syria",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
MiG_21Bis,
|
||||
MiG_19P,
|
||||
MiG_15bis, # Standing as Mig-17
|
||||
|
||||
Su_17M4, # Standing as Su-7
|
||||
Mi_8MT,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
|
||||
AirDefence.SAM_SA_2_LN_SM_90,
|
||||
|
||||
Armor.ARV_BRDM_2,
|
||||
Armor.MBT_T_55,
|
||||
Armor.MT_Pz_Kpfw_IV_Ausf_H,
|
||||
Armor.StuG_III_Ausf__G,
|
||||
Armor.TD_Jagdpanzer_IV,
|
||||
Artillery.MLRS_BM_21_Grad,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
|
||||
Infantry.Soldier_RPG,
|
||||
Infantry.Soldier_AK
|
||||
|
||||
], "requirements": {
|
||||
"WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
|
||||
},
|
||||
"shorad": [
|
||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
], "boat": [
|
||||
"GrishaGroupGenerator"
|
||||
]
|
||||
}
|
||||
|
||||
Arab_Armies_1948 = {
|
||||
"country": "Syria",
|
||||
"side": "red",
|
||||
"units": [
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
|
||||
AirDefence.SAM_SA_2_LN_SM_90,
|
||||
|
||||
Armor.MT_M4_Sherman,
|
||||
Armor.MT_Pz_Kpfw_IV_Ausf_H,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
|
||||
], "requirements": {
|
||||
"WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
|
||||
},
|
||||
"shorad": [
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
], "boat": [
|
||||
"GrishaGroupGenerator"
|
||||
]
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Turkey_2005 = {
|
||||
"country": "Turkey",
|
||||
"side": "blue",
|
||||
"units":[
|
||||
F_16C_50,
|
||||
F_4E,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
AH_1W,
|
||||
|
||||
Armor.MBT_Leopard_2,
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.APC_Cobra,
|
||||
Armor.APC_BTR_80,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad":[
|
||||
AirDefence.AAA_ZU_23_Emplacement,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka
|
||||
], "boat":[
|
||||
"OliverHazardPerryGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
UAE_2005 = {
|
||||
"country": "United Arab Emirates",
|
||||
"side": "blue",
|
||||
"units":[
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
F_16C_50,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
AH_64D,
|
||||
|
||||
Armor.MBT_Leclerc,
|
||||
Armor.IFV_BMP_3,
|
||||
Armor.TPz_Fuchs,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
|
||||
AirDefence.Rapier_FSA_Launcher,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "boat":[
|
||||
"OliverHazardPerryGroupGenerator"
|
||||
],
|
||||
"has_jtac": True,
|
||||
"jtac_unit": WingLoong_I
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from game.data.building_data import WW2_ALLIES_BUILDINGS
|
||||
from game.data.doctrine import WWII_DOCTRINE
|
||||
|
||||
UK_1944 = {
|
||||
"country": "UK",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
P_47D_30,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
A_20G,
|
||||
B_17G,
|
||||
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
Armor.MT_M4_Sherman,
|
||||
Armor.APC_M2A1,
|
||||
Armor.CT_Cromwell_IV,
|
||||
Armor.ST_Centaur_IV,
|
||||
Armor.HIT_Churchill_VII,
|
||||
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
|
||||
LS_Samuel_Chase,
|
||||
LST_Mk_II,
|
||||
LCVP__Higgins_boat,
|
||||
|
||||
Unarmed.CCKW_353,
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
], "shorad":[
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
],"requirements":{
|
||||
"WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
|
||||
},
|
||||
"objects": WW2_ALLIES_BUILDINGS,
|
||||
"doctrine": WWII_DOCTRINE,
|
||||
"boat": ["WW2LSTGroupGenerator"],
|
||||
"boat_count": 1
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
UnitedKingdom_1990 = {
|
||||
"country": "UK",
|
||||
"side": "blue",
|
||||
"units":[
|
||||
AV8BNA, # Standing as BAE Harrier 2
|
||||
Tornado_GR4,
|
||||
F_4E,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
SA342M,
|
||||
AH_64A,
|
||||
|
||||
Armor.MBT_Challenger_II,
|
||||
Armor.IFV_MCV_80,
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
Armor.ATGM_M1045_HMMWV_TOW,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.Rapier_FSA_Launcher,
|
||||
AirDefence.SAM_Avenger_M1097, # Standing as Starstreak
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad":[
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "destroyer": [
|
||||
Oliver_Hazzard_Perry_class,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "lhanames": [
|
||||
"HMS Invincible",
|
||||
"HMS Illustrious",
|
||||
"HMS Ark Royal",
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Ukraine_2010 = {
|
||||
"country": "Ukraine",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_24M,
|
||||
Su_27,
|
||||
MiG_29S,
|
||||
L_39ZA,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
A_50,
|
||||
|
||||
Mi_8MT,
|
||||
Mi_24V,
|
||||
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73,
|
||||
AirDefence.SAM_SA_11_Buk_LN_9A310M1,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85C,
|
||||
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
Armor.IFV_BMP_3,
|
||||
Armor.IFV_BMP_2,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.MBT_T_80U,
|
||||
Armor.MBT_T_72B,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
Infantry.Soldier_AK,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Bulk_cargo_ship_Yakushev,
|
||||
Dry_cargo_ship_Ivanov,
|
||||
Tanker_Elnya_160,
|
||||
],
|
||||
"shorad":[
|
||||
AirDefence.SAM_SA_19_Tunguska_2S6,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
|
||||
AirDefence.AAA_ZU_23_on_Ural_375
|
||||
], "boat":[
|
||||
"GrishaGroupGenerator"
|
||||
]
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
US_Aggressors = {
|
||||
"country": "USAF Aggressors",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
F_15C,
|
||||
F_5E_3,
|
||||
FA_18C_hornet,
|
||||
F_16C_50,
|
||||
Su_27,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
AH_64D,
|
||||
Ka_50,
|
||||
SA342M,
|
||||
SA342L,
|
||||
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_Leopard_2,
|
||||
Armor.ATGM_M1134_Stryker,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
|
||||
Artillery.MLRS_M270,
|
||||
Artillery.SPH_M109_Paladin,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Patriot_EPP_III,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "destroyer": [
|
||||
Oliver_Hazzard_Perry_class,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "carrier_names": [
|
||||
"CVN-71 Theodore Roosevelt",
|
||||
"CVN-72 Abraham Lincoln",
|
||||
"CVN-73 George Washington",
|
||||
"CVN-74 John C. Stennis",
|
||||
], "lhanames": [
|
||||
"LHA-1 Tarawa",
|
||||
"LHA-2 Saipan",
|
||||
"LHA-3 Belleau Wood",
|
||||
"LHA-4 Nassau",
|
||||
"LHA-5 Peleliu"
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
|
||||
]
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from game.data.building_data import WW2_ALLIES_BUILDINGS
|
||||
from game.data.doctrine import WWII_DOCTRINE
|
||||
|
||||
USA_1944 = {
|
||||
"country": "USA",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
P_47D_30,
|
||||
A_20G,
|
||||
B_17G,
|
||||
|
||||
Armor.MT_M4_Sherman,
|
||||
Armor.M30_Cargo_Carrier,
|
||||
Armor.APC_M2A1,
|
||||
Armor.LAC_M8_Greyhound,
|
||||
Armor.TD_M10_GMC,
|
||||
Artillery.M12_GMC,
|
||||
|
||||
Infantry.Infantry_M1_Garand,
|
||||
|
||||
LS_Samuel_Chase,
|
||||
LST_Mk_II,
|
||||
LCVP__Higgins_boat,
|
||||
|
||||
Unarmed.CCKW_353,
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
], "shorad":[
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
],"requirements":{
|
||||
"WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
|
||||
},
|
||||
"objects": WW2_ALLIES_BUILDINGS,
|
||||
"doctrine": WWII_DOCTRINE,
|
||||
"boat": ["WW2LSTGroupGenerator"],
|
||||
"boat_count": 2
|
||||
}
|
||||
|
||||
ALLIES_1944 = {
|
||||
"country": "USA",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
P_47D_30,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
A_20G,
|
||||
B_17G,
|
||||
|
||||
Armor.MT_M4_Sherman,
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
Armor.CT_Cromwell_IV,
|
||||
Armor.M30_Cargo_Carrier,
|
||||
Armor.APC_M2A1,
|
||||
Armor.CT_Cromwell_IV,
|
||||
Armor.ST_Centaur_IV,
|
||||
Armor.HIT_Churchill_VII,
|
||||
Armor.LAC_M8_Greyhound,
|
||||
Armor.TD_M10_GMC,
|
||||
Artillery.M12_GMC,
|
||||
|
||||
Infantry.Infantry_M1_Garand,
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
|
||||
LS_Samuel_Chase,
|
||||
LST_Mk_II,
|
||||
LCVP__Higgins_boat,
|
||||
|
||||
Unarmed.CCKW_353,
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
], "shorad":[
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
],"requirements":{
|
||||
"WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
|
||||
},
|
||||
"objects": WW2_ALLIES_BUILDINGS,
|
||||
"doctrine": WWII_DOCTRINE,
|
||||
"boat": ["WW2LSTGroupGenerator"],
|
||||
"boat_count": 2
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
from dcs.vehicles import *
|
||||
from dcs.ships import *
|
||||
from dcs.planes import *
|
||||
from dcs.helicopters import *
|
||||
|
||||
USA_1955 = {
|
||||
"country": "USA",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
F_86F_Sabre,
|
||||
P_51D,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
Armor.MT_M4_Sherman,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.APC_M2A1,
|
||||
Armor.M30_Cargo_Carrier,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
]
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
from dcs.vehicles import *
|
||||
from dcs.ships import *
|
||||
from dcs.planes import *
|
||||
from dcs.helicopters import *
|
||||
|
||||
USA_1960 = {
|
||||
"country": "USA",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
F_86F_Sabre,
|
||||
P_51D,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.APC_M113,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
],
|
||||
"shorad":[
|
||||
AirDefence.AAA_Vulcan_M163
|
||||
]
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
USA_1965 = {
|
||||
"country": "USA",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
|
||||
F_5E_3,
|
||||
F_4E,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
B_52H,
|
||||
|
||||
UH_1H,
|
||||
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.APC_M113,
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
],
|
||||
"shorad":[
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
AirDefence.SAM_Chaparral_M48
|
||||
], "boat":[
|
||||
]
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
USA_1990 = {
|
||||
"country": "USA",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
F_15C,
|
||||
F_15E,
|
||||
F_14B,
|
||||
FA_18C_hornet,
|
||||
F_16C_50,
|
||||
|
||||
A_10A,
|
||||
AV8BNA,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
AH_64A,
|
||||
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
Armor.ATGM_M1045_HMMWV_TOW,
|
||||
Armor.ATGM_M1134_Stryker,
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad":[
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "destroyer": [
|
||||
Oliver_Hazzard_Perry_class,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "carrier_names": [
|
||||
"CVN-72 Abraham Lincoln",
|
||||
"CVN-73 Georges Washington",
|
||||
"CVN-74 John C. Stennis",
|
||||
], "lhanames": [
|
||||
"LHA-1 Tarawa",
|
||||
"LHA-2 Saipan",
|
||||
"LHA-3 Belleau Wood",
|
||||
"LHA-4 Nassau",
|
||||
"LHA-5 Peleliu"
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
USA_2005 = {
|
||||
"country": "USA",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
F_15C,
|
||||
F_15E,
|
||||
F_14B,
|
||||
FA_18C_hornet,
|
||||
F_16C_50,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
MQ_9_Reaper,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
AH_64D,
|
||||
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.ATGM_M1134_Stryker,
|
||||
Armor.APC_M1126_Stryker_ICV,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
Armor.ATGM_M1045_HMMWV_TOW,
|
||||
|
||||
Artillery.MLRS_M270,
|
||||
Artillery.SPH_M109_Paladin,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Patriot_EPP_III,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "destroyer": [
|
||||
USS_Arleigh_Burke_IIa,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "carrier_names": [
|
||||
"CVN-71 Theodore Roosevelt",
|
||||
"CVN-72 Abraham Lincoln",
|
||||
"CVN-73 George Washington",
|
||||
"CVN-74 John C. Stennis",
|
||||
], "lhanames": [
|
||||
"LHA-1 Tarawa",
|
||||
"LHA-2 Saipan",
|
||||
"LHA-3 Belleau Wood",
|
||||
"LHA-4 Nassau",
|
||||
"LHA-5 Peleliu"
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
491
game/game.py
491
game/game.py
@@ -1,11 +1,41 @@
|
||||
from datetime import datetime, timedelta
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from game.db import REWARDS, PLAYER_BUDGET_BASE, sys
|
||||
from dcs.action import Coalition
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from game import db
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
from game.models.game_stats import GameStats
|
||||
from gen.flights.ai_flight_planner import FlightPlanner
|
||||
from game.plugins import LuaPluginManager
|
||||
from gen.ato import AirTaskingOrder
|
||||
from gen.conflictgen import Conflict
|
||||
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import FlightType
|
||||
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
||||
from .event import *
|
||||
from . import persistency
|
||||
from .debriefing import Debriefing
|
||||
from .event.event import Event, UnitsDeliveryEvent
|
||||
from .event.frontlineattack import FrontlineAttackEvent
|
||||
from .factions.faction import Faction
|
||||
from .income import Income
|
||||
from .infos.information import Information
|
||||
from .navmesh import NavMesh
|
||||
from .procurement import ProcurementAi
|
||||
from .settings import Settings
|
||||
from .theater import ConflictTheater, ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import MissileSiteGroundObject
|
||||
from .threatzones import ThreatZones
|
||||
from .unitmap import UnitMap
|
||||
from .weather import Conditions, TimeOfDay
|
||||
|
||||
COMMISION_UNIT_VARIETY = 4
|
||||
COMMISION_LIMITS_SCALE = 1.5
|
||||
@@ -37,49 +67,88 @@ ENEMY_BASE_STRENGTH_RECOVERY = 0.05
|
||||
# cost of AWACS for single operation
|
||||
AWACS_BUDGET_COST = 4
|
||||
|
||||
# Initial budget value
|
||||
PLAYER_BUDGET_INITIAL = 650
|
||||
|
||||
# Bonus multiplier logarithm base
|
||||
PLAYER_BUDGET_IMPORTANCE_LOG = 2
|
||||
|
||||
|
||||
class TurnState(Enum):
|
||||
WIN = 0
|
||||
LOSS = 1
|
||||
CONTINUE = 2
|
||||
|
||||
|
||||
class Game:
|
||||
settings = None # type: Settings
|
||||
budget = PLAYER_BUDGET_INITIAL
|
||||
events = None # type: typing.List[Event]
|
||||
pending_transfers = None # type: typing.Dict[]
|
||||
ignored_cps = None # type: typing.Collection[ControlPoint]
|
||||
turn = 0
|
||||
game_stats: GameStats = None
|
||||
|
||||
current_unit_id = 0
|
||||
current_group_id = 0
|
||||
|
||||
def __init__(self, player_name: str, enemy_name: str, theater: ConflictTheater, start_date: datetime, settings):
|
||||
def __init__(self, player_name: str, enemy_name: str,
|
||||
theater: ConflictTheater, start_date: datetime,
|
||||
settings: Settings, player_budget: float,
|
||||
enemy_budget: float) -> None:
|
||||
self.settings = settings
|
||||
self.events = []
|
||||
self.events: List[Event] = []
|
||||
self.theater = theater
|
||||
self.player_name = player_name
|
||||
self.player_country = db.FACTIONS[player_name]["country"]
|
||||
self.player_country = db.FACTIONS[player_name].country
|
||||
self.enemy_name = enemy_name
|
||||
self.enemy_country = db.FACTIONS[enemy_name]["country"]
|
||||
self.enemy_country = db.FACTIONS[enemy_name].country
|
||||
self.turn = 0
|
||||
self.date = datetime(start_date.year, start_date.month, start_date.day)
|
||||
self.date = date(start_date.year, start_date.month, start_date.day)
|
||||
self.game_stats = GameStats()
|
||||
self.game_stats.update(self)
|
||||
self.planners = {}
|
||||
self.ground_planners = {}
|
||||
self.ground_planners: Dict[int, GroundPlanner] = {}
|
||||
self.informations = []
|
||||
self.informations.append(Information("Game Start", "-" * 40, 0))
|
||||
self.__culling_points = self.compute_conflicts_position()
|
||||
self.__frontlineData = []
|
||||
self.__destroyed_units = []
|
||||
self.jtacs = []
|
||||
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
|
||||
self.__culling_zones: List[Point] = []
|
||||
# Culling Points are for individual theater ground objects that we don't wish to cull.
|
||||
self.__culling_points: List[Point] = []
|
||||
self.__destroyed_units: List[str] = []
|
||||
self.savepath = ""
|
||||
self.budget = player_budget
|
||||
self.enemy_budget = enemy_budget
|
||||
self.current_unit_id = 0
|
||||
self.current_group_id = 0
|
||||
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
self.blue_ato = AirTaskingOrder()
|
||||
self.red_ato = AirTaskingOrder()
|
||||
|
||||
self.aircraft_inventory = GlobalAircraftInventory(
|
||||
self.theater.controlpoints
|
||||
)
|
||||
|
||||
self.sanitize_sides()
|
||||
|
||||
self.on_load()
|
||||
|
||||
# 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()
|
||||
|
||||
red_planner = CoalitionMissionPlanner(self, is_player=False)
|
||||
red_planner.plan_missions()
|
||||
|
||||
self.plan_procurement(blue_planner, red_planner)
|
||||
|
||||
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"]
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: Dict[str, Any]) -> None:
|
||||
self.__dict__.update(state)
|
||||
# Regenerate any state that was not persisted.
|
||||
self.on_load()
|
||||
|
||||
def generate_conditions(self) -> Conditions:
|
||||
return Conditions.generate(self.theater, self.date,
|
||||
self.current_turn_time_of_day, self.settings)
|
||||
|
||||
def sanitize_sides(self):
|
||||
"""
|
||||
@@ -95,13 +164,18 @@ class Game:
|
||||
self.enemy_country = "Russia"
|
||||
|
||||
@property
|
||||
def player_faction(self):
|
||||
def player_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.player_name]
|
||||
|
||||
@property
|
||||
def enemy_faction(self):
|
||||
def enemy_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.enemy_name]
|
||||
|
||||
def faction_for(self, player: bool) -> Faction:
|
||||
if player:
|
||||
return self.player_faction
|
||||
return self.enemy_faction
|
||||
|
||||
def _roll(self, prob, mult):
|
||||
if self.settings.version == "dev":
|
||||
# always generate all events for dev
|
||||
@@ -113,75 +187,34 @@ class Game:
|
||||
self.events.append(event_class(self, player_cp, enemy_cp, enemy_cp.position, self.player_name, self.enemy_name))
|
||||
|
||||
def _generate_events(self):
|
||||
for player_cp, enemy_cp in self.theater.conflicts(True):
|
||||
self._generate_player_event(FrontlineAttackEvent, player_cp, enemy_cp)
|
||||
for front_line in self.theater.conflicts(True):
|
||||
self._generate_player_event(FrontlineAttackEvent,
|
||||
front_line.control_point_a,
|
||||
front_line.control_point_b)
|
||||
|
||||
def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> typing.Collection[UnitType]:
|
||||
importance_factor = (cp.importance - IMPORTANCE_LOW) / (IMPORTANCE_HIGH - IMPORTANCE_LOW)
|
||||
|
||||
if for_task == AirDefence and not self.settings.sams:
|
||||
return [x for x in db.find_unittype(AirDefence, self.enemy_name) if x not in db.SAM_BAN]
|
||||
def adjust_budget(self, amount: float, player: bool) -> None:
|
||||
if player:
|
||||
self.budget += amount
|
||||
else:
|
||||
return db.choose_units(for_task, importance_factor, COMMISION_UNIT_VARIETY, self.enemy_name)
|
||||
self.enemy_budget += amount
|
||||
|
||||
def _commision_units(self, cp: ControlPoint):
|
||||
for for_task in [CAS, CAP, AirDefence]:
|
||||
limit = COMMISION_LIMITS_FACTORS[for_task] * math.pow(cp.importance,
|
||||
COMMISION_LIMITS_SCALE) * self.settings.multiplier
|
||||
missing_units = limit - cp.base.total_units(for_task)
|
||||
if missing_units > 0:
|
||||
awarded_points = COMMISION_AMOUNTS_FACTORS[for_task] * math.pow(cp.importance,
|
||||
COMMISION_AMOUNTS_SCALE) * self.settings.multiplier
|
||||
points_to_spend = cp.base.append_commision_points(for_task, awarded_points)
|
||||
if points_to_spend > 0:
|
||||
unittypes = self.commision_unit_types(cp, for_task)
|
||||
if len(unittypes) > 0:
|
||||
d = {random.choice(unittypes): points_to_spend}
|
||||
logging.info("Commision {}: {}".format(cp, d))
|
||||
cp.base.commision_units(d)
|
||||
def process_player_income(self):
|
||||
self.budget += Income(self, player=True).total
|
||||
|
||||
@property
|
||||
def budget_reward_amount(self):
|
||||
reward = 0
|
||||
if len(self.theater.player_points()) > 0:
|
||||
reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
|
||||
for cp in self.theater.player_points():
|
||||
for g in cp.ground_objects:
|
||||
if g.category in REWARDS.keys():
|
||||
reward = reward + REWARDS[g.category]
|
||||
return reward
|
||||
else:
|
||||
return reward
|
||||
def process_enemy_income(self):
|
||||
# TODO: Clean up save compat.
|
||||
if not hasattr(self, "enemy_budget"):
|
||||
self.enemy_budget = 0
|
||||
self.enemy_budget += Income(self, player=False).total
|
||||
|
||||
def _budget_player(self):
|
||||
self.budget += self.budget_reward_amount
|
||||
|
||||
def awacs_expense_commit(self):
|
||||
self.budget -= AWACS_BUDGET_COST
|
||||
|
||||
def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent:
|
||||
event = UnitsDeliveryEvent(attacker_name=self.player_name,
|
||||
defender_name=self.player_name,
|
||||
from_cp=to_cp,
|
||||
to_cp=to_cp,
|
||||
game=self)
|
||||
self.events.append(event)
|
||||
return event
|
||||
|
||||
def units_delivery_remove(self, event: Event):
|
||||
if event in self.events:
|
||||
self.events.remove(event)
|
||||
|
||||
def initiate_event(self, event: Event):
|
||||
def initiate_event(self, event: Event) -> UnitMap:
|
||||
#assert event in self.events
|
||||
logging.info("Generating {} (regular)".format(event))
|
||||
event.generate()
|
||||
return event.generate()
|
||||
|
||||
def finish_event(self, event: Event, debriefing: Debriefing):
|
||||
logging.info("Finishing event {}".format(event))
|
||||
event.commit(debriefing)
|
||||
if event.is_successfull(debriefing):
|
||||
self.budget += event.bonus()
|
||||
|
||||
if event in self.events:
|
||||
self.events.remove(event)
|
||||
@@ -192,24 +225,25 @@ class Game:
|
||||
if isinstance(event, Event):
|
||||
return event and event.attacker_name and event.attacker_name == self.player_name
|
||||
else:
|
||||
return event and event.name and event.name == self.player_name
|
||||
raise RuntimeError(f"{event} was passed when an Event type was expected")
|
||||
|
||||
def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint] = None):
|
||||
def on_load(self) -> None:
|
||||
LuaPluginManager.load_settings(self.settings)
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
self.compute_conflicts_position()
|
||||
self.compute_threat_zones()
|
||||
|
||||
def pass_turn(self, no_action: bool = False) -> None:
|
||||
logging.info("Pass turn")
|
||||
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
|
||||
self.turn = self.turn + 1
|
||||
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()
|
||||
for control_point in self.theater.controlpoints:
|
||||
control_point.process_turn(self)
|
||||
|
||||
self._enemy_reinforcement()
|
||||
self._budget_player()
|
||||
self.process_enemy_income()
|
||||
|
||||
self.process_player_income()
|
||||
|
||||
if not no_action and self.turn > 1:
|
||||
for cp in self.theater.player_points():
|
||||
@@ -219,111 +253,95 @@ class Game:
|
||||
if not cp.is_carrier and not cp.is_lha:
|
||||
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)
|
||||
|
||||
self.ignored_cps = []
|
||||
if ignored_cps:
|
||||
self.ignored_cps = ignored_cps
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
self.events = [] # type: typing.List[Event]
|
||||
self.initialize_turn()
|
||||
|
||||
# Autosave progress
|
||||
persistency.autosave(self)
|
||||
|
||||
def check_win_loss(self):
|
||||
captured_states = {i.captured for i in self.theater.controlpoints}
|
||||
if True not in captured_states:
|
||||
return TurnState.LOSS
|
||||
if False not in captured_states:
|
||||
return TurnState.WIN
|
||||
return TurnState.CONTINUE
|
||||
|
||||
def initialize_turn(self) -> None:
|
||||
self.events = []
|
||||
self._generate_events()
|
||||
|
||||
# Update statistics
|
||||
self.game_stats.update(self)
|
||||
|
||||
# Plan flights & combat for next turn
|
||||
self.__culling_points = self.compute_conflicts_position()
|
||||
self.planners = {}
|
||||
self.ground_planners = {}
|
||||
self.aircraft_inventory.reset()
|
||||
for cp in self.theater.controlpoints:
|
||||
if cp.has_runway():
|
||||
planner = FlightPlanner(cp, self)
|
||||
planner.plan_flights()
|
||||
self.planners[cp.id] = planner
|
||||
self.aircraft_inventory.set_from_control_point(cp)
|
||||
|
||||
# Check for win or loss condition
|
||||
turn_state = self.check_win_loss()
|
||||
if turn_state in (TurnState.LOSS,TurnState.WIN):
|
||||
return self.process_win_loss(turn_state)
|
||||
|
||||
# Plan flights & combat for next turn
|
||||
self.compute_conflicts_position()
|
||||
self.compute_threat_zones()
|
||||
self.ground_planners = {}
|
||||
self.blue_ato.clear()
|
||||
self.red_ato.clear()
|
||||
|
||||
blue_planner = CoalitionMissionPlanner(self, is_player=True)
|
||||
blue_planner.plan_missions()
|
||||
|
||||
red_planner = CoalitionMissionPlanner(self, is_player=False)
|
||||
red_planner.plan_missions()
|
||||
|
||||
for cp in self.theater.controlpoints:
|
||||
if cp.has_frontline:
|
||||
gplanner = GroundPlanner(cp, self)
|
||||
gplanner.plan_groundwar()
|
||||
self.ground_planners[cp.id] = gplanner
|
||||
|
||||
# Autosave progress
|
||||
persistency.autosave(self)
|
||||
self.plan_procurement(blue_planner, red_planner)
|
||||
|
||||
def _enemy_reinforcement(self):
|
||||
"""
|
||||
Compute and commision reinforcement for enemy bases
|
||||
"""
|
||||
def plan_procurement(self, blue_planner: CoalitionMissionPlanner,
|
||||
red_planner: CoalitionMissionPlanner) -> 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.
|
||||
ground_portion = 0.3 if self.turn == 0 else 0.5
|
||||
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,
|
||||
front_line_budget_share=ground_portion
|
||||
).spend_budget(self.budget, blue_planner.procurement_requests)
|
||||
|
||||
MAX_ARMOR = 30 * self.settings.multiplier
|
||||
MAX_AIRCRAFT = 25 * self.settings.multiplier
|
||||
self.enemy_budget = ProcurementAi(
|
||||
self,
|
||||
for_player=False,
|
||||
faction=self.enemy_faction,
|
||||
manage_runways=True,
|
||||
manage_front_line=True,
|
||||
manage_aircraft=True,
|
||||
front_line_budget_share=ground_portion
|
||||
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
|
||||
|
||||
production = 0.0
|
||||
for enemy_point in self.theater.enemy_points():
|
||||
for g in enemy_point.ground_objects:
|
||||
if g.category in REWARDS.keys():
|
||||
production = production + REWARDS[g.category]
|
||||
|
||||
production = production * 0.75
|
||||
budget_for_armored_units = production / 2
|
||||
budget_for_aircraft = production / 2
|
||||
|
||||
potential_cp_armor = []
|
||||
for cp in self.theater.enemy_points():
|
||||
for cpe in cp.connected_points:
|
||||
if cpe.captured and cp.base.total_armor < MAX_ARMOR:
|
||||
potential_cp_armor.append(cp)
|
||||
if len(potential_cp_armor) == 0:
|
||||
potential_cp_armor = self.theater.enemy_points()
|
||||
|
||||
i = 0
|
||||
potential_units = [u for u in db.FACTIONS[self.enemy_name]["units"] if u in db.UNIT_BY_TASK[PinpointStrike]]
|
||||
|
||||
print("Enemy Recruiting")
|
||||
print(potential_cp_armor)
|
||||
print(budget_for_armored_units)
|
||||
print(potential_units)
|
||||
|
||||
if len(potential_units) > 0 and len(potential_cp_armor) > 0:
|
||||
while budget_for_armored_units > 0:
|
||||
i = i + 1
|
||||
if i > 50 or budget_for_armored_units <= 0:
|
||||
break
|
||||
target_cp = random.choice(potential_cp_armor)
|
||||
if target_cp.base.total_armor >= MAX_ARMOR:
|
||||
continue
|
||||
unit = random.choice(potential_units)
|
||||
price = db.PRICES[unit] * 2
|
||||
budget_for_armored_units -= price * 2
|
||||
target_cp.base.armor[unit] = target_cp.base.armor.get(unit, 0) + 2
|
||||
info = Information("Enemy Reinforcement", unit.id + " x 2 at " + target_cp.name, self.turn)
|
||||
print(str(info))
|
||||
self.informations.append(info)
|
||||
|
||||
if budget_for_armored_units > 0:
|
||||
budget_for_aircraft += budget_for_armored_units
|
||||
|
||||
potential_units = [u for u in db.FACTIONS[self.enemy_name]["units"] if
|
||||
u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
|
||||
if len(potential_units) > 0 and len(potential_cp_armor) > 0:
|
||||
while budget_for_aircraft > 0:
|
||||
i = i + 1
|
||||
if i > 50 or budget_for_aircraft <= 0:
|
||||
break
|
||||
target_cp = random.choice(potential_cp_armor)
|
||||
if target_cp.base.total_planes >= MAX_AIRCRAFT:
|
||||
continue
|
||||
unit = random.choice(potential_units)
|
||||
price = db.PRICES[unit] * 2
|
||||
budget_for_aircraft -= price * 2
|
||||
target_cp.base.aircraft[unit] = target_cp.base.aircraft.get(unit, 0) + 2
|
||||
info = Information("Enemy Reinforcement", unit.id + " x 2 at " + target_cp.name, self.turn)
|
||||
print(str(info))
|
||||
self.informations.append(info)
|
||||
def message(self, text: str) -> None:
|
||||
self.informations.append(Information(text, turn=self.turn))
|
||||
|
||||
@property
|
||||
def current_turn_daytime(self):
|
||||
return ["dawn", "day", "dusk", "night"][self.turn % 4]
|
||||
def current_turn_time_of_day(self) -> TimeOfDay:
|
||||
return list(TimeOfDay)[self.turn % 4]
|
||||
|
||||
@property
|
||||
def current_day(self):
|
||||
def current_day(self) -> date:
|
||||
return self.date + timedelta(days=self.turn // 4)
|
||||
|
||||
def next_unit_id(self):
|
||||
@@ -340,21 +358,54 @@ class Game:
|
||||
self.current_group_id += 1
|
||||
return self.current_group_id
|
||||
|
||||
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
|
||||
"""
|
||||
zones = []
|
||||
points = []
|
||||
|
||||
# By default, use the existing frontline conflict position
|
||||
for conflict in self.theater.conflicts():
|
||||
points.append(Conflict.frontline_position(self.theater, conflict[0], conflict[1])[0])
|
||||
points.append(conflict[0].position)
|
||||
points.append(conflict[1].position)
|
||||
for front_line in self.theater.conflicts():
|
||||
position = Conflict.frontline_position(front_line.control_point_a,
|
||||
front_line.control_point_b,
|
||||
self.theater)
|
||||
zones.append(position[0])
|
||||
zones.append(front_line.control_point_a.position)
|
||||
zones.append(front_line.control_point_b.position)
|
||||
|
||||
for cp in self.theater.controlpoints:
|
||||
# Don't cull missile sites - their range is long enough to make them
|
||||
# easily culled despite being a threat.
|
||||
for tgo in cp.ground_objects:
|
||||
if isinstance(tgo, MissileSiteGroundObject):
|
||||
points.append(tgo.position)
|
||||
# If do_not_cull_carrier is enabled, add carriers as culling point
|
||||
if self.settings.perf_do_not_cull_carrier:
|
||||
if cp.is_carrier or cp.is_lha:
|
||||
zones.append(cp.position)
|
||||
|
||||
# If there is no conflict take the center point between the two nearest opposing bases
|
||||
if len(points) == 0:
|
||||
if len(zones) == 0:
|
||||
cpoint = None
|
||||
min_distance = sys.maxsize
|
||||
for cp in self.theater.player_points():
|
||||
@@ -363,20 +414,33 @@ class Game:
|
||||
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)
|
||||
zones.append(cp.position)
|
||||
zones.append(cp2.position)
|
||||
break
|
||||
if cpoint is not None:
|
||||
break
|
||||
if cpoint is not None:
|
||||
points.append(cpoint)
|
||||
zones.append(cpoint)
|
||||
|
||||
packages = itertools.chain(self.blue_ato.packages,
|
||||
self.red_ato.packages)
|
||||
for package in packages:
|
||||
if package.primary_task is FlightType.BARCAP:
|
||||
# BARCAPs will be planned at most locations on smaller theaters,
|
||||
# rendering culling fairly useless. BARCAP packages don't really
|
||||
# need the ground detail since they're defensive. SAMs nearby
|
||||
# are only interesting if there are enemies in the area, and if
|
||||
# there are they won't be culled because of the enemy's mission.
|
||||
continue
|
||||
zones.append(package.target.position)
|
||||
|
||||
# Else 0,0, since we need a default value
|
||||
# (in this case this means the whole map is owned by the same player, so it is not an issue)
|
||||
if len(points) == 0:
|
||||
points.append(Point(0, 0))
|
||||
if len(zones) == 0:
|
||||
zones.append(Point(0, 0))
|
||||
|
||||
return points
|
||||
self.__culling_zones = zones
|
||||
self.__culling_points = points
|
||||
|
||||
def add_destroyed_units(self, data):
|
||||
pos = Point(data["x"], data["z"])
|
||||
@@ -395,11 +459,28 @@ class Game:
|
||||
if self.settings.perf_culling == False:
|
||||
return False
|
||||
else:
|
||||
for c in self.__culling_points:
|
||||
if c.distance_to_point(pos) < self.settings.perf_culling_distance * 1000:
|
||||
for z in self.__culling_zones:
|
||||
if z.distance_to_point(pos) < self.settings.perf_culling_distance * 1000:
|
||||
return False
|
||||
for p in self.__culling_points:
|
||||
if p.distance_to_point(pos) < 2500:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_culling_zones(self):
|
||||
"""
|
||||
Check culling points
|
||||
:return: List of culling zones
|
||||
"""
|
||||
return self.__culling_zones
|
||||
|
||||
def get_culling_points(self):
|
||||
"""
|
||||
Check culling points
|
||||
:return: List of culling points
|
||||
"""
|
||||
return self.__culling_points
|
||||
|
||||
# 1 = red, 2 = blue
|
||||
def get_player_coalition_id(self):
|
||||
return 2
|
||||
@@ -408,13 +489,19 @@ class Game:
|
||||
return 1
|
||||
|
||||
def get_player_coalition(self):
|
||||
return dcs.action.Coalition.Blue
|
||||
return Coalition.Blue
|
||||
|
||||
def get_enemy_coalition(self):
|
||||
return dcs.action.Coalition.Red
|
||||
return Coalition.Red
|
||||
|
||||
def get_player_color(self):
|
||||
return "blue"
|
||||
|
||||
def get_enemy_color(self):
|
||||
return "red"
|
||||
return "red"
|
||||
|
||||
def process_win_loss(self, turn_state: TurnState):
|
||||
if turn_state is TurnState.WIN:
|
||||
return self.message("Congratulations, you are victorious! Start a new campaign to continue.")
|
||||
elif turn_state is TurnState.LOSS:
|
||||
return self.message("Game Over, you lose. Start a new campaign to continue.")
|
||||
|
||||
55
game/income.py
Normal file
55
game/income.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.db import REWARDS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BuildingIncome:
|
||||
name: str
|
||||
category: str
|
||||
number: int
|
||||
income_per_building: int
|
||||
|
||||
@property
|
||||
def income(self) -> int:
|
||||
return self.number * self.income_per_building
|
||||
|
||||
|
||||
class Income:
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
if player:
|
||||
self.multiplier = game.settings.player_income_multiplier
|
||||
else:
|
||||
self.multiplier = game.settings.enemy_income_multiplier
|
||||
self.control_points = []
|
||||
self.buildings = []
|
||||
|
||||
names = set()
|
||||
for cp in game.theater.control_points_for(player):
|
||||
if cp.income_per_turn:
|
||||
self.control_points.append(cp)
|
||||
for tgo in cp.ground_objects:
|
||||
names.add(tgo.obj_name)
|
||||
|
||||
for name in names:
|
||||
count = 0
|
||||
tgos = game.theater.find_ground_objects_by_obj_name(name)
|
||||
category = tgos[0].category
|
||||
if category not in REWARDS:
|
||||
continue
|
||||
for tgo in tgos:
|
||||
if not tgo.is_dead:
|
||||
count += 1
|
||||
self.buildings.append(BuildingIncome(name, category, count,
|
||||
REWARDS[category]))
|
||||
|
||||
self.from_bases = sum(cp.income_per_turn for cp in self.control_points)
|
||||
self.total_buildings = sum(b.income for b in self.buildings)
|
||||
self.total = ((self.total_buildings + self.from_bases) *
|
||||
self.multiplier)
|
||||
@@ -1,3 +1,4 @@
|
||||
import datetime
|
||||
|
||||
class Information():
|
||||
|
||||
@@ -5,7 +6,12 @@ class Information():
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.turn = turn
|
||||
self.timestamp = datetime.datetime.now()
|
||||
|
||||
def __str__(self):
|
||||
s = "[" + str(self.turn) + "] " + self.title + "\n" + self.text
|
||||
return s
|
||||
return '[{}][{}] {} {}'.format(
|
||||
self.timestamp.strftime("%Y-%m-%d %H:%M:%S") if self.timestamp is not None else '',
|
||||
self.turn,
|
||||
self.title,
|
||||
self.text
|
||||
)
|
||||
129
game/inventory.py
Normal file
129
game/inventory.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Inventory management APIs."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ControlPoint
|
||||
|
||||
|
||||
class ControlPointAircraftInventory:
|
||||
"""Aircraft inventory for a single control point."""
|
||||
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
self.control_point = control_point
|
||||
self.inventory: Dict[Type[FlyingType], int] = defaultdict(int)
|
||||
|
||||
def add_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
|
||||
"""Adds aircraft to the inventory.
|
||||
|
||||
Args:
|
||||
aircraft: The type of aircraft to add.
|
||||
count: The number of aircraft to add.
|
||||
"""
|
||||
self.inventory[aircraft] += count
|
||||
|
||||
def remove_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
|
||||
"""Removes aircraft from the inventory.
|
||||
|
||||
Args:
|
||||
aircraft: The type of aircraft to remove.
|
||||
count: The number of aircraft to remove.
|
||||
|
||||
Raises:
|
||||
ValueError: The control point cannot fulfill the requested number of
|
||||
aircraft.
|
||||
"""
|
||||
available = self.inventory[aircraft]
|
||||
if available < count:
|
||||
raise ValueError(
|
||||
f"Cannot remove {count} {aircraft.id} from "
|
||||
f"{self.control_point.name}. Only have {available}."
|
||||
)
|
||||
self.inventory[aircraft] -= count
|
||||
|
||||
def available(self, aircraft: Type[FlyingType]) -> int:
|
||||
"""Returns the number of available aircraft of the given type.
|
||||
|
||||
Args:
|
||||
aircraft: The type of aircraft to query.
|
||||
"""
|
||||
try:
|
||||
return self.inventory[aircraft]
|
||||
except KeyError:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def types_available(self) -> Iterator[Type[FlyingType]]:
|
||||
"""Iterates over all available aircraft types."""
|
||||
for aircraft, count in self.inventory.items():
|
||||
if count > 0:
|
||||
yield aircraft
|
||||
|
||||
@property
|
||||
def all_aircraft(self) -> Iterator[Tuple[Type[FlyingType], int]]:
|
||||
"""Iterates over all available aircraft types, including amounts."""
|
||||
for aircraft, count in self.inventory.items():
|
||||
if count > 0:
|
||||
yield aircraft, count
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clears all aircraft from the inventory."""
|
||||
self.inventory.clear()
|
||||
|
||||
|
||||
class GlobalAircraftInventory:
|
||||
"""Game-wide aircraft inventory."""
|
||||
def __init__(self, control_points: Iterable[ControlPoint]) -> None:
|
||||
self.inventories: Dict[ControlPoint, ControlPointAircraftInventory] = {
|
||||
cp: ControlPointAircraftInventory(cp) for cp in control_points
|
||||
}
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Clears all control points and their inventories."""
|
||||
for inventory in self.inventories.values():
|
||||
inventory.clear()
|
||||
|
||||
def set_from_control_point(self, control_point: ControlPoint) -> None:
|
||||
"""Set the control point's aircraft inventory.
|
||||
|
||||
If the inventory for the given control point has already been set for
|
||||
the turn, it will be overwritten.
|
||||
"""
|
||||
inventory = self.inventories[control_point]
|
||||
for aircraft, count in control_point.base.aircraft.items():
|
||||
inventory.add_aircraft(aircraft, count)
|
||||
|
||||
def for_control_point(
|
||||
self,
|
||||
control_point: ControlPoint) -> ControlPointAircraftInventory:
|
||||
"""Returns the inventory specific to the given control point."""
|
||||
return self.inventories[control_point]
|
||||
|
||||
@property
|
||||
def available_types_for_player(self) -> Iterator[Type[FlyingType]]:
|
||||
"""Iterates over all aircraft types available to the player."""
|
||||
seen: Set[Type[FlyingType]] = set()
|
||||
for control_point, inventory in self.inventories.items():
|
||||
if control_point.captured:
|
||||
for aircraft in inventory.types_available:
|
||||
if not control_point.can_operate(aircraft):
|
||||
continue
|
||||
if aircraft not in seen:
|
||||
seen.add(aircraft)
|
||||
yield aircraft
|
||||
|
||||
def claim_for_flight(self, flight: Flight) -> None:
|
||||
"""Removes aircraft from the inventory for the given flight."""
|
||||
inventory = self.for_control_point(flight.from_cp)
|
||||
inventory.remove_aircraft(flight.unit_type, flight.count)
|
||||
|
||||
def return_from_flight(self, flight: Flight) -> None:
|
||||
"""Returns a flight's aircraft to the inventory."""
|
||||
inventory = self.for_control_point(flight.from_cp)
|
||||
inventory.add_aircraft(flight.unit_type, flight.count)
|
||||
@@ -1,4 +1,4 @@
|
||||
from theater import ControlPoint
|
||||
from game.theater import ControlPoint
|
||||
|
||||
|
||||
class FrontlineData:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import List
|
||||
|
||||
class FactionTurnMetadata:
|
||||
"""
|
||||
Store metadata about a faction
|
||||
@@ -31,10 +33,8 @@ class GameStats:
|
||||
Store statistics for the current game
|
||||
"""
|
||||
|
||||
data_per_turn: [GameTurnMetadata] = []
|
||||
|
||||
def __init__(self):
|
||||
self.data_per_turn = []
|
||||
self.data_per_turn: List[GameTurnMetadata] = []
|
||||
|
||||
def update(self, game):
|
||||
"""
|
||||
|
||||
270
game/navmesh.py
Normal file
270
game/navmesh.py
Normal file
@@ -0,0 +1,270 @@
|
||||
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 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.contains(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 RuntimeError(
|
||||
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 ValueError(f"Origin point {origin} is outside the navmesh")
|
||||
destination_poly = self.localize(destination)
|
||||
if destination_poly is None:
|
||||
raise ValueError(
|
||||
f"Origin 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(100).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,39 +0,0 @@
|
||||
from game.db import assigned_units_split
|
||||
|
||||
from .operation import *
|
||||
|
||||
|
||||
MAX_DISTANCE_BETWEEN_GROUPS = 12000
|
||||
|
||||
|
||||
class FrontlineAttackOperation(Operation):
|
||||
interceptors = None # type: db.AssignedUnitsDict
|
||||
escort = None # type: db.AssignedUnitsDict
|
||||
strikegroup = None # type: db.AssignedUnitsDict
|
||||
|
||||
attackers = None # type: db.ArmorDict
|
||||
defenders = None # type: db.ArmorDict
|
||||
|
||||
def prepare(self, terrain: Terrain, is_quick: bool):
|
||||
super(FrontlineAttackOperation, self).prepare(terrain, is_quick)
|
||||
if self.defender_name == self.game.player_name:
|
||||
self.attackers_starting_position = None
|
||||
self.defenders_starting_position = None
|
||||
|
||||
conflict = Conflict.frontline_cas_conflict(
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
attacker=self.current_mission.country(self.attacker_country),
|
||||
defender=self.current_mission.country(self.defender_country),
|
||||
from_cp=self.from_cp,
|
||||
to_cp=self.to_cp,
|
||||
theater=self.game.theater
|
||||
)
|
||||
|
||||
self.initialize(mission=self.current_mission,
|
||||
conflict=conflict)
|
||||
|
||||
def generate(self):
|
||||
self.briefinggen.title = "Frontline CAS"
|
||||
self.briefinggen.description = "Provide CAS for the ground forces attacking enemy lines. Operation will be considered successful if total number of enemy units will be lower than your own by a factor of 1.5 (i.e. with 12 units from both sides, enemy forces need to be reduced to at least 8), meaning that you (and, probably, your wingmans) should concentrate on destroying the enemy units. Target base strength will be lowered as a result. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu."
|
||||
super(FrontlineAttackOperation, self).generate()
|
||||
@@ -1,371 +1,223 @@
|
||||
from typing import Set
|
||||
from __future__ import annotations
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
from gen import *
|
||||
from gen.airfields import AIRFIELD_DATA
|
||||
from gen.beacons import load_beacons_for_terrain
|
||||
from gen.radios import RadioRegistry
|
||||
from gen.tacan import TacanRegistry
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Iterable, List, Optional, Set
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.action import DoScript, DoScriptFile
|
||||
from dcs.coalition import Coalition
|
||||
from dcs.countries import country_dict
|
||||
from dcs.lua.parse import loads
|
||||
from dcs.terrain.terrain import Terrain
|
||||
from userdata.debriefing import *
|
||||
from dcs.mapping import Point
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import TriggerStart
|
||||
from game.plugins import LuaPluginManager
|
||||
from gen import Conflict, FlightType, VisualGenerator
|
||||
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
|
||||
from gen.airfields import AIRFIELD_DATA
|
||||
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
|
||||
from gen.armor import GroundConflictGenerator, JtacInfo
|
||||
from gen.beacons import load_beacons_for_terrain
|
||||
from gen.briefinggen import BriefingGenerator, MissionInfoGenerator
|
||||
from gen.environmentgen import EnvironmentGenerator
|
||||
from gen.forcedoptionsgen import ForcedOptionsGenerator
|
||||
from gen.groundobjectsgen import GroundObjectsGenerator
|
||||
from gen.kneeboard import KneeboardGenerator
|
||||
from gen.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 ..theater import Airfield
|
||||
from ..unitmap import UnitMap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
class Operation:
|
||||
attackers_starting_position = None # type: db.StartingPosition
|
||||
defenders_starting_position = None # type: db.StartingPosition
|
||||
|
||||
current_mission = None # type: dcs.Mission
|
||||
regular_mission = None # type: dcs.Mission
|
||||
quick_mission = None # type: dcs.Mission
|
||||
conflict = None # type: Conflict
|
||||
armorgen = None # type: ArmorConflictGenerator
|
||||
"""Static class for managing the final Mission generation"""
|
||||
current_mission = None # type: Mission
|
||||
airgen = None # type: AircraftConflictGenerator
|
||||
triggersgen = None # type: TriggersGenerator
|
||||
airsupportgen = None # type: AirSupportConflictGenerator
|
||||
visualgen = None # type: VisualGenerator
|
||||
envgen = None # type: EnvironmentGenerator
|
||||
groundobjectgen = None # type: GroundObjectsGenerator
|
||||
briefinggen = None # type: BriefingGenerator
|
||||
forcedoptionsgen = None # type: ForcedOptionsGenerator
|
||||
radio_registry: Optional[RadioRegistry] = None
|
||||
tacan_registry: Optional[TacanRegistry] = None
|
||||
|
||||
game = None # type: Game
|
||||
environment_settings = None
|
||||
trigger_radius = TRIGGER_RADIUS_MEDIUM
|
||||
is_quick = None
|
||||
is_awacs_enabled = False
|
||||
ca_slots = 0
|
||||
player_awacs_enabled = True
|
||||
# TODO: #436 Generate Air Support for red
|
||||
enemy_awacs_enabled = True
|
||||
ca_slots = 1
|
||||
unit_map: UnitMap
|
||||
jtacs: List[JtacInfo] = []
|
||||
plugin_scripts: List[str] = []
|
||||
|
||||
def __init__(self,
|
||||
game,
|
||||
attacker_name: str,
|
||||
defender_name: str,
|
||||
from_cp: ControlPoint,
|
||||
departure_cp: ControlPoint,
|
||||
to_cp: ControlPoint = None):
|
||||
self.game = game
|
||||
self.attacker_name = attacker_name
|
||||
self.attacker_country = db.FACTIONS[attacker_name]["country"]
|
||||
self.defender_name = defender_name
|
||||
self.defender_country = db.FACTIONS[defender_name]["country"]
|
||||
print(self.defender_country, self.attacker_country)
|
||||
self.from_cp = from_cp
|
||||
self.departure_cp = departure_cp
|
||||
self.to_cp = to_cp
|
||||
self.is_quick = False
|
||||
|
||||
def units_of(self, country_name: str) -> typing.Collection[UnitType]:
|
||||
return []
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_player_attack(self) -> bool:
|
||||
return self.from_cp.captured
|
||||
|
||||
def initialize(self, mission: Mission, conflict: Conflict):
|
||||
self.current_mission = mission
|
||||
self.conflict = conflict
|
||||
self.briefinggen = BriefingGenerator(self.current_mission,
|
||||
self.conflict, self.game)
|
||||
|
||||
def prepare(self, terrain: Terrain, is_quick: bool):
|
||||
@classmethod
|
||||
def prepare(cls, game: Game):
|
||||
with open("resources/default_options.lua", "r") as f:
|
||||
options_dict = loads(f.read())["options"]
|
||||
cls._set_mission(Mission(game.theater.terrain))
|
||||
cls.game = game
|
||||
cls._setup_mission_coalitions()
|
||||
cls.current_mission.options.load_from_dict(options_dict)
|
||||
|
||||
self.current_mission = dcs.Mission(terrain)
|
||||
@classmethod
|
||||
def conflicts(cls) -> Iterable[Conflict]:
|
||||
assert cls.game
|
||||
for frontline in cls.game.theater.conflicts():
|
||||
yield Conflict(
|
||||
cls.game.theater,
|
||||
frontline.control_point_a,
|
||||
frontline.control_point_b,
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
cls.game.player_country,
|
||||
cls.game.enemy_country,
|
||||
frontline.position
|
||||
)
|
||||
|
||||
print(self.game.player_country)
|
||||
print(country_dict[db.country_id_from_name(self.game.player_country)])
|
||||
print(country_dict[db.country_id_from_name(self.game.player_country)]())
|
||||
|
||||
# Setup coalition :
|
||||
self.current_mission.coalition["blue"] = Coalition("blue")
|
||||
self.current_mission.coalition["red"] = Coalition("red")
|
||||
|
||||
p_country = self.game.player_country
|
||||
e_country = self.game.enemy_country
|
||||
self.current_mission.coalition["blue"].add_country(country_dict[db.country_id_from_name(p_country)]())
|
||||
self.current_mission.coalition["red"].add_country(country_dict[db.country_id_from_name(e_country)]())
|
||||
|
||||
print([c for c in self.current_mission.coalition["blue"].countries.keys()])
|
||||
print([c for c in self.current_mission.coalition["red"].countries.keys()])
|
||||
|
||||
if is_quick:
|
||||
self.quick_mission = self.current_mission
|
||||
else:
|
||||
self.regular_mission = self.current_mission
|
||||
|
||||
self.current_mission.options.load_from_dict(options_dict)
|
||||
self.is_quick = is_quick
|
||||
|
||||
if is_quick:
|
||||
self.attackers_starting_position = None
|
||||
self.defenders_starting_position = None
|
||||
else:
|
||||
self.attackers_starting_position = self.departure_cp.at
|
||||
self.defenders_starting_position = self.to_cp.at
|
||||
|
||||
def generate(self):
|
||||
radio_registry = RadioRegistry()
|
||||
tacan_registry = TacanRegistry()
|
||||
|
||||
# Dedup beacon/radio frequencies, since some maps have some frequencies
|
||||
# used multiple times.
|
||||
beacons = load_beacons_for_terrain(self.game.theater.terrain.name)
|
||||
unique_map_frequencies: Set[RadioFrequency] = set()
|
||||
for beacon in beacons:
|
||||
unique_map_frequencies.add(beacon.frequency)
|
||||
if beacon.is_tacan:
|
||||
if beacon.channel is None:
|
||||
logging.error(
|
||||
f"TACAN beacon has no channel: {beacon.callsign}")
|
||||
else:
|
||||
tacan_registry.reserve(beacon.tacan_channel)
|
||||
|
||||
for airfield, data in AIRFIELD_DATA.items():
|
||||
if data.theater == self.game.theater.terrain.name:
|
||||
unique_map_frequencies.add(data.atc.hf)
|
||||
unique_map_frequencies.add(data.atc.vhf_fm)
|
||||
unique_map_frequencies.add(data.atc.vhf_am)
|
||||
unique_map_frequencies.add(data.atc.uhf)
|
||||
# No need to reserve ILS or TACAN because those are in the
|
||||
# beacon list.
|
||||
|
||||
for frequency in unique_map_frequencies:
|
||||
radio_registry.reserve(frequency)
|
||||
|
||||
# Generate meteo
|
||||
envgen = EnviromentGenerator(self.current_mission, self.conflict,
|
||||
self.game)
|
||||
if self.environment_settings is None:
|
||||
self.environment_settings = envgen.generate()
|
||||
else:
|
||||
envgen.load(self.environment_settings)
|
||||
|
||||
# Generate ground object first
|
||||
|
||||
groundobjectgen = GroundObjectsGenerator(
|
||||
self.current_mission,
|
||||
self.conflict,
|
||||
self.game,
|
||||
radio_registry,
|
||||
tacan_registry
|
||||
@classmethod
|
||||
def air_conflict(cls) -> Conflict:
|
||||
assert cls.game
|
||||
player_cp, enemy_cp = cls.game.theater.closest_opposing_control_points()
|
||||
mid_point = player_cp.position.point_from_heading(
|
||||
player_cp.position.heading_between_point(enemy_cp.position),
|
||||
player_cp.position.distance_to_point(enemy_cp.position) / 2
|
||||
)
|
||||
return Conflict(
|
||||
cls.game.theater,
|
||||
player_cp,
|
||||
enemy_cp,
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
cls.game.player_country,
|
||||
cls.game.enemy_country,
|
||||
mid_point
|
||||
)
|
||||
groundobjectgen.generate()
|
||||
|
||||
# Generate destroyed units
|
||||
for d in self.game.get_destroyed_units():
|
||||
try:
|
||||
utype = db.unit_type_from_name(d["type"])
|
||||
except KeyError:
|
||||
continue
|
||||
@classmethod
|
||||
def _set_mission(cls, mission: Mission) -> None:
|
||||
cls.current_mission = mission
|
||||
|
||||
pos = Point(d["x"], d["z"])
|
||||
if utype is not None and not self.game.position_culled(pos) and self.game.settings.perf_destroyed_units:
|
||||
self.current_mission.static_group(
|
||||
country=self.current_mission.country(self.game.player_country),
|
||||
name="",
|
||||
_type=utype,
|
||||
hidden=True,
|
||||
position=pos,
|
||||
heading=d["orientation"],
|
||||
dead=True,
|
||||
)
|
||||
@classmethod
|
||||
def _setup_mission_coalitions(cls):
|
||||
cls.current_mission.coalition["blue"] = Coalition("blue")
|
||||
cls.current_mission.coalition["red"] = Coalition("red")
|
||||
|
||||
# Air Support (Tanker & Awacs)
|
||||
airsupportgen = AirSupportConflictGenerator(
|
||||
self.current_mission, self.conflict, self.game, radio_registry,
|
||||
tacan_registry)
|
||||
airsupportgen.generate(self.is_awacs_enabled)
|
||||
p_country = cls.game.player_country
|
||||
e_country = cls.game.enemy_country
|
||||
cls.current_mission.coalition["blue"].add_country(
|
||||
country_dict[db.country_id_from_name(p_country)]())
|
||||
cls.current_mission.coalition["red"].add_country(
|
||||
country_dict[db.country_id_from_name(e_country)]())
|
||||
|
||||
# Generate Activity on the map
|
||||
airgen = AircraftConflictGenerator(
|
||||
self.current_mission, self.conflict, self.game.settings, self.game,
|
||||
radio_registry)
|
||||
for cp in self.game.theater.controlpoints:
|
||||
side = cp.captured
|
||||
if side:
|
||||
country = self.current_mission.country(self.game.player_country)
|
||||
else:
|
||||
country = self.current_mission.country(self.game.enemy_country)
|
||||
if cp.id in self.game.planners.keys():
|
||||
airgen.generate_flights(
|
||||
cp,
|
||||
country,
|
||||
self.game.planners[cp.id],
|
||||
groundobjectgen.runways
|
||||
)
|
||||
@classmethod
|
||||
def inject_lua_trigger(cls, contents: str, comment: str) -> None:
|
||||
trigger = TriggerStart(comment=comment)
|
||||
trigger.add_action(DoScript(String(contents)))
|
||||
cls.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
# Generate ground units on frontline everywhere
|
||||
jtacs: List[JtacInfo] = []
|
||||
for player_cp, enemy_cp in self.game.theater.conflicts(True):
|
||||
conflict = Conflict.frontline_cas_conflict(self.attacker_name, self.defender_name,
|
||||
self.current_mission.country(self.attacker_country),
|
||||
self.current_mission.country(self.defender_country),
|
||||
player_cp, enemy_cp, self.game.theater)
|
||||
# Generate frontline ops
|
||||
player_gp = self.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id]
|
||||
enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
|
||||
groundConflictGen = GroundConflictGenerator(self.current_mission, conflict, self.game, player_gp, enemy_gp, player_cp.stances[enemy_cp.id])
|
||||
groundConflictGen.generate()
|
||||
jtacs.extend(groundConflictGen.jtacs)
|
||||
@classmethod
|
||||
def bypass_plugin_script(cls, mnemonic: str) -> None:
|
||||
cls.plugin_scripts.append(mnemonic)
|
||||
|
||||
# Setup combined arms parameters
|
||||
self.current_mission.groundControl.pilot_can_control_vehicles = self.ca_slots > 0
|
||||
if self.game.player_country in [country.name for country in self.current_mission.coalition["blue"].countries.values()]:
|
||||
self.current_mission.groundControl.blue_tactical_commander = self.ca_slots
|
||||
@classmethod
|
||||
def inject_plugin_script(cls, plugin_mnemonic: str, script: str,
|
||||
script_mnemonic: str) -> None:
|
||||
if script_mnemonic in cls.plugin_scripts:
|
||||
logging.debug(
|
||||
f"Skipping already loaded {script} for {plugin_mnemonic}"
|
||||
)
|
||||
else:
|
||||
self.current_mission.groundControl.red_tactical_commander = self.ca_slots
|
||||
cls.plugin_scripts.append(script_mnemonic)
|
||||
|
||||
# Triggers
|
||||
triggersgen = TriggersGenerator(self.current_mission, self.conflict,
|
||||
self.game)
|
||||
triggersgen.generate()
|
||||
plugin_path = Path("./resources/plugins", plugin_mnemonic)
|
||||
|
||||
# Options
|
||||
forcedoptionsgen = ForcedOptionsGenerator(self.current_mission,
|
||||
self.conflict, self.game)
|
||||
forcedoptionsgen.generate()
|
||||
script_path = Path(plugin_path, script)
|
||||
if not script_path.exists():
|
||||
logging.error(
|
||||
f"Cannot find {script_path} for plugin {plugin_mnemonic}"
|
||||
)
|
||||
return
|
||||
|
||||
# Generate Visuals Smoke Effects
|
||||
visualgen = VisualGenerator(self.current_mission, self.conflict,
|
||||
self.game)
|
||||
if self.game.settings.perf_smoke_gen:
|
||||
visualgen.generate()
|
||||
|
||||
# Inject Plugins Lua Scripts
|
||||
listOfPluginsScripts = []
|
||||
plugin_file_path = Path("./resources/scripts/plugins/__plugins.lst")
|
||||
if plugin_file_path.exists():
|
||||
for line in plugin_file_path.read_text().splitlines():
|
||||
name = line.strip()
|
||||
if not name.startswith( '#' ):
|
||||
trigger = TriggerStart(comment="Load " + name)
|
||||
listOfPluginsScripts.append(name)
|
||||
fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/plugins/" + name)
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
else:
|
||||
logging.info(
|
||||
f"Not loading plugins, {plugin_file_path} does not exist")
|
||||
|
||||
# Inject Mist Script if not done already in the plugins
|
||||
if not "mist.lua" in listOfPluginsScripts and not "mist_4_3_74.lua" in listOfPluginsScripts: # don't load the script twice
|
||||
trigger = TriggerStart(comment="Load Mist Lua framework")
|
||||
fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/mist_4_3_74.lua")
|
||||
trigger = TriggerStart(comment=f"Load {script_mnemonic}")
|
||||
filename = script_path.resolve()
|
||||
fileref = cls.current_mission.map_resource.add_resource_file(
|
||||
filename)
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
cls.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
# Inject JSON library if not done already in the plugins
|
||||
if not "json.lua" in listOfPluginsScripts : # don't load the script twice
|
||||
trigger = TriggerStart(comment="Load JSON Lua library")
|
||||
fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/json.lua")
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
# Inject Ciribob's JTACAutoLase if not done already in the plugins
|
||||
if not "JTACAutoLase.lua" in listOfPluginsScripts : # don't load the script twice
|
||||
trigger = TriggerStart(comment="Load JTACAutoLase.lua script")
|
||||
fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/JTACAutoLase.lua")
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
# 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("state.json") + "]]"
|
||||
lua = """
|
||||
-- setting configuration table
|
||||
env.info("DCSLiberation|: setting configuration table")
|
||||
|
||||
-- all data in this table is overridable.
|
||||
dcsLiberation = {}
|
||||
|
||||
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
|
||||
dcsLiberation.installPath=""" + state_location + """
|
||||
|
||||
-- you can override dcsLiberation.JTACAutoLase to make it use your own function ; it will be called with these parameters : ({jtac.unit_name}, {jtac.code}, {smoke}, 'vehicle') for all JTACs
|
||||
if ctld then
|
||||
dcsLiberation.JTACAutoLase=ctld.JTACAutoLase
|
||||
elseif JTACAutoLase then
|
||||
dcsLiberation.JTACAutoLase=JTACAutoLase
|
||||
end
|
||||
|
||||
-- later, we'll add more data to the table
|
||||
--dcsLiberation.POIs = {}
|
||||
--dcsLiberation.BASEs = {}
|
||||
--dcsLiberation.JTACs = {}
|
||||
@classmethod
|
||||
def notify_info_generators(
|
||||
cls,
|
||||
groundobjectgen: GroundObjectsGenerator,
|
||||
airsupportgen: AirSupportConflictGenerator,
|
||||
jtacs: List[JtacInfo],
|
||||
airgen: AircraftConflictGenerator,
|
||||
):
|
||||
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)
|
||||
"""
|
||||
gens: List[MissionInfoGenerator] = [
|
||||
KneeboardGenerator(cls.current_mission, cls.game),
|
||||
BriefingGenerator(cls.current_mission, cls.game)
|
||||
]
|
||||
for gen in gens:
|
||||
for dynamic_runway in groundobjectgen.runways.values():
|
||||
gen.add_dynamic_runway(dynamic_runway)
|
||||
|
||||
trigger = TriggerStart(comment="Set DCS Liberation data")
|
||||
trigger.add_action(DoScript(String(lua)))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
gen.add_tanker(tanker)
|
||||
|
||||
# Inject DCS-Liberation script if not done already in the plugins
|
||||
if not "dcs_liberation.lua" in listOfPluginsScripts : # don't load the script twice
|
||||
trigger = TriggerStart(comment="Load DCS Liberation script")
|
||||
fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/dcs_liberation.lua")
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
if cls.player_awacs_enabled:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
gen.add_awacs(awacs)
|
||||
|
||||
# add a configuration for JTACAutoLase and start lasing for all JTACs
|
||||
smoke = "true"
|
||||
if hasattr(self.game.settings, "jtac_smoke_on"):
|
||||
if not self.game.settings.jtac_smoke_on:
|
||||
smoke = "false"
|
||||
for jtac in jtacs:
|
||||
gen.add_jtac(jtac)
|
||||
|
||||
lua = """
|
||||
-- setting and starting JTACs
|
||||
env.info("DCSLiberation|: setting and starting JTACs")
|
||||
"""
|
||||
for flight in airgen.flights:
|
||||
gen.add_flight(flight)
|
||||
gen.generate()
|
||||
|
||||
for jtac in jtacs:
|
||||
lua += f"if dcsLiberation.JTACAutoLase then dcsLiberation.JTACAutoLase('{jtac.unit_name}', {jtac.code}, {smoke}, 'vehicle') end\n"
|
||||
@classmethod
|
||||
def create_unit_map(cls) -> None:
|
||||
cls.unit_map = UnitMap()
|
||||
for control_point in cls.game.theater.controlpoints:
|
||||
if isinstance(control_point, Airfield):
|
||||
cls.unit_map.add_airfield(control_point)
|
||||
|
||||
trigger = TriggerStart(comment="Start JTACs")
|
||||
trigger.add_action(DoScript(String(lua)))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
@classmethod
|
||||
def create_radio_registries(cls) -> None:
|
||||
unique_map_frequencies: Set[RadioFrequency] = set()
|
||||
cls._create_tacan_registry(unique_map_frequencies)
|
||||
cls._create_radio_registry(unique_map_frequencies)
|
||||
|
||||
self.assign_channels_to_flights(airgen.flights,
|
||||
airsupportgen.air_support)
|
||||
assert cls.radio_registry is not None
|
||||
for frequency in unique_map_frequencies:
|
||||
cls.radio_registry.reserve(frequency)
|
||||
|
||||
kneeboard_generator = KneeboardGenerator(self.current_mission)
|
||||
|
||||
for dynamic_runway in groundobjectgen.runways.values():
|
||||
self.briefinggen.add_dynamic_runway(dynamic_runway)
|
||||
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
self.briefinggen.add_tanker(tanker)
|
||||
kneeboard_generator.add_tanker(tanker)
|
||||
|
||||
if self.is_awacs_enabled:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
self.briefinggen.add_awacs(awacs)
|
||||
kneeboard_generator.add_awacs(awacs)
|
||||
|
||||
for jtac in jtacs:
|
||||
self.briefinggen.add_jtac(jtac)
|
||||
kneeboard_generator.add_jtac(jtac)
|
||||
|
||||
for flight in airgen.flights:
|
||||
self.briefinggen.add_flight(flight)
|
||||
kneeboard_generator.add_flight(flight)
|
||||
|
||||
self.briefinggen.generate()
|
||||
kneeboard_generator.generate()
|
||||
|
||||
def assign_channels_to_flights(self, flights: List[FlightData],
|
||||
@classmethod
|
||||
def assign_channels_to_flights(cls, flights: List[FlightData],
|
||||
air_support: AirSupport) -> None:
|
||||
"""Assigns preset radio channels for client flights."""
|
||||
for flight in flights:
|
||||
if not flight.client_units:
|
||||
continue
|
||||
self.assign_channels_to_flight(flight, air_support)
|
||||
cls.assign_channels_to_flight(flight, air_support)
|
||||
|
||||
def assign_channels_to_flight(self, flight: FlightData,
|
||||
@staticmethod
|
||||
def assign_channels_to_flight(flight: FlightData,
|
||||
air_support: AirSupport) -> None:
|
||||
"""Assigns preset radio channels for a client flight."""
|
||||
airframe = flight.aircraft_type
|
||||
@@ -376,6 +228,348 @@ class Operation:
|
||||
logging.warning(f"No aircraft data for {airframe.id}")
|
||||
return
|
||||
|
||||
aircraft_data.channel_allocator.assign_channels_for_flight(
|
||||
flight, air_support
|
||||
if aircraft_data.channel_allocator is not None:
|
||||
aircraft_data.channel_allocator.assign_channels_for_flight(
|
||||
flight, air_support
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _create_tacan_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None:
|
||||
"""
|
||||
Dedup beacon/radio frequencies, since some maps have some frequencies
|
||||
used multiple times.
|
||||
"""
|
||||
cls.tacan_registry = TacanRegistry()
|
||||
beacons = load_beacons_for_terrain(cls.game.theater.terrain.name)
|
||||
|
||||
for beacon in beacons:
|
||||
unique_map_frequencies.add(beacon.frequency)
|
||||
if beacon.is_tacan:
|
||||
if beacon.channel is None:
|
||||
logging.error(
|
||||
f"TACAN beacon has no channel: {beacon.callsign}")
|
||||
else:
|
||||
cls.tacan_registry.reserve(beacon.tacan_channel)
|
||||
|
||||
@classmethod
|
||||
def _create_radio_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None:
|
||||
cls.radio_registry = RadioRegistry()
|
||||
for data in AIRFIELD_DATA.values():
|
||||
if data.theater == cls.game.theater.terrain.name and data.atc:
|
||||
unique_map_frequencies.add(data.atc.hf)
|
||||
unique_map_frequencies.add(data.atc.vhf_fm)
|
||||
unique_map_frequencies.add(data.atc.vhf_am)
|
||||
unique_map_frequencies.add(data.atc.uhf)
|
||||
# No need to reserve ILS or TACAN because those are in the
|
||||
# beacon list.
|
||||
|
||||
@classmethod
|
||||
def _generate_ground_units(cls):
|
||||
cls.groundobjectgen = GroundObjectsGenerator(
|
||||
cls.current_mission,
|
||||
cls.game,
|
||||
cls.radio_registry,
|
||||
cls.tacan_registry,
|
||||
cls.unit_map
|
||||
)
|
||||
cls.groundobjectgen.generate()
|
||||
|
||||
@classmethod
|
||||
def _generate_destroyed_units(cls) -> None:
|
||||
"""Add destroyed units to the Mission"""
|
||||
for d in cls.game.get_destroyed_units():
|
||||
try:
|
||||
utype = db.unit_type_from_name(d["type"])
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
pos = Point(d["x"], d["z"])
|
||||
if utype is not None and not cls.game.position_culled(pos) and cls.game.settings.perf_destroyed_units:
|
||||
cls.current_mission.static_group(
|
||||
country=cls.current_mission.country(
|
||||
cls.game.player_country),
|
||||
name="",
|
||||
_type=utype,
|
||||
hidden=True,
|
||||
position=pos,
|
||||
heading=d["orientation"],
|
||||
dead=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate(cls) -> UnitMap:
|
||||
"""Build the final Mission to be exported"""
|
||||
cls.create_unit_map()
|
||||
cls.create_radio_registries()
|
||||
# Set mission time and weather conditions.
|
||||
EnvironmentGenerator(cls.current_mission,
|
||||
cls.game.conditions).generate()
|
||||
cls._generate_ground_units()
|
||||
cls._generate_destroyed_units()
|
||||
cls._generate_air_units()
|
||||
cls.assign_channels_to_flights(cls.airgen.flights,
|
||||
cls.airsupportgen.air_support)
|
||||
cls._generate_ground_conflicts()
|
||||
|
||||
# Triggers
|
||||
triggersgen = TriggersGenerator(cls.current_mission, cls.game)
|
||||
triggersgen.generate()
|
||||
|
||||
# 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
|
||||
|
||||
# Options
|
||||
forcedoptionsgen = ForcedOptionsGenerator(
|
||||
cls.current_mission, cls.game)
|
||||
forcedoptionsgen.generate()
|
||||
|
||||
# Generate Visuals Smoke Effects
|
||||
visualgen = VisualGenerator(cls.current_mission, cls.game)
|
||||
if cls.game.settings.perf_smoke_gen:
|
||||
visualgen.generate()
|
||||
|
||||
cls.generate_lua(cls.airgen, cls.airsupportgen, cls.jtacs)
|
||||
|
||||
# Inject Plugins Lua Scripts and data
|
||||
cls.plugin_scripts.clear()
|
||||
for plugin in LuaPluginManager.plugins():
|
||||
if plugin.enabled:
|
||||
plugin.inject_scripts(cls)
|
||||
plugin.inject_configuration(cls)
|
||||
|
||||
cls.assign_channels_to_flights(cls.airgen.flights,
|
||||
cls.airsupportgen.air_support)
|
||||
cls.notify_info_generators(
|
||||
cls.groundobjectgen,
|
||||
cls.airsupportgen,
|
||||
cls.jtacs,
|
||||
cls.airgen
|
||||
)
|
||||
cls.reset_naming_ids()
|
||||
return cls.unit_map
|
||||
|
||||
@classmethod
|
||||
def _generate_air_units(cls) -> None:
|
||||
"""Generate the air units for the Operation"""
|
||||
|
||||
# Air Support (Tanker & Awacs)
|
||||
assert cls.radio_registry and cls.tacan_registry
|
||||
cls.airsupportgen = AirSupportConflictGenerator(
|
||||
cls.current_mission, cls.air_conflict(), cls.game, cls.radio_registry,
|
||||
cls.tacan_registry)
|
||||
cls.airsupportgen.generate()
|
||||
|
||||
# Generate Aircraft Activity on the map
|
||||
cls.airgen = AircraftConflictGenerator(
|
||||
cls.current_mission, cls.game.settings, cls.game,
|
||||
cls.radio_registry, cls.unit_map)
|
||||
cls.airgen.clear_parking_slots()
|
||||
|
||||
cls.airgen.generate_flights(
|
||||
cls.current_mission.country(cls.game.player_country),
|
||||
cls.game.blue_ato,
|
||||
cls.groundobjectgen.runways
|
||||
)
|
||||
cls.airgen.generate_flights(
|
||||
cls.current_mission.country(cls.game.enemy_country),
|
||||
cls.game.red_ato,
|
||||
cls.groundobjectgen.runways
|
||||
)
|
||||
cls.airgen.spawn_unused_aircraft(
|
||||
cls.current_mission.country(cls.game.player_country),
|
||||
cls.current_mission.country(cls.game.enemy_country))
|
||||
|
||||
@classmethod
|
||||
def _generate_ground_conflicts(cls) -> None:
|
||||
"""For each frontline in the Operation, generate the ground conflicts and JTACs"""
|
||||
for front_line in cls.game.theater.conflicts(True):
|
||||
player_cp = front_line.control_point_a
|
||||
enemy_cp = front_line.control_point_b
|
||||
conflict = Conflict.frontline_cas_conflict(
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
cls.current_mission.country(cls.game.player_country),
|
||||
cls.current_mission.country(cls.game.enemy_country),
|
||||
player_cp,
|
||||
enemy_cp,
|
||||
cls.game.theater
|
||||
)
|
||||
# Generate frontline ops
|
||||
player_gp = cls.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id]
|
||||
enemy_gp = cls.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
|
||||
ground_conflict_gen = GroundConflictGenerator(
|
||||
cls.current_mission,
|
||||
conflict, cls.game,
|
||||
player_gp, enemy_gp,
|
||||
player_cp.stances[enemy_cp.id],
|
||||
cls.unit_map
|
||||
)
|
||||
ground_conflict_gen.generate()
|
||||
cls.jtacs.extend(ground_conflict_gen.jtacs)
|
||||
|
||||
@classmethod
|
||||
def 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": {},
|
||||
"Tankers": {},
|
||||
"AWACs": {},
|
||||
"JTACs": {},
|
||||
"TargetPoints": {},
|
||||
} # type: ignore
|
||||
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
luaData["Tankers"][tanker.callsign] = {
|
||||
"dcsGroupName": tanker.dcsGroupName,
|
||||
"callsign": tanker.callsign,
|
||||
"variant": tanker.variant,
|
||||
"radio": tanker.freq.mhz,
|
||||
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name
|
||||
}
|
||||
|
||||
if airsupportgen.air_support.awacs:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
luaData["AWACs"][awacs.callsign] = {
|
||||
"dcsGroupName": awacs.dcsGroupName,
|
||||
"callsign": awacs.callsign,
|
||||
"radio": awacs.freq.mhz
|
||||
}
|
||||
|
||||
for jtac in jtacs:
|
||||
luaData["JTACs"][jtac.callsign] = {
|
||||
"dcsGroupName": jtac.dcsGroupName,
|
||||
"callsign": jtac.callsign,
|
||||
"zone": jtac.region,
|
||||
"dcsUnit": jtac.unit_name,
|
||||
"laserCode": jtac.code
|
||||
}
|
||||
|
||||
for flight in airgen.flights:
|
||||
if flight.friendly and flight.flight_type in [FlightType.ANTISHIP,
|
||||
FlightType.DEAD,
|
||||
FlightType.SEAD,
|
||||
FlightType.STRIKE]:
|
||||
flightType = str(flight.flight_type)
|
||||
flightTarget = flight.package.target
|
||||
if flightTarget:
|
||||
flightTargetName = None
|
||||
flightTargetType = None
|
||||
if isinstance(flightTarget, TheaterGroundObject):
|
||||
flightTargetName = flightTarget.obj_name
|
||||
flightTargetType = flightType + \
|
||||
f" TGT ({flightTarget.category})"
|
||||
elif hasattr(flightTarget, 'name'):
|
||||
flightTargetName = flightTarget.name
|
||||
flightTargetType = flightType + " TGT (Airbase)"
|
||||
luaData["TargetPoints"][flightTargetName] = {
|
||||
"name": flightTargetName,
|
||||
"type": flightTargetType,
|
||||
"position": {"x": flightTarget.position.x,
|
||||
"y": flightTarget.position.y}
|
||||
}
|
||||
|
||||
# set a LUA table with data from Liberation that we want to set
|
||||
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
|
||||
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
|
||||
state_location = "[[" + os.path.abspath(".") + "]]"
|
||||
lua = """
|
||||
-- setting configuration table
|
||||
env.info("DCSLiberation|: setting configuration table")
|
||||
|
||||
-- all data in this table is overridable.
|
||||
dcsLiberation = {}
|
||||
|
||||
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
|
||||
dcsLiberation.installPath=""" + state_location + """
|
||||
|
||||
"""
|
||||
# Process the tankers
|
||||
lua += """
|
||||
|
||||
-- list the tankers generated by Liberation
|
||||
dcsLiberation.Tankers = {
|
||||
"""
|
||||
for key in luaData["Tankers"]:
|
||||
data = luaData["Tankers"][key]
|
||||
dcsGroupName = data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
variant = data["variant"]
|
||||
tacan = data["tacan"]
|
||||
radio = data["radio"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', variant='{variant}', tacan='{tacan}', radio='{radio}' }}, \n"
|
||||
# lua += f" {{name='{dcsGroupName}', description='{callsign} ({variant})', information='Tacan:{tacan} Radio:{radio}' }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the AWACSes
|
||||
lua += """
|
||||
|
||||
-- list the AWACs generated by Liberation
|
||||
dcsLiberation.AWACs = {
|
||||
"""
|
||||
for key in luaData["AWACs"]:
|
||||
data = luaData["AWACs"][key]
|
||||
dcsGroupName = data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
radio = data["radio"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', radio='{radio}' }}, \n"
|
||||
# lua += f" {{name='{dcsGroupName}', description='{callsign} (AWACS)', information='Radio:{radio}' }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the JTACs
|
||||
lua += """
|
||||
|
||||
-- list the JTACs generated by Liberation
|
||||
dcsLiberation.JTACs = {
|
||||
"""
|
||||
for key in luaData["JTACs"]:
|
||||
data = luaData["JTACs"][key]
|
||||
dcsGroupName = data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
zone = data["zone"]
|
||||
laserCode = data["laserCode"]
|
||||
dcsUnit = data["dcsUnit"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone='{zone}', laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
|
||||
# lua += f" {{name='{dcsGroupName}', description='JTAC {callsign} ', information='Laser:{laserCode}', jtac={laserCode} }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the Target Points
|
||||
lua += """
|
||||
|
||||
-- list the target points generated by Liberation
|
||||
dcsLiberation.TargetPoints = {
|
||||
"""
|
||||
for key in luaData["TargetPoints"]:
|
||||
data = luaData["TargetPoints"][key]
|
||||
name = data["name"]
|
||||
pointType = data["type"]
|
||||
positionX = data["position"]["x"]
|
||||
positionY = data["position"]["y"]
|
||||
lua += f" {{name='{name}', pointType='{pointType}', positionX='{positionX}', positionY='{positionY}' }}, \n"
|
||||
# lua += f" {{name='{pointType} {name}', point{{x={positionX}, z={positionY} }} }}, \n"
|
||||
lua += "}"
|
||||
|
||||
lua += """
|
||||
|
||||
-- list the airbases generated by Liberation
|
||||
-- dcsLiberation.Airbases = {}
|
||||
|
||||
-- list the aircraft carriers generated by Liberation
|
||||
-- dcsLiberation.Carriers = {}
|
||||
|
||||
-- later, we'll add more data to the table
|
||||
|
||||
"""
|
||||
|
||||
trigger = TriggerStart(comment="Set DCS Liberation data")
|
||||
trigger.add_action(DoScript(String(lua)))
|
||||
Operation.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
@@ -2,68 +2,58 @@ import logging
|
||||
import os
|
||||
import pickle
|
||||
import shutil
|
||||
from typing import Optional
|
||||
|
||||
_dcs_saved_game_folder = None # type: str
|
||||
_dcs_saved_game_folder: Optional[str] = None
|
||||
_file_abs_path = None
|
||||
|
||||
|
||||
def setup(user_folder: str):
|
||||
global _dcs_saved_game_folder
|
||||
_dcs_saved_game_folder = user_folder
|
||||
_file_abs_path = os.path.join(base_path(), "default.liberation")
|
||||
|
||||
|
||||
def base_path() -> str:
|
||||
global _dcs_saved_game_folder
|
||||
assert _dcs_saved_game_folder
|
||||
return _dcs_saved_game_folder
|
||||
|
||||
def _save_file() -> str:
|
||||
return os.path.join(base_path(), "default.liberation")
|
||||
|
||||
def _temporary_save_file() -> str:
|
||||
return os.path.join(base_path(), "tmpsave.liberation")
|
||||
|
||||
|
||||
def _autosave_path() -> str:
|
||||
return os.path.join(base_path(), "autosave.liberation")
|
||||
|
||||
def _save_file_exists() -> bool:
|
||||
return os.path.exists(_save_file())
|
||||
|
||||
def mission_path_for(name: str) -> str:
|
||||
return os.path.join(base_path(), "Missions", "{}".format(name))
|
||||
|
||||
|
||||
def restore_game():
|
||||
if not _save_file_exists():
|
||||
return None
|
||||
|
||||
with open(_save_file(), "rb") as f:
|
||||
try:
|
||||
save = pickle.load(f)
|
||||
return save
|
||||
except:
|
||||
logging.error("Invalid Save game")
|
||||
return None
|
||||
|
||||
def load_game(path):
|
||||
with open(path, "rb") as f:
|
||||
try:
|
||||
save = pickle.load(f)
|
||||
save.savepath = path
|
||||
return save
|
||||
except:
|
||||
logging.error("Invalid Save game")
|
||||
except Exception:
|
||||
logging.exception("Invalid Save game")
|
||||
return None
|
||||
|
||||
|
||||
def save_game(game) -> bool:
|
||||
try:
|
||||
with open(_temporary_save_file(), "wb") as f:
|
||||
pickle.dump(game, f)
|
||||
shutil.copy(_temporary_save_file(), game.savepath)
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
except Exception:
|
||||
logging.exception("Could not save game")
|
||||
return False
|
||||
|
||||
|
||||
def autosave(game) -> bool:
|
||||
"""
|
||||
Autosave to the autosave location
|
||||
@@ -74,7 +64,7 @@ def autosave(game) -> bool:
|
||||
with open(_autosave_path(), "wb") as f:
|
||||
pickle.dump(game, f)
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
except Exception:
|
||||
logging.exception("Could not save game")
|
||||
return False
|
||||
|
||||
2
game/plugins/__init__.py
Normal file
2
game/plugins/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .luaplugin import LuaPlugin
|
||||
from .manager import LuaPluginManager
|
||||
180
game/plugins/luaplugin.py
Normal file
180
game/plugins/luaplugin.py
Normal file
@@ -0,0 +1,180 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import textwrap
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from game.settings import Settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.operation.operation import Operation
|
||||
|
||||
|
||||
class LuaPluginWorkOrder:
|
||||
|
||||
def __init__(self, parent_mnemonic: str, filename: str, mnemonic: str,
|
||||
disable: bool) -> None:
|
||||
self.parent_mnemonic = parent_mnemonic
|
||||
self.filename = filename
|
||||
self.mnemonic = mnemonic
|
||||
self.disable = disable
|
||||
|
||||
def work(self, operation: Type[Operation]) -> None:
|
||||
if self.disable:
|
||||
operation.bypass_plugin_script(self.mnemonic)
|
||||
else:
|
||||
operation.inject_plugin_script(self.parent_mnemonic, self.filename,
|
||||
self.mnemonic)
|
||||
|
||||
|
||||
class PluginSettings:
|
||||
def __init__(self, identifier: str, enabled_by_default: bool) -> None:
|
||||
self.identifier = identifier
|
||||
self.enabled_by_default = enabled_by_default
|
||||
self.settings = Settings()
|
||||
self.initialize_settings()
|
||||
|
||||
def set_settings(self, settings: Settings):
|
||||
self.settings = settings
|
||||
self.initialize_settings()
|
||||
|
||||
def initialize_settings(self) -> None:
|
||||
# Plugin options are saved in the game's Settings, but it's possible for
|
||||
# plugins to change across loads. If new plugins are added or new
|
||||
# options added to those plugins, initialize the new settings.
|
||||
self.settings.initialize_plugin_option(self.identifier,
|
||||
self.enabled_by_default)
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
return self.settings.plugin_option(self.identifier)
|
||||
|
||||
def set_enabled(self, enabled: bool) -> None:
|
||||
self.settings.set_plugin_option(self.identifier, enabled)
|
||||
|
||||
|
||||
class LuaPluginOption(PluginSettings):
|
||||
def __init__(self, identifier: str, name: str,
|
||||
enabled_by_default: bool) -> None:
|
||||
super().__init__(identifier, enabled_by_default)
|
||||
self.name = name
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LuaPluginDefinition:
|
||||
identifier: str
|
||||
name: str
|
||||
present_in_ui: bool
|
||||
enabled_by_default: bool
|
||||
options: List[LuaPluginOption]
|
||||
work_orders: List[LuaPluginWorkOrder]
|
||||
config_work_orders: List[LuaPluginWorkOrder]
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, name: str, path: Path) -> LuaPluginDefinition:
|
||||
data = json.loads(path.read_text())
|
||||
|
||||
options = []
|
||||
for option in data.get("specificOptions"):
|
||||
option_id = option["mnemonic"]
|
||||
options.append(LuaPluginOption(
|
||||
identifier=f"{name}.{option_id}",
|
||||
name=option.get("nameInUI", name),
|
||||
enabled_by_default=option.get("defaultValue")
|
||||
))
|
||||
|
||||
work_orders = []
|
||||
for work_order in data.get("scriptsWorkOrders"):
|
||||
work_orders.append(LuaPluginWorkOrder(
|
||||
name, work_order.get("file"), work_order["mnemonic"],
|
||||
work_order.get("disable", False)
|
||||
))
|
||||
config_work_orders = []
|
||||
for work_order in data.get("configurationWorkOrders"):
|
||||
config_work_orders.append(LuaPluginWorkOrder(
|
||||
name, work_order.get("file"), work_order["mnemonic"],
|
||||
work_order.get("disable", False)
|
||||
))
|
||||
|
||||
return cls(
|
||||
identifier=name,
|
||||
name=data["nameInUI"],
|
||||
present_in_ui=not data.get("skipUI", False),
|
||||
enabled_by_default=data.get("defaultValue", False),
|
||||
options=options,
|
||||
work_orders=work_orders,
|
||||
config_work_orders=config_work_orders
|
||||
)
|
||||
|
||||
|
||||
class LuaPlugin(PluginSettings):
|
||||
|
||||
def __init__(self, definition: LuaPluginDefinition) -> None:
|
||||
self.definition = definition
|
||||
super().__init__(self.definition.identifier,
|
||||
self.definition.enabled_by_default)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.definition.name
|
||||
|
||||
@property
|
||||
def show_in_ui(self) -> bool:
|
||||
return self.definition.present_in_ui
|
||||
|
||||
@property
|
||||
def options(self) -> List[LuaPluginOption]:
|
||||
return self.definition.options
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, name: str, path: Path) -> Optional[LuaPlugin]:
|
||||
try:
|
||||
definition = LuaPluginDefinition.from_json(name, path)
|
||||
except KeyError:
|
||||
logging.exception("Required plugin configuration value missing")
|
||||
return None
|
||||
|
||||
return cls(definition)
|
||||
|
||||
def set_settings(self, settings: Settings):
|
||||
super().set_settings(settings)
|
||||
for option in self.definition.options:
|
||||
option.set_settings(self.settings)
|
||||
|
||||
def inject_scripts(self, operation: Type[Operation]) -> None:
|
||||
for work_order in self.definition.work_orders:
|
||||
work_order.work(operation)
|
||||
|
||||
def inject_configuration(self, operation: Type[Operation]) -> None:
|
||||
# inject the plugin options
|
||||
if self.options:
|
||||
option_decls = []
|
||||
for option in self.options:
|
||||
enabled = str(option.enabled).lower()
|
||||
name = option.identifier
|
||||
option_decls.append(
|
||||
f" dcsLiberation.plugins.{name} = {enabled}")
|
||||
|
||||
joined_options = "\n".join(option_decls)
|
||||
|
||||
lua = textwrap.dedent(f"""\
|
||||
-- {self.identifier} plugin configuration.
|
||||
|
||||
if dcsLiberation then
|
||||
if not dcsLiberation.plugins then
|
||||
dcsLiberation.plugins = {{}}
|
||||
end
|
||||
dcsLiberation.plugins.{self.identifier} = {{}}
|
||||
{joined_options}
|
||||
end
|
||||
|
||||
""")
|
||||
|
||||
operation.inject_lua_trigger(
|
||||
lua, f"{self.identifier} plugin configuration")
|
||||
|
||||
for work_order in self.definition.config_work_orders:
|
||||
work_order.work(operation)
|
||||
50
game/plugins/manager.py
Normal file
50
game/plugins/manager.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from game.settings import Settings
|
||||
from game.plugins.luaplugin import LuaPlugin
|
||||
|
||||
|
||||
class LuaPluginManager:
|
||||
_plugins_loaded = False
|
||||
_plugins: Dict[str, LuaPlugin] = {}
|
||||
|
||||
@classmethod
|
||||
def _load_plugins(cls) -> None:
|
||||
plugins_path = Path("resources/plugins")
|
||||
|
||||
path = plugins_path / "plugins.json"
|
||||
if not path.exists():
|
||||
raise RuntimeError(f"{path} does not exist. Cannot continue.")
|
||||
|
||||
logging.info(f"Reading plugins list from {path}")
|
||||
|
||||
data = json.loads(path.read_text())
|
||||
for name in data:
|
||||
plugin_path = plugins_path / name / "plugin.json"
|
||||
if not plugin_path.exists():
|
||||
raise RuntimeError(
|
||||
f"Invalid plugin configuration: required plugin {name} "
|
||||
f"does not exist at {plugin_path}")
|
||||
logging.info(f"Loading plugin {name} from {plugin_path}")
|
||||
plugin = LuaPlugin.from_json(name, plugin_path)
|
||||
if plugin is not None:
|
||||
cls._plugins[name] = plugin
|
||||
cls._plugins_loaded = True
|
||||
|
||||
@classmethod
|
||||
def _get_plugins(cls) -> Dict[str, LuaPlugin]:
|
||||
if not cls._plugins_loaded:
|
||||
cls._load_plugins()
|
||||
return cls._plugins
|
||||
|
||||
@classmethod
|
||||
def plugins(cls) -> List[LuaPlugin]:
|
||||
return list(cls._get_plugins().values())
|
||||
|
||||
@classmethod
|
||||
def load_settings(cls, settings: Settings) -> None:
|
||||
for plugin in cls.plugins():
|
||||
plugin.set_settings(settings)
|
||||
255
game/procurement.py
Normal file
255
game/procurement.py
Normal file
@@ -0,0 +1,255 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator, List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.unittype import FlyingType, VehicleType
|
||||
|
||||
from game import db
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.utils import Distance
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import FlightType
|
||||
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AircraftProcurementRequest:
|
||||
near: MissionTarget
|
||||
range: Distance
|
||||
task_capability: FlightType
|
||||
number: int
|
||||
|
||||
def __str__(self) -> str:
|
||||
task = self.task_capability.value
|
||||
distance = self.range.nautical_miles
|
||||
target = self.near.name
|
||||
return f"{self.number} ship {task} within {distance} nm of {target}"
|
||||
|
||||
|
||||
class ProcurementAi:
|
||||
def __init__(self, game: Game, for_player: bool, faction: Faction,
|
||||
manage_runways: bool, manage_front_line: bool,
|
||||
manage_aircraft: bool, front_line_budget_share: float) -> None:
|
||||
if front_line_budget_share > 1.0:
|
||||
raise ValueError
|
||||
|
||||
self.game = game
|
||||
self.is_player = for_player
|
||||
self.faction = faction
|
||||
self.manage_runways = manage_runways
|
||||
self.manage_front_line = manage_front_line
|
||||
self.manage_aircraft = manage_aircraft
|
||||
self.front_line_budget_share = front_line_budget_share
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
def spend_budget(
|
||||
self, budget: float,
|
||||
aircraft_requests: List[AircraftProcurementRequest]) -> float:
|
||||
if self.manage_runways:
|
||||
budget = self.repair_runways(budget)
|
||||
if self.manage_front_line:
|
||||
armor_budget = math.ceil(budget * self.front_line_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)
|
||||
return budget
|
||||
|
||||
def sell_incomplete_squadrons(self) -> float:
|
||||
# Selling incomplete squadrons gives us more money to spend on the next
|
||||
# turn. This serves as a short term fix for
|
||||
# https://github.com/Khopa/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/Khopa/dcs_liberation/issues/365).
|
||||
total = 0.0
|
||||
for cp in self.game.theater.control_points_for(self.is_player):
|
||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||
for aircraft, available in inventory.all_aircraft:
|
||||
# We only ever plan even groups, so the odd aircraft is unlikely
|
||||
# to get used.
|
||||
if available % 2 == 0:
|
||||
continue
|
||||
inventory.remove_aircraft(aircraft, 1)
|
||||
total += db.PRICES[aircraft]
|
||||
return total
|
||||
|
||||
def repair_runways(self, budget: float) -> float:
|
||||
for control_point in self.owned_points:
|
||||
if budget < db.RUNWAY_REPAIR_COST:
|
||||
break
|
||||
if control_point.runway_can_be_repaired:
|
||||
control_point.begin_runway_repair()
|
||||
budget -= db.RUNWAY_REPAIR_COST
|
||||
if self.is_player:
|
||||
self.game.message(
|
||||
"OPFOR has begun repairing the runway at "
|
||||
f"{control_point}"
|
||||
)
|
||||
else:
|
||||
self.game.message(
|
||||
"We have begun repairing the runway at "
|
||||
f"{control_point}"
|
||||
)
|
||||
return budget
|
||||
|
||||
def random_affordable_ground_unit(
|
||||
self, budget: float,
|
||||
cp: ControlPoint) -> Optional[Type[VehicleType]]:
|
||||
affordable_units = [u for u in self.faction.frontline_units + self.faction.artillery_units if
|
||||
db.PRICES[u] <= budget]
|
||||
|
||||
total_number_aa = cp.base.total_frontline_aa + cp.pending_frontline_aa_deliveries_count
|
||||
total_non_aa = cp.base.total_armor + cp.pending_deliveries_count - total_number_aa
|
||||
max_aa = math.ceil(total_non_aa/8)
|
||||
|
||||
# Limit the number of AA units the AI will buy
|
||||
if not total_number_aa < max_aa:
|
||||
for unit in [u for u in affordable_units if u in TYPE_SHORAD]:
|
||||
affordable_units.remove(unit)
|
||||
|
||||
if not affordable_units:
|
||||
return None
|
||||
return random.choice(affordable_units)
|
||||
|
||||
def reinforce_front_line(self, budget: float) -> float:
|
||||
if not self.faction.frontline_units and not self.faction.artillery_units:
|
||||
return budget
|
||||
|
||||
while budget > 0:
|
||||
candidates = self.front_line_candidates()
|
||||
if not candidates:
|
||||
break
|
||||
|
||||
cp = random.choice(candidates)
|
||||
unit = self.random_affordable_ground_unit(budget, cp)
|
||||
if unit is None:
|
||||
# Can't afford any more units.
|
||||
break
|
||||
|
||||
budget -= db.PRICES[unit]
|
||||
cp.pending_unit_deliveries.order({unit: 1})
|
||||
|
||||
return budget
|
||||
|
||||
def _affordable_aircraft_of_types(
|
||||
self, types: List[Type[FlyingType]], airbase: ControlPoint,
|
||||
number: int, max_price: float) -> Optional[Type[FlyingType]]:
|
||||
best_choice: Optional[Type[FlyingType]] = None
|
||||
for unit in [u for u in self.faction.aircrafts if u in types]:
|
||||
if db.PRICES[unit] * number > max_price:
|
||||
continue
|
||||
if not airbase.can_operate(unit):
|
||||
continue
|
||||
|
||||
# Affordable and compatible. To keep some variety, skip with a 50/50
|
||||
# chance. Might be a good idea to have the chance to skip based on
|
||||
# the price compared to the rest of the choices.
|
||||
best_choice = unit
|
||||
if random.choice([True, False]):
|
||||
break
|
||||
return best_choice
|
||||
|
||||
def affordable_aircraft_for(
|
||||
self, request: AircraftProcurementRequest,
|
||||
airbase: ControlPoint, budget: float) -> Optional[Type[FlyingType]]:
|
||||
return self._affordable_aircraft_of_types(
|
||||
aircraft_for_task(request.task_capability),
|
||||
airbase, request.number, budget)
|
||||
|
||||
def purchase_aircraft(
|
||||
self, budget: float,
|
||||
aircraft_requests: List[AircraftProcurementRequest]) -> float:
|
||||
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
|
||||
airbase.pending_unit_deliveries.order({unit: request.number})
|
||||
|
||||
return budget
|
||||
|
||||
@property
|
||||
def owned_points(self) -> List[ControlPoint]:
|
||||
if self.is_player:
|
||||
return self.game.theater.player_points()
|
||||
else:
|
||||
return self.game.theater.enemy_points()
|
||||
|
||||
def best_airbases_for(
|
||||
self,
|
||||
request: AircraftProcurementRequest) -> Iterator[ControlPoint]:
|
||||
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
|
||||
request.near
|
||||
)
|
||||
threatened = []
|
||||
for cp in distance_cache.airfields_within(request.range):
|
||||
if not cp.is_friendly(self.is_player):
|
||||
continue
|
||||
if not cp.runway_is_operational():
|
||||
continue
|
||||
if cp.unclaimed_parking(self.game) < request.number:
|
||||
continue
|
||||
if self.threat_zones.threatened(cp.position):
|
||||
threatened.append(cp)
|
||||
yield cp
|
||||
yield from threatened
|
||||
|
||||
def front_line_candidates(self) -> List[ControlPoint]:
|
||||
candidates = []
|
||||
|
||||
# Prefer to buy front line units at active front lines that are not
|
||||
# already overloaded.
|
||||
for cp in self.owned_points:
|
||||
if cp.expected_ground_units_next_turn.total >= 30:
|
||||
# Control point is already sufficiently defended.
|
||||
continue
|
||||
for connected in cp.connected_points:
|
||||
if not connected.is_friendly(to_player=self.is_player):
|
||||
candidates.append(cp)
|
||||
|
||||
if not candidates:
|
||||
# Otherwise buy reserves, but don't exceed 10 reserve units per CP.
|
||||
# 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 not cp.can_deploy_ground_units:
|
||||
continue
|
||||
if cp.expected_ground_units_next_turn.total >= 10:
|
||||
continue
|
||||
if cp.is_global:
|
||||
continue
|
||||
candidates.append(cp)
|
||||
|
||||
return candidates
|
||||
109
game/settings.py
109
game/settings.py
@@ -1,43 +1,82 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Optional
|
||||
|
||||
from dcs.forcedoptions import ForcedOptions
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
|
||||
def __init__(self):
|
||||
# Generator settings
|
||||
self.inverted = False
|
||||
self.do_not_generate_carrier = False # TODO : implement
|
||||
self.do_not_generate_lha = False # TODO : implement
|
||||
self.do_not_generate_player_navy = True # TODO : implement
|
||||
self.do_not_generate_enemy_navy = True # TODO : implement
|
||||
# Difficulty settings
|
||||
player_skill: str = "Good"
|
||||
enemy_skill: str = "Average"
|
||||
enemy_vehicle_skill: str = "Average"
|
||||
map_coalition_visibility: ForcedOptions.Views = ForcedOptions.Views.All
|
||||
labels: str = "Full"
|
||||
only_player_takeoff: bool = True # Legacy parameter do not use
|
||||
night_disabled: bool = False
|
||||
external_views_allowed: bool = True
|
||||
supercarrier: bool = False
|
||||
generate_marks: bool = True
|
||||
manpads: bool = True
|
||||
version: Optional[str] = None
|
||||
player_income_multiplier: float = 1.0
|
||||
enemy_income_multiplier: float = 1.0
|
||||
|
||||
# Difficulty settings
|
||||
self.player_skill = "Good"
|
||||
self.enemy_skill = "Average"
|
||||
self.enemy_vehicle_skill = "Average"
|
||||
self.map_coalition_visibility = "All Units"
|
||||
self.labels = "Full"
|
||||
self.only_player_takeoff = True # Legacy parameter do not use
|
||||
self.night_disabled = False
|
||||
self.external_views_allowed = True
|
||||
self.supercarrier = False
|
||||
self.multiplier = 1
|
||||
self.generate_marks = True
|
||||
self.sams = True # Legacy parameter do not use
|
||||
self.cold_start = False # Legacy parameter do not use
|
||||
self.version = None
|
||||
self.include_jtac_if_available = True
|
||||
self.jtac_smoke_on = True
|
||||
default_start_type: str = "Cold"
|
||||
|
||||
# Performance oriented
|
||||
self.perf_red_alert_state = True
|
||||
self.perf_smoke_gen = True
|
||||
self.perf_artillery = True
|
||||
self.perf_moving_units = True
|
||||
self.perf_infantry = True
|
||||
self.perf_ai_parking_start = True
|
||||
self.perf_destroyed_units = True
|
||||
# Campaign management
|
||||
automate_runway_repair: bool = False
|
||||
automate_front_line_reinforcements: bool = False
|
||||
automate_aircraft_reinforcements: bool = False
|
||||
restrict_weapons_by_date: bool = False
|
||||
|
||||
# Performance culling
|
||||
self.perf_culling = False
|
||||
self.perf_culling_distance = 100
|
||||
# Performance oriented
|
||||
perf_red_alert_state: bool = True
|
||||
perf_smoke_gen: bool = True
|
||||
perf_artillery: bool = True
|
||||
perf_moving_units: bool = True
|
||||
perf_infantry: bool = True
|
||||
perf_destroyed_units: bool = True
|
||||
|
||||
# Performance culling
|
||||
perf_culling: bool = False
|
||||
perf_culling_distance: int = 100
|
||||
perf_do_not_cull_carrier = True
|
||||
|
||||
# LUA Plugins system
|
||||
plugins: Dict[str, bool] = field(default_factory=dict)
|
||||
|
||||
# Cheating
|
||||
show_red_ato: bool = False
|
||||
enable_frontline_cheats: bool = False
|
||||
enable_base_capture_cheat: bool = False
|
||||
|
||||
never_delay_player_flights: bool = False
|
||||
|
||||
@staticmethod
|
||||
def plugin_settings_key(identifier: str) -> str:
|
||||
return f"plugins.{identifier}"
|
||||
|
||||
def initialize_plugin_option(self, identifier: str,
|
||||
default_value: bool) -> None:
|
||||
try:
|
||||
self.plugin_option(identifier)
|
||||
except KeyError:
|
||||
self.set_plugin_option(identifier, default_value)
|
||||
|
||||
def plugin_option(self, identifier: str) -> bool:
|
||||
return self.plugins[self.plugin_settings_key(identifier)]
|
||||
|
||||
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
|
||||
self.plugins[self.plugin_settings_key(identifier)] = enabled
|
||||
|
||||
def __setstate__(self, state) -> None:
|
||||
# __setstate__ is called with the dict of the object being unpickled. We
|
||||
# can provide save compatibility for new settings options (which
|
||||
# normally would not be present in the unpickled object) by creating a
|
||||
# new settings object, updating it with the unpickled state, and
|
||||
# updating our dict with that.
|
||||
new_state = Settings().__dict__
|
||||
new_state.update(state)
|
||||
self.__dict__.update(new_state)
|
||||
|
||||
5
game/theater/__init__.py
Normal file
5
game/theater/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .base import *
|
||||
from .conflicttheater import *
|
||||
from .controlpoint import *
|
||||
from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import SamGroundObject
|
||||
@@ -1,14 +1,16 @@
|
||||
import logging
|
||||
import typing
|
||||
import math
|
||||
import itertools
|
||||
import logging
|
||||
import math
|
||||
import typing
|
||||
from typing import Dict, Type
|
||||
|
||||
from dcs.planes import *
|
||||
from dcs.vehicles import *
|
||||
from dcs.task import *
|
||||
from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task
|
||||
from dcs.unittype import FlyingType, UnitType, VehicleType
|
||||
from dcs.vehicles import AirDefence, Armor
|
||||
|
||||
from game import db
|
||||
from gen import aaa
|
||||
from game.db import PRICES
|
||||
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
|
||||
|
||||
STRENGTH_AA_ASSEMBLE_MIN = 0.2
|
||||
PLANES_SCRAMBLE_MIN_BASE = 2
|
||||
@@ -20,27 +22,36 @@ BASE_MIN_STRENGTH = 0
|
||||
|
||||
|
||||
class Base:
|
||||
aircraft = {} # type: typing.Dict[PlaneType, int]
|
||||
armor = {} # type: typing.Dict[Armor, int]
|
||||
aa = {} # type: typing.Dict[AirDefence, int]
|
||||
strength = 1 # type: float
|
||||
commision_points = {}
|
||||
|
||||
def __init__(self):
|
||||
self.aircraft = {}
|
||||
self.armor = {}
|
||||
self.aa = {}
|
||||
self.commision_points = {}
|
||||
self.aircraft: Dict[Type[FlyingType], int] = {}
|
||||
self.armor: Dict[Type[VehicleType], int] = {}
|
||||
self.aa: Dict[AirDefence, int] = {}
|
||||
self.commision_points: Dict[Type, float] = {}
|
||||
self.strength = 1
|
||||
|
||||
@property
|
||||
def total_planes(self) -> int:
|
||||
def total_aircraft(self) -> int:
|
||||
return sum(self.aircraft.values())
|
||||
|
||||
@property
|
||||
def total_armor(self) -> int:
|
||||
return sum(self.armor.values())
|
||||
|
||||
@property
|
||||
def total_armor_value(self) -> int:
|
||||
total = 0
|
||||
for unit_type, count in self.armor.items():
|
||||
try:
|
||||
total += PRICES[unit_type] * count
|
||||
except KeyError:
|
||||
logging.exception(f"No price found for {unit_type.id}")
|
||||
return total
|
||||
|
||||
@property
|
||||
def total_frontline_aa(self) -> int:
|
||||
return sum([v for k, v in self.armor.items() if k in TYPE_SHORAD])
|
||||
|
||||
@property
|
||||
def total_aa(self) -> int:
|
||||
return sum(self.aa.values())
|
||||
@@ -55,17 +66,19 @@ class Base:
|
||||
def all_units(self):
|
||||
return itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items())
|
||||
|
||||
def _find_best_unit(self, dict, for_type: Task, count: int) -> typing.Dict:
|
||||
def _find_best_unit(self, available_units: Dict[UnitType, int],
|
||||
for_type: Task, count: int) -> Dict[UnitType, int]:
|
||||
if count <= 0:
|
||||
logging.warning("{}: no units for {}".format(self, for_type))
|
||||
return {}
|
||||
|
||||
sorted_units = [key for key in dict.keys() if key in db.UNIT_BY_TASK[for_type]]
|
||||
sorted_units = [key for key in available_units if
|
||||
key in db.UNIT_BY_TASK[for_type]]
|
||||
sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True)
|
||||
|
||||
result = {}
|
||||
result: Dict[UnitType, int] = {}
|
||||
for unit_type in sorted_units:
|
||||
existing_count = dict[unit_type] # type: int
|
||||
existing_count = available_units[unit_type] # type: int
|
||||
if not existing_count:
|
||||
continue
|
||||
|
||||
@@ -81,7 +94,7 @@ class Base:
|
||||
logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
|
||||
return result
|
||||
|
||||
def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[PlaneType, int]:
|
||||
def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[FlyingType, int]:
|
||||
return self._find_best_unit(self.aircraft, for_type, count)
|
||||
|
||||
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
|
||||
@@ -101,11 +114,11 @@ class Base:
|
||||
self.armor = {k: v for k, v in self.armor.items() if k in applicable_units}
|
||||
|
||||
def commision_units(self, units: typing.Dict[typing.Any, int]):
|
||||
for value in units.values():
|
||||
assert value > 0
|
||||
assert value == math.floor(value)
|
||||
|
||||
for unit_type, unit_count in units.items():
|
||||
if unit_count <= 0:
|
||||
continue
|
||||
|
||||
for_task = db.unit_task(unit_type)
|
||||
|
||||
target_dict = None
|
||||
@@ -116,8 +129,10 @@ class Base:
|
||||
elif for_task == AirDefence:
|
||||
target_dict = self.aa
|
||||
|
||||
assert target_dict is not None
|
||||
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
||||
if target_dict is not None:
|
||||
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
||||
else:
|
||||
logging.error("Unable to determine target dict for " + str(unit_type))
|
||||
|
||||
def commit_losses(self, units_lost: typing.Dict[typing.Any, int]):
|
||||
|
||||
@@ -146,11 +161,14 @@ class Base:
|
||||
elif self.strength <= 0:
|
||||
self.strength = BASE_MIN_STRENGTH
|
||||
|
||||
def set_strength_to_minimum(self) -> None:
|
||||
self.strength = BASE_MIN_STRENGTH
|
||||
|
||||
def scramble_count(self, multiplier: float, task: Task = None) -> int:
|
||||
if task:
|
||||
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task])
|
||||
else:
|
||||
count = self.total_planes
|
||||
count = self.total_aircraft
|
||||
|
||||
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
|
||||
return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count)
|
||||
@@ -162,18 +180,18 @@ class Base:
|
||||
# previous logic removed because we always want the full air defense capabilities.
|
||||
return self.total_aa
|
||||
|
||||
def scramble_sweep(self, multiplier: float) -> typing.Dict[PlaneType, int]:
|
||||
def scramble_sweep(self, multiplier: float) -> typing.Dict[FlyingType, int]:
|
||||
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
|
||||
|
||||
def scramble_last_defense(self):
|
||||
# return as many CAP-capable aircraft as we can since this is the last defense of the base
|
||||
# (but not more than 20 - that's just nuts)
|
||||
return self._find_best_planes(CAP, min(self.total_planes, 20))
|
||||
return self._find_best_planes(CAP, min(self.total_aircraft, 20))
|
||||
|
||||
def scramble_cas(self, multiplier: float) -> typing.Dict[PlaneType, int]:
|
||||
def scramble_cas(self, multiplier: float) -> typing.Dict[FlyingType, int]:
|
||||
return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS))
|
||||
|
||||
def scramble_interceptors(self, multiplier: float) -> typing.Dict[PlaneType, int]:
|
||||
def scramble_interceptors(self, multiplier: float) -> typing.Dict[FlyingType, int]:
|
||||
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
|
||||
|
||||
def assemble_attack(self) -> typing.Dict[Armor, int]:
|
||||
960
game/theater/conflicttheater.py
Normal file
960
game/theater/conflicttheater.py
Normal file
@@ -0,0 +1,960 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from itertools import tee
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union, cast
|
||||
|
||||
from shapely import geometry
|
||||
from shapely import ops
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.countries import (
|
||||
CombinedJointTaskForcesBlue,
|
||||
CombinedJointTaskForcesRed,
|
||||
)
|
||||
from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
from dcs.planes import F_15C
|
||||
from dcs.ships import (
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
)
|
||||
from dcs.statics import Fortification
|
||||
from dcs.terrain import (
|
||||
caucasus,
|
||||
nevada,
|
||||
normandy,
|
||||
persiangulf,
|
||||
syria,
|
||||
thechannel,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, Terrain
|
||||
from dcs.unitgroup import (
|
||||
FlyingGroup,
|
||||
Group,
|
||||
ShipGroup,
|
||||
StaticGroup,
|
||||
VehicleGroup,
|
||||
)
|
||||
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
||||
|
||||
from gen.flights.flight import FlightType
|
||||
from .controlpoint import (
|
||||
Airfield,
|
||||
Carrier,
|
||||
ControlPoint,
|
||||
Lha,
|
||||
MissionTarget,
|
||||
OffMapSpawn,
|
||||
Fob,
|
||||
)
|
||||
from .landmap import Landmap, load_landmap, poly_contains
|
||||
from ..utils import Distance, meters, nautical_miles
|
||||
|
||||
Numeric = Union[int, float]
|
||||
|
||||
SIZE_TINY = 150
|
||||
SIZE_SMALL = 600
|
||||
SIZE_REGULAR = 1000
|
||||
SIZE_BIG = 2000
|
||||
SIZE_LARGE = 3000
|
||||
|
||||
IMPORTANCE_LOW = 1
|
||||
IMPORTANCE_MEDIUM = 1.2
|
||||
IMPORTANCE_HIGH = 1.4
|
||||
|
||||
FRONTLINE_MIN_CP_DISTANCE = 5000
|
||||
|
||||
def pairwise(iterable):
|
||||
"""
|
||||
itertools recipe
|
||||
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
||||
"""
|
||||
a, b = tee(iterable)
|
||||
next(b, None)
|
||||
return zip(a, b)
|
||||
|
||||
|
||||
class MizCampaignLoader:
|
||||
BLUE_COUNTRY = CombinedJointTaskForcesBlue()
|
||||
RED_COUNTRY = CombinedJointTaskForcesRed()
|
||||
|
||||
OFF_MAP_UNIT_TYPE = F_15C.id
|
||||
|
||||
CV_UNIT_TYPE = CVN_74_John_C__Stennis.id
|
||||
LHA_UNIT_TYPE = LHA_1_Tarawa.id
|
||||
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
|
||||
|
||||
FOB_UNIT_TYPE = Unarmed.CP_SKP_11_ATC_Mobile_Command_Post.id
|
||||
|
||||
EWR_UNIT_TYPE = AirDefence.EWR_55G6.id
|
||||
SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300PS_SR_64H6E.id
|
||||
GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_2S6.id
|
||||
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
|
||||
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
|
||||
MISSILE_SITE_UNIT_TYPE = MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M.id
|
||||
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.SS_N_2_Silkworm.id
|
||||
|
||||
# Multiple options for the required SAMs so campaign designers can more
|
||||
# accurately see the coverage of their IADS for the expected type.
|
||||
REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.SAM_Patriot_LN_M901.id,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85C.id,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85D.id,
|
||||
}
|
||||
|
||||
REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.SAM_Hawk_LN_M192.id,
|
||||
AirDefence.SAM_SA_2_LN_SM_90.id,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73.id,
|
||||
}
|
||||
|
||||
BASE_DEFENSE_RADIUS = nautical_miles(2)
|
||||
|
||||
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
|
||||
self.theater = theater
|
||||
self.mission = Mission()
|
||||
self.mission.load_file(str(miz))
|
||||
self.control_point_id = itertools.count(1000)
|
||||
|
||||
# If there are no red carriers there usually aren't red units. Make sure
|
||||
# both countries are initialized so we don't have to deal with None.
|
||||
if self.mission.country(self.BLUE_COUNTRY.name) is None:
|
||||
self.mission.coalition["blue"].add_country(self.BLUE_COUNTRY)
|
||||
if self.mission.country(self.RED_COUNTRY.name) is None:
|
||||
self.mission.coalition["red"].add_country(self.RED_COUNTRY)
|
||||
|
||||
@staticmethod
|
||||
def control_point_from_airport(airport: Airport) -> ControlPoint:
|
||||
|
||||
# The wiki says this is a legacy property and to just use regular.
|
||||
size = SIZE_REGULAR
|
||||
|
||||
# The importance is taken from the periodicity of the airport's
|
||||
# warehouse divided by 10. 30 is the default, and out of range (valid
|
||||
# values are between 1.0 and 1.4). If it is used, pick the default
|
||||
# importance.
|
||||
if airport.periodicity == 30:
|
||||
importance = IMPORTANCE_MEDIUM
|
||||
else:
|
||||
importance = airport.periodicity / 10
|
||||
|
||||
cp = Airfield(airport, size, importance)
|
||||
cp.captured = airport.is_blue()
|
||||
|
||||
# Use the unlimited aircraft option to determine if an airfield should
|
||||
# be owned by the player when the campaign is "inverted".
|
||||
cp.captured_invert = airport.unlimited_aircrafts
|
||||
|
||||
return cp
|
||||
|
||||
def country(self, blue: bool) -> Country:
|
||||
country = self.mission.country(
|
||||
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name)
|
||||
# Should be guaranteed because we initialized them.
|
||||
assert country
|
||||
return country
|
||||
|
||||
@property
|
||||
def blue(self) -> Country:
|
||||
return self.country(blue=True)
|
||||
|
||||
@property
|
||||
def red(self) -> Country:
|
||||
return self.country(blue=False)
|
||||
|
||||
def off_map_spawns(self, blue: bool) -> Iterator[FlyingGroup]:
|
||||
for group in self.country(blue).plane_group:
|
||||
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def carriers(self, blue: bool) -> Iterator[ShipGroup]:
|
||||
for group in self.country(blue).ship_group:
|
||||
if group.units[0].type == self.CV_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def lhas(self, blue: bool) -> Iterator[ShipGroup]:
|
||||
for group in self.country(blue).ship_group:
|
||||
if group.units[0].type == self.LHA_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
|
||||
for group in self.country(blue).vehicle_group:
|
||||
if group.units[0].type == self.FOB_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ships(self) -> Iterator[ShipGroup]:
|
||||
for group in self.blue.ship_group:
|
||||
if group.units[0].type == self.SHIP_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ewrs(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.EWR_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.SAM_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def garrisons(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.GARRISON_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def offshore_strike_targets(self) -> Iterator[StaticGroup]:
|
||||
for group in self.blue.static_group:
|
||||
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def missile_sites(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def coastal_defenses(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_long_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.REQUIRED_LONG_RANGE_SAM_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_medium_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@cached_property
|
||||
def control_points(self) -> Dict[int, ControlPoint]:
|
||||
control_points = {}
|
||||
for airport in self.mission.terrain.airport_list():
|
||||
if airport.is_blue() or airport.is_red():
|
||||
control_point = self.control_point_from_airport(airport)
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
for blue in (False, True):
|
||||
for group in self.off_map_spawns(blue):
|
||||
control_point = OffMapSpawn(next(self.control_point_id),
|
||||
str(group.name), group.position)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for group in self.carriers(blue):
|
||||
# TODO: Name the carrier.
|
||||
control_point = Carrier(
|
||||
"carrier", group.position, next(self.control_point_id))
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for group in self.lhas(blue):
|
||||
# TODO: Name the LHA.
|
||||
control_point = Lha(
|
||||
"lha", group.position, next(self.control_point_id))
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for group in self.fobs(blue):
|
||||
control_point = Fob(
|
||||
str(group.name), group.position, next(self.control_point_id)
|
||||
)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
return control_points
|
||||
|
||||
@property
|
||||
def front_line_path_groups(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.country(blue=True).vehicle_group:
|
||||
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@cached_property
|
||||
def front_lines(self) -> Dict[str, ComplexFrontLine]:
|
||||
# Dict of front line ID to a front line.
|
||||
front_lines = {}
|
||||
for group in self.front_line_path_groups:
|
||||
# The unit will have its first waypoint at the source CP and the
|
||||
# final waypoint at the destination CP. Intermediate waypoints
|
||||
# define the curve of the front line.
|
||||
waypoints = [p.position for p in group.points]
|
||||
origin = self.theater.closest_control_point(waypoints[0])
|
||||
if origin is None:
|
||||
raise RuntimeError(
|
||||
f"No control point near the first waypoint of {group.name}")
|
||||
destination = self.theater.closest_control_point(waypoints[-1])
|
||||
if destination is None:
|
||||
raise RuntimeError(
|
||||
f"No control point near the final waypoint of {group.name}")
|
||||
|
||||
# Snap the begin and end points to the control points.
|
||||
waypoints[0] = origin.position
|
||||
waypoints[-1] = destination.position
|
||||
front_line_id = f"{origin.id}|{destination.id}"
|
||||
front_lines[front_line_id] = ComplexFrontLine(origin, waypoints)
|
||||
self.control_points[origin.id].connect(
|
||||
self.control_points[destination.id])
|
||||
self.control_points[destination.id].connect(
|
||||
self.control_points[origin.id])
|
||||
return front_lines
|
||||
|
||||
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
|
||||
closest = self.theater.closest_control_point(group.position)
|
||||
distance = meters(closest.position.distance_to_point(group.position))
|
||||
return closest, distance
|
||||
|
||||
def add_preset_locations(self) -> None:
|
||||
for group in self.garrisons:
|
||||
closest, distance = self.objective_info(group)
|
||||
if distance < self.BASE_DEFENSE_RADIUS:
|
||||
closest.preset_locations.base_garrisons.append(group.position)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Found garrison unit too far from base: {group.name}")
|
||||
|
||||
for group in self.sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
if distance < self.BASE_DEFENSE_RADIUS:
|
||||
closest.preset_locations.base_air_defense.append(group.position)
|
||||
else:
|
||||
closest.preset_locations.strike_locations.append(group.position)
|
||||
|
||||
for group in self.ewrs:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.ewrs.append(group.position)
|
||||
|
||||
for group in self.offshore_strike_targets:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.offshore_strike_locations.append(
|
||||
group.position)
|
||||
|
||||
for group in self.ships:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.ships.append(group.position)
|
||||
|
||||
for group in self.missile_sites:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.missile_sites.append(group.position)
|
||||
|
||||
for group in self.coastal_defenses:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.coastal_defenses.append(group.position)
|
||||
|
||||
for group in self.required_long_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_long_range_sams.append(
|
||||
group.position
|
||||
)
|
||||
|
||||
for group in self.required_medium_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_medium_range_sams.append(
|
||||
group.position
|
||||
)
|
||||
|
||||
def populate_theater(self) -> None:
|
||||
for control_point in self.control_points.values():
|
||||
self.theater.add_controlpoint(control_point)
|
||||
self.add_preset_locations()
|
||||
self.theater.set_frontline_data(self.front_lines)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReferencePoint:
|
||||
world_coordinates: Point
|
||||
image_coordinates: Point
|
||||
|
||||
|
||||
class ConflictTheater:
|
||||
terrain: Terrain
|
||||
|
||||
reference_points: Tuple[ReferencePoint, ReferencePoint]
|
||||
overview_image: str
|
||||
landmap: Optional[Landmap]
|
||||
"""
|
||||
land_poly = None # type: Polygon
|
||||
"""
|
||||
daytime_map: Dict[str, Tuple[int, int]]
|
||||
_frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
|
||||
|
||||
def __init__(self):
|
||||
self.controlpoints: List[ControlPoint] = []
|
||||
self._frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
|
||||
"""
|
||||
self.land_poly = geometry.Polygon(self.landmap[0][0])
|
||||
for x in self.landmap[1]:
|
||||
self.land_poly = self.land_poly.difference(geometry.Polygon(x))
|
||||
"""
|
||||
|
||||
@property
|
||||
def frontline_data(self) -> Optional[Dict[str, ComplexFrontLine]]:
|
||||
if self._frontline_data is None:
|
||||
self.load_frontline_data_from_file()
|
||||
return self._frontline_data
|
||||
|
||||
def load_frontline_data_from_file(self) -> None:
|
||||
if self._frontline_data is not None:
|
||||
logging.warning("Replacing existing frontline data from file")
|
||||
self._frontline_data = FrontLine.load_json_frontlines(self)
|
||||
if self._frontline_data is None:
|
||||
self._frontline_data = {}
|
||||
|
||||
def set_frontline_data(self, data: Dict[str, ComplexFrontLine]) -> None:
|
||||
if self._frontline_data is not None:
|
||||
logging.warning("Replacing existing frontline data")
|
||||
self._frontline_data = data
|
||||
|
||||
def add_controlpoint(self, point: ControlPoint,
|
||||
connected_to: Optional[List[ControlPoint]] = None):
|
||||
if connected_to is None:
|
||||
connected_to = []
|
||||
for connected_point in connected_to:
|
||||
point.connect(to=connected_point)
|
||||
|
||||
self.controlpoints.append(point)
|
||||
|
||||
def find_ground_objects_by_obj_name(self, obj_name):
|
||||
found = []
|
||||
for cp in self.controlpoints:
|
||||
for g in cp.ground_objects:
|
||||
if g.obj_name == obj_name:
|
||||
found.append(g)
|
||||
return found
|
||||
|
||||
def is_in_sea(self, point: Point) -> bool:
|
||||
if not self.landmap:
|
||||
return False
|
||||
|
||||
if self.is_on_land(point):
|
||||
return False
|
||||
|
||||
for exclusion_zone in self.landmap.exclusion_zones:
|
||||
if poly_contains(point.x, point.y, exclusion_zone):
|
||||
return False
|
||||
|
||||
for sea in self.landmap.sea_zones:
|
||||
if poly_contains(point.x, point.y, sea):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_on_land(self, point: Point) -> bool:
|
||||
if not self.landmap:
|
||||
return True
|
||||
|
||||
is_point_included = False
|
||||
if poly_contains(point.x, point.y, self.landmap.inclusion_zones):
|
||||
is_point_included = True
|
||||
|
||||
if not is_point_included:
|
||||
return False
|
||||
|
||||
for exclusion_zone in self.landmap.exclusion_zones:
|
||||
if poly_contains(point.x, point.y, exclusion_zone):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def nearest_land_pos(self, point: Point, extend_dist: int = 50) -> Point:
|
||||
"""Returns the nearest point inside a land exclusion zone from point
|
||||
`extend_dist` determines how far inside the zone the point should be placed"""
|
||||
if self.is_on_land(point):
|
||||
return point
|
||||
point = geometry.Point(point.x, point.y)
|
||||
nearest_points = []
|
||||
if not self.landmap:
|
||||
raise RuntimeError("Landmap not initialized")
|
||||
for inclusion_zone in self.landmap.inclusion_zones:
|
||||
nearest_pair = ops.nearest_points(point, inclusion_zone)
|
||||
nearest_points.append(nearest_pair[1])
|
||||
min_distance = point.distance(nearest_points[0]) # type: geometry.Point
|
||||
nearest_point = nearest_points[0] # type: geometry.Point
|
||||
for pt in nearest_points[1:]:
|
||||
distance = point.distance(pt)
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
nearest_point = pt
|
||||
assert isinstance(nearest_point, geometry.Point)
|
||||
point = Point(point.x, point.y)
|
||||
nearest_point = Point(nearest_point.x, nearest_point.y)
|
||||
new_point = point.point_from_heading(
|
||||
point.heading_between_point(nearest_point),
|
||||
point.distance_to_point(nearest_point) + extend_dist
|
||||
)
|
||||
return new_point
|
||||
|
||||
def control_points_for(self, player: bool) -> Iterator[ControlPoint]:
|
||||
for point in self.controlpoints:
|
||||
if point.captured == player:
|
||||
yield point
|
||||
|
||||
def player_points(self) -> List[ControlPoint]:
|
||||
return list(self.control_points_for(player=True))
|
||||
|
||||
def conflicts(self, from_player=True) -> Iterator[FrontLine]:
|
||||
for cp in [x for x in self.controlpoints if x.captured == from_player]:
|
||||
for connected_point in [x for x in cp.connected_points if x.captured != from_player]:
|
||||
yield FrontLine(cp, connected_point, self)
|
||||
|
||||
def enemy_points(self) -> List[ControlPoint]:
|
||||
return list(self.control_points_for(player=False))
|
||||
|
||||
def closest_control_point(self, point: Point) -> ControlPoint:
|
||||
closest = self.controlpoints[0]
|
||||
closest_distance = point.distance_to_point(closest.position)
|
||||
for control_point in self.controlpoints[1:]:
|
||||
distance = point.distance_to_point(control_point.position)
|
||||
if distance < closest_distance:
|
||||
closest = control_point
|
||||
closest_distance = distance
|
||||
return closest
|
||||
|
||||
def closest_target(self, point: Point) -> MissionTarget:
|
||||
closest: MissionTarget = self.controlpoints[0]
|
||||
closest_distance = point.distance_to_point(closest.position)
|
||||
for control_point in self.controlpoints[1:]:
|
||||
distance = point.distance_to_point(control_point.position)
|
||||
if distance < closest_distance:
|
||||
closest = control_point
|
||||
closest_distance = distance
|
||||
for tgo in control_point.ground_objects:
|
||||
distance = point.distance_to_point(tgo.position)
|
||||
if distance < closest_distance:
|
||||
closest = tgo
|
||||
closest_distance = distance
|
||||
for conflict in self.conflicts():
|
||||
distance = conflict.position.distance_to_point(point)
|
||||
if distance < closest_distance:
|
||||
closest = conflict
|
||||
closest_distance = distance
|
||||
return closest
|
||||
|
||||
def closest_opposing_control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
||||
"""
|
||||
Returns a tuple of the two nearest opposing ControlPoints in theater.
|
||||
(player_cp, enemy_cp)
|
||||
"""
|
||||
all_cp_min_distances = {}
|
||||
for idx, control_point in enumerate(self.controlpoints):
|
||||
distances = {}
|
||||
closest_distance = None
|
||||
for i, cp in enumerate(self.controlpoints):
|
||||
if i != idx and cp.captured is not control_point.captured:
|
||||
dist = cp.position.distance_to_point(control_point.position)
|
||||
if not closest_distance:
|
||||
closest_distance = dist
|
||||
distances[cp.id] = dist
|
||||
if dist < closest_distance:
|
||||
distances[cp.id] = dist
|
||||
closest_cp_id = min(distances, key=distances.get) # type: ignore
|
||||
|
||||
all_cp_min_distances[(control_point.id, closest_cp_id)] = distances[closest_cp_id]
|
||||
closest_opposing_cps = [
|
||||
self.find_control_point_by_id(i)
|
||||
for i
|
||||
in min(all_cp_min_distances, key=all_cp_min_distances.get) # type: ignore
|
||||
] # type: List[ControlPoint]
|
||||
assert len(closest_opposing_cps) == 2
|
||||
if closest_opposing_cps[0].captured:
|
||||
return cast(Tuple[ControlPoint, ControlPoint], tuple(closest_opposing_cps))
|
||||
else:
|
||||
return cast(Tuple[ControlPoint, ControlPoint], tuple(reversed(closest_opposing_cps)))
|
||||
|
||||
def find_control_point_by_id(self, id: int) -> ControlPoint:
|
||||
for i in self.controlpoints:
|
||||
if i.id == id:
|
||||
return i
|
||||
raise RuntimeError(f"Cannot find ControlPoint with ID {id}")
|
||||
|
||||
def add_json_cp(self, theater, p: dict) -> ControlPoint:
|
||||
cp: ControlPoint
|
||||
if p["type"] == "airbase":
|
||||
|
||||
airbase = theater.terrain.airports[p["id"]]
|
||||
|
||||
if "size" in p.keys():
|
||||
size = p["size"]
|
||||
else:
|
||||
size = SIZE_REGULAR
|
||||
|
||||
if "importance" in p.keys():
|
||||
importance = p["importance"]
|
||||
else:
|
||||
importance = IMPORTANCE_MEDIUM
|
||||
|
||||
cp = Airfield(airbase, size, importance)
|
||||
elif p["type"] == "carrier":
|
||||
cp = Carrier("carrier", Point(p["x"], p["y"]), p["id"])
|
||||
else:
|
||||
cp = Lha("lha", Point(p["x"], p["y"]), p["id"])
|
||||
|
||||
if "captured_invert" in p.keys():
|
||||
cp.captured_invert = p["captured_invert"]
|
||||
else:
|
||||
cp.captured_invert = False
|
||||
|
||||
return cp
|
||||
|
||||
@staticmethod
|
||||
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
|
||||
theaters = {
|
||||
"Caucasus": CaucasusTheater,
|
||||
"Nevada": NevadaTheater,
|
||||
"Persian Gulf": PersianGulfTheater,
|
||||
"Normandy": NormandyTheater,
|
||||
"The Channel": TheChannelTheater,
|
||||
"Syria": SyriaTheater,
|
||||
}
|
||||
theater = theaters[data["theater"]]
|
||||
t = theater()
|
||||
|
||||
miz = data.get("miz", None)
|
||||
if miz is not None:
|
||||
MizCampaignLoader(directory / miz, t).populate_theater()
|
||||
return t
|
||||
|
||||
cps = {}
|
||||
for p in data["player_points"]:
|
||||
cp = t.add_json_cp(theater, p)
|
||||
cp.captured = True
|
||||
cps[p["id"]] = cp
|
||||
t.add_controlpoint(cp)
|
||||
|
||||
for p in data["enemy_points"]:
|
||||
cp = t.add_json_cp(theater, p)
|
||||
cps[p["id"]] = cp
|
||||
t.add_controlpoint(cp)
|
||||
|
||||
for l in data["links"]:
|
||||
cps[l[0]].connect(cps[l[1]])
|
||||
cps[l[1]].connect(cps[l[0]])
|
||||
|
||||
return t
|
||||
|
||||
|
||||
class CaucasusTheater(ConflictTheater):
|
||||
terrain = caucasus.Caucasus()
|
||||
overview_image = "caumap.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(caucasus.Gelendzhik.position, Point(176, 298)),
|
||||
ReferencePoint(caucasus.Batumi.position, Point(1307, 1205)),
|
||||
)
|
||||
|
||||
landmap = load_landmap("resources\\caulandmap.p")
|
||||
daytime_map = {
|
||||
"dawn": (6, 9),
|
||||
"day": (9, 18),
|
||||
"dusk": (18, 20),
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
class PersianGulfTheater(ConflictTheater):
|
||||
terrain = persiangulf.PersianGulf()
|
||||
overview_image = "persiangulf.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(persiangulf.Jiroft_Airport.position,
|
||||
Point(1692, 1343)),
|
||||
ReferencePoint(persiangulf.Liwa_Airbase.position, Point(358, 3238)),
|
||||
)
|
||||
landmap = load_landmap("resources\\gulflandmap.p")
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (8, 16),
|
||||
"dusk": (16, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
class NevadaTheater(ConflictTheater):
|
||||
terrain = nevada.Nevada()
|
||||
overview_image = "nevada.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(nevada.Mina_Airport_3Q0.position, Point(252, 295)),
|
||||
ReferencePoint(nevada.Laughlin_Airport.position, Point(844, 909)),
|
||||
)
|
||||
landmap = load_landmap("resources\\nevlandmap.p")
|
||||
daytime_map = {
|
||||
"dawn": (4, 6),
|
||||
"day": (6, 17),
|
||||
"dusk": (17, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
class NormandyTheater(ConflictTheater):
|
||||
terrain = normandy.Normandy()
|
||||
overview_image = "normandy.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(normandy.Needs_Oar_Point.position, Point(515, 329)),
|
||||
ReferencePoint(normandy.Evreux.position, Point(2029, 1709)),
|
||||
)
|
||||
landmap = load_landmap("resources\\normandylandmap.p")
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (10, 17),
|
||||
"dusk": (17, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
class TheChannelTheater(ConflictTheater):
|
||||
terrain = thechannel.TheChannel()
|
||||
overview_image = "thechannel.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(thechannel.Abbeville_Drucat.position, Point(2005, 2390)),
|
||||
ReferencePoint(thechannel.Detling.position, Point(706, 382))
|
||||
)
|
||||
landmap = load_landmap("resources\\channellandmap.p")
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (10, 17),
|
||||
"dusk": (17, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
class SyriaTheater(ConflictTheater):
|
||||
terrain = syria.Syria()
|
||||
overview_image = "syria.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(syria.Eyn_Shemer.position, Point(564, 1289)),
|
||||
ReferencePoint(syria.Tabqa.position, Point(1329, 491)),
|
||||
)
|
||||
landmap = load_landmap("resources\\syrialandmap.p")
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (8, 16),
|
||||
"dusk": (16, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComplexFrontLine:
|
||||
"""
|
||||
Stores data necessary for building a multi-segment frontline.
|
||||
"points" should be ordered from closest to farthest distance originating from start_cp.position
|
||||
"""
|
||||
|
||||
start_cp: ControlPoint
|
||||
points: List[Point]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FrontLineSegment:
|
||||
"""
|
||||
Describes a line segment of a FrontLine
|
||||
"""
|
||||
|
||||
point_a: Point
|
||||
point_b: Point
|
||||
|
||||
@property
|
||||
def attack_heading(self) -> Numeric:
|
||||
"""The heading of the frontline segment from player to enemy control point"""
|
||||
return self.point_a.heading_between_point(self.point_b)
|
||||
|
||||
@property
|
||||
def attack_distance(self) -> Numeric:
|
||||
"""Length of the segment"""
|
||||
return self.point_a.distance_to_point(self.point_b)
|
||||
|
||||
|
||||
class FrontLine(MissionTarget):
|
||||
"""Defines a front line location between two control points.
|
||||
Front lines are the area where ground combat happens.
|
||||
Overwrites the entirety of MissionTarget __init__ method to allow for
|
||||
dynamic position calculation.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
control_point_a: ControlPoint,
|
||||
control_point_b: ControlPoint,
|
||||
theater: ConflictTheater
|
||||
) -> None:
|
||||
self.control_point_a = control_point_a
|
||||
self.control_point_b = control_point_b
|
||||
self.segments: List[FrontLineSegment] = []
|
||||
self.theater = theater
|
||||
self._build_segments()
|
||||
self.name = f"Front line {control_point_a}/{control_point_b}"
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
"""Returns True if the objective is in friendly territory."""
|
||||
return False
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
yield from [
|
||||
FlightType.CAS,
|
||||
# TODO: FlightType.TROOP_TRANSPORT
|
||||
# TODO: FlightType.EVAC
|
||||
]
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
"""
|
||||
The position where the conflict should occur
|
||||
according to the current strength of each control point.
|
||||
"""
|
||||
return self.point_from_a(self._position_distance)
|
||||
|
||||
@property
|
||||
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
||||
"""Returns a tuple of the two control points."""
|
||||
return self.control_point_a, self.control_point_b
|
||||
|
||||
@property
|
||||
def attack_distance(self):
|
||||
"""The total distance of all segments"""
|
||||
return sum(i.attack_distance for i in self.segments)
|
||||
|
||||
@property
|
||||
def attack_heading(self):
|
||||
"""The heading of the active attack segment from player to enemy control point"""
|
||||
return self.active_segment.attack_heading
|
||||
|
||||
@property
|
||||
def active_segment(self) -> FrontLineSegment:
|
||||
"""The FrontLine segment where there can be an active conflict"""
|
||||
if self._position_distance <= self.segments[0].attack_distance:
|
||||
return self.segments[0]
|
||||
|
||||
remaining_dist = self._position_distance
|
||||
for segment in self.segments:
|
||||
if remaining_dist <= segment.attack_distance:
|
||||
return segment
|
||||
else:
|
||||
remaining_dist -= segment.attack_distance
|
||||
logging.error(
|
||||
"Frontline attack distance is greater than the sum of its segments"
|
||||
)
|
||||
return self.segments[0]
|
||||
|
||||
def point_from_a(self, distance: Numeric) -> Point:
|
||||
"""
|
||||
Returns a point {distance} away from control_point_a along the frontline segments.
|
||||
"""
|
||||
if distance < self.segments[0].attack_distance:
|
||||
return self.control_point_a.position.point_from_heading(
|
||||
self.segments[0].attack_heading, distance
|
||||
)
|
||||
remaining_dist = distance
|
||||
for segment in self.segments:
|
||||
if remaining_dist < segment.attack_distance:
|
||||
return segment.point_a.point_from_heading(
|
||||
segment.attack_heading, remaining_dist
|
||||
)
|
||||
else:
|
||||
remaining_dist -= segment.attack_distance
|
||||
|
||||
@property
|
||||
def _position_distance(self) -> float:
|
||||
"""
|
||||
The distance from point "a" where the conflict should occur
|
||||
according to the current strength of each control point
|
||||
"""
|
||||
total_strength = (
|
||||
self.control_point_a.base.strength + self.control_point_b.base.strength
|
||||
)
|
||||
if self.control_point_a.base.strength == 0:
|
||||
return self._adjust_for_min_dist(0)
|
||||
if self.control_point_b.base.strength == 0:
|
||||
return self._adjust_for_min_dist(self.attack_distance)
|
||||
strength_pct = self.control_point_a.base.strength / total_strength
|
||||
return self._adjust_for_min_dist(strength_pct * self.attack_distance)
|
||||
|
||||
def _adjust_for_min_dist(self, distance: Numeric) -> Numeric:
|
||||
"""
|
||||
Ensures the frontline conflict is never located within the minimum distance
|
||||
constant of either end control point.
|
||||
"""
|
||||
if (distance > self.attack_distance / 2) and (
|
||||
distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance
|
||||
):
|
||||
distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE
|
||||
elif (distance < self.attack_distance / 2) and (
|
||||
distance < FRONTLINE_MIN_CP_DISTANCE
|
||||
):
|
||||
distance = FRONTLINE_MIN_CP_DISTANCE
|
||||
return distance
|
||||
|
||||
def _build_segments(self) -> None:
|
||||
"""Create line segments for the frontline"""
|
||||
control_point_ids = "|".join(
|
||||
[str(self.control_point_a.id), str(self.control_point_b.id)]
|
||||
) # from_cp.id|to_cp.id
|
||||
reversed_cp_ids = "|".join(
|
||||
[str(self.control_point_b.id), str(self.control_point_a.id)]
|
||||
)
|
||||
complex_frontlines = self.theater.frontline_data
|
||||
if (complex_frontlines) and (
|
||||
(control_point_ids in complex_frontlines)
|
||||
or (reversed_cp_ids in complex_frontlines)
|
||||
):
|
||||
# The frontline segments must be stored in the correct order for the distance algorithms to work.
|
||||
# The points in the frontline are ordered from the id before the | to the id after.
|
||||
# First, check if control point id pair matches in order, and create segments if a match is found.
|
||||
if control_point_ids in complex_frontlines:
|
||||
point_pairs = pairwise(complex_frontlines[control_point_ids].points)
|
||||
for i in point_pairs:
|
||||
self.segments.append(FrontLineSegment(i[0], i[1]))
|
||||
# Check the reverse order and build in reverse if found.
|
||||
elif reversed_cp_ids in complex_frontlines:
|
||||
point_pairs = pairwise(
|
||||
reversed(complex_frontlines[reversed_cp_ids].points)
|
||||
)
|
||||
for i in point_pairs:
|
||||
self.segments.append(FrontLineSegment(i[0], i[1]))
|
||||
# If no complex frontline has been configured, fall back to the old straight line method.
|
||||
else:
|
||||
self.segments.append(
|
||||
FrontLineSegment(
|
||||
self.control_point_a.position, self.control_point_b.position
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def load_json_frontlines(
|
||||
theater: ConflictTheater
|
||||
) -> Optional[Dict[str, ComplexFrontLine]]:
|
||||
"""Load complex frontlines from json"""
|
||||
try:
|
||||
path = Path(f"resources/frontlines/{theater.terrain.name.lower()}.json")
|
||||
with open(path, "r") as file:
|
||||
logging.debug(f"Loading frontline from {path}...")
|
||||
data = json.load(file)
|
||||
return {
|
||||
frontline: ComplexFrontLine(
|
||||
data[frontline]["start_cp"],
|
||||
[Point(i[0], i[1]) for i in data[frontline]["points"]],
|
||||
)
|
||||
for frontline in data
|
||||
}
|
||||
except OSError:
|
||||
logging.warning(
|
||||
f"Unable to load preset frontlines for {theater.terrain.name}"
|
||||
)
|
||||
return None
|
||||
883
game/theater/controlpoint.py
Normal file
883
game/theater/controlpoint.py
Normal file
@@ -0,0 +1,883 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from functools import total_ordering
|
||||
from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.ships import (
|
||||
CVN_74_John_C__Stennis,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
LHA_1_Tarawa,
|
||||
Type_071_Amphibious_Transport_Dock,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, ParkingSlot
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from gen.runways import RunwayAssigner, RunwayData
|
||||
from .base import Base
|
||||
from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import (
|
||||
BaseDefenseGroundObject,
|
||||
EwrGroundObject,
|
||||
GenericCarrierGroundObject,
|
||||
SamGroundObject,
|
||||
TheaterGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
)
|
||||
from ..db import PRICES
|
||||
from ..utils import nautical_miles
|
||||
from ..weather import Conditions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
class ControlPointType(Enum):
|
||||
#: An airbase with slots for everything.
|
||||
AIRBASE = 0
|
||||
#: A group with a Stennis type carrier (F/A-18, F-14 compatible).
|
||||
AIRCRAFT_CARRIER_GROUP = 1
|
||||
#: A group with a Tarawa carrier (Helicopters & Harrier).
|
||||
LHA_GROUP = 2
|
||||
#: A FARP, with slots for helicopters
|
||||
FARP = 4
|
||||
#: A FOB (ground units only)
|
||||
FOB = 5
|
||||
OFF_MAP = 6
|
||||
|
||||
|
||||
class LocationType(Enum):
|
||||
BaseAirDefense = "base air defense"
|
||||
Coastal = "coastal defense"
|
||||
Ewr = "EWR"
|
||||
Garrison = "garrison"
|
||||
MissileSite = "missile site"
|
||||
OffshoreStrikeTarget = "offshore strike target"
|
||||
Sam = "SAM"
|
||||
Ship = "ship"
|
||||
Shorad = "SHORAD"
|
||||
StrikeTarget = "strike target"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PresetLocations:
|
||||
"""Defines the preset locations loaded from the campaign mission file."""
|
||||
|
||||
#: Locations used for spawning ground defenses for bases.
|
||||
base_garrisons: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations used for spawning air defenses for bases. Used by SAMs, AAA,
|
||||
#: and SHORADs.
|
||||
base_air_defense: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations used by EWRs.
|
||||
ewrs: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations used by non-carrier ships. Carriers and LHAs are not random.
|
||||
ships: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations used by coastal defenses.
|
||||
coastal_defenses: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations used by ground based strike objectives.
|
||||
strike_locations: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations used by offshore strike objectives.
|
||||
offshore_strike_locations: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations used by missile sites like scuds and V-2s.
|
||||
missile_sites: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations of long range SAMs which should always be spawned.
|
||||
required_long_range_sams: List[Point] = field(default_factory=list)
|
||||
|
||||
#: Locations of medium range SAMs which should always be spawned.
|
||||
required_medium_range_sams: List[Point] = field(default_factory=list)
|
||||
|
||||
@staticmethod
|
||||
def _random_from(points: List[Point]) -> Optional[Point]:
|
||||
"""Finds, removes, and returns a random position from the given list."""
|
||||
if not points:
|
||||
return None
|
||||
point = random.choice(points)
|
||||
points.remove(point)
|
||||
return point
|
||||
|
||||
def random_for(self, location_type: LocationType) -> Optional[Point]:
|
||||
"""Returns a position suitable for the given location type.
|
||||
|
||||
The location, if found, will be claimed by the caller and not available
|
||||
to subsequent calls.
|
||||
"""
|
||||
if location_type == LocationType.BaseAirDefense:
|
||||
return self._random_from(self.base_air_defense)
|
||||
if location_type == LocationType.Coastal:
|
||||
return self._random_from(self.coastal_defenses)
|
||||
if location_type == LocationType.Ewr:
|
||||
return self._random_from(self.ewrs)
|
||||
if location_type == LocationType.Garrison:
|
||||
return self._random_from(self.base_garrisons)
|
||||
if location_type == LocationType.MissileSite:
|
||||
return self._random_from(self.missile_sites)
|
||||
if location_type == LocationType.OffshoreStrikeTarget:
|
||||
return self._random_from(self.offshore_strike_locations)
|
||||
if location_type == LocationType.Sam:
|
||||
return self._random_from(self.strike_locations)
|
||||
if location_type == LocationType.Ship:
|
||||
return self._random_from(self.ships)
|
||||
if location_type == LocationType.Shorad:
|
||||
return self._random_from(self.base_garrisons)
|
||||
if location_type == LocationType.StrikeTarget:
|
||||
return self._random_from(self.strike_locations)
|
||||
logging.error(f"Unknown location type: {location_type}")
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PendingOccupancy:
|
||||
present: int
|
||||
ordered: int
|
||||
transferring: int
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
return self.present + self.ordered + self.transferring
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunwayStatus:
|
||||
damaged: bool = False
|
||||
repair_turns_remaining: Optional[int] = None
|
||||
|
||||
def damage(self) -> None:
|
||||
self.damaged = True
|
||||
# If the runway is already under repair and is damaged again, progress
|
||||
# is reset.
|
||||
self.repair_turns_remaining = None
|
||||
|
||||
def begin_repair(self) -> None:
|
||||
if self.repair_turns_remaining is not None:
|
||||
logging.error("Runway already under repair. Restarting.")
|
||||
self.repair_turns_remaining = 4
|
||||
|
||||
def process_turn(self) -> None:
|
||||
if self.repair_turns_remaining is not None:
|
||||
if self.repair_turns_remaining == 1:
|
||||
self.repair_turns_remaining = None
|
||||
self.damaged = False
|
||||
else:
|
||||
self.repair_turns_remaining -= 1
|
||||
|
||||
@property
|
||||
def needs_repair(self) -> bool:
|
||||
return self.damaged and self.repair_turns_remaining is None
|
||||
|
||||
def __str__(self) -> str:
|
||||
if not self.damaged:
|
||||
return "Runway operational"
|
||||
|
||||
turns_remaining = self.repair_turns_remaining
|
||||
if turns_remaining is None:
|
||||
return "Runway damaged"
|
||||
|
||||
return f"Runway repairing, {turns_remaining} turns remaining"
|
||||
|
||||
|
||||
@total_ordering
|
||||
class GroundUnitDestination:
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
self.control_point = control_point
|
||||
|
||||
@property
|
||||
def total_value(self) -> float:
|
||||
return self.control_point.base.total_armor_value
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not isinstance(other, GroundUnitDestination):
|
||||
raise TypeError
|
||||
|
||||
return self.total_value == other.total_value
|
||||
|
||||
def __lt__(self, other: Any) -> bool:
|
||||
if not isinstance(other, GroundUnitDestination):
|
||||
raise TypeError
|
||||
|
||||
return self.total_value < other.total_value
|
||||
|
||||
|
||||
class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
position = None # type: Point
|
||||
name = None # type: str
|
||||
|
||||
captured = False
|
||||
has_frontline = True
|
||||
|
||||
alt = 0
|
||||
|
||||
# TODO: Only airbases have IDs.
|
||||
# TODO: has_frontline is only reasonable for airbases.
|
||||
# TODO: cptype is obsolete.
|
||||
def __init__(self, cp_id: int, name: str, position: Point,
|
||||
at: db.StartingPosition, size: int,
|
||||
importance: float, has_frontline=True,
|
||||
cptype=ControlPointType.AIRBASE):
|
||||
super().__init__(name, position)
|
||||
# TODO: Should be Airbase specific.
|
||||
self.id = cp_id
|
||||
self.full_name = name
|
||||
self.at = at
|
||||
self.connected_objectives: List[TheaterGroundObject] = []
|
||||
self.base_defenses: List[BaseDefenseGroundObject] = []
|
||||
self.preset_locations = PresetLocations()
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
self.size = size
|
||||
self.importance = importance
|
||||
self.captured = False
|
||||
self.captured_invert = False
|
||||
# TODO: Should be Airbase specific.
|
||||
self.has_frontline = has_frontline
|
||||
self.connected_points: List[ControlPoint] = []
|
||||
self.base: Base = Base()
|
||||
self.cptype = cptype
|
||||
# TODO: Should be Airbase specific.
|
||||
self.stances: Dict[int, CombatStance] = {}
|
||||
from ..event import UnitsDeliveryEvent
|
||||
self.pending_unit_deliveries = UnitsDeliveryEvent(self)
|
||||
|
||||
self.target_position: Optional[Point] = None
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{__class__}: {self.name}>"
|
||||
|
||||
@property
|
||||
def ground_objects(self) -> List[TheaterGroundObject]:
|
||||
return list(
|
||||
itertools.chain(self.connected_objectives, self.base_defenses))
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def heading(self) -> int:
|
||||
...
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def is_global(self):
|
||||
return not self.connected_points
|
||||
|
||||
@property
|
||||
def is_carrier(self):
|
||||
"""
|
||||
:return: Whether this control point is an aircraft carrier
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_fleet(self):
|
||||
"""
|
||||
:return: Whether this control point is a boat (mobile)
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_lha(self):
|
||||
"""
|
||||
:return: Whether this control point is an LHA
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def moveable(self) -> bool:
|
||||
"""
|
||||
:return: Whether this control point can be moved around
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def total_aircraft_parking(self):
|
||||
"""
|
||||
:return: The maximum number of aircraft that can be stored in this
|
||||
control point
|
||||
"""
|
||||
...
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def connect(self, to: ControlPoint) -> None:
|
||||
self.connected_points.append(to)
|
||||
self.stances[to.id] = CombatStance.DEFENSIVE
|
||||
|
||||
@abstractmethod
|
||||
def runway_is_operational(self) -> bool:
|
||||
"""
|
||||
Check whether this control point supports taking offs and landings.
|
||||
:return:
|
||||
"""
|
||||
...
|
||||
|
||||
# TODO: Should be naval specific.
|
||||
def get_carrier_group_name(self):
|
||||
"""
|
||||
Get the carrier group name if the airbase is a carrier
|
||||
:return: Carrier group name
|
||||
"""
|
||||
if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||
ControlPointType.LHA_GROUP]:
|
||||
for g in self.ground_objects:
|
||||
if g.dcs_identifier == "CARRIER":
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
if db.unit_type_from_name(u.type) in [
|
||||
CVN_74_John_C__Stennis,
|
||||
CV_1143_5_Admiral_Kuznetsov]:
|
||||
return group.name
|
||||
elif g.dcs_identifier == "LHA":
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
if db.unit_type_from_name(u.type) in [LHA_1_Tarawa]:
|
||||
return group.name
|
||||
return None
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def is_connected(self, to) -> bool:
|
||||
return to in self.connected_points
|
||||
|
||||
def find_ground_objects_by_obj_name(self, obj_name):
|
||||
found = []
|
||||
for g in self.ground_objects:
|
||||
if g.obj_name == obj_name:
|
||||
found.append(g)
|
||||
return found
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
return self.captured == to_player
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def clear_base_defenses(self) -> None:
|
||||
for base_defense in self.base_defenses:
|
||||
if isinstance(base_defense, EwrGroundObject):
|
||||
self.preset_locations.ewrs.append(base_defense.position)
|
||||
elif isinstance(base_defense, SamGroundObject):
|
||||
self.preset_locations.base_air_defense.append(
|
||||
base_defense.position)
|
||||
elif isinstance(base_defense, VehicleGroupGroundObject):
|
||||
self.preset_locations.base_garrisons.append(
|
||||
base_defense.position)
|
||||
else:
|
||||
logging.error(
|
||||
"Could not determine preset location type for "
|
||||
f"{base_defense}. Assuming garrison type.")
|
||||
self.preset_locations.base_garrisons.append(
|
||||
base_defense.position)
|
||||
self.base_defenses = []
|
||||
|
||||
def capture_equipment(self, game: Game) -> None:
|
||||
total = self.base.total_armor_value
|
||||
self.base.armor.clear()
|
||||
game.adjust_budget(total, player=not self.captured)
|
||||
game.message(
|
||||
f"{self.name} is not connected to any friendly points. Ground "
|
||||
f"vehicles have been captured and sold for ${total}M.")
|
||||
|
||||
def retreat_ground_units(self, game: Game):
|
||||
# When there are multiple valid destinations, deliver units to whichever
|
||||
# base is least defended first. The closest approximation of unit
|
||||
# strength we have is price
|
||||
destinations = [GroundUnitDestination(cp)
|
||||
for cp in self.connected_points
|
||||
if cp.captured == self.captured]
|
||||
if not destinations:
|
||||
self.capture_equipment(game)
|
||||
return
|
||||
|
||||
heapq.heapify(destinations)
|
||||
destination = heapq.heappop(destinations)
|
||||
while self.base.armor:
|
||||
unit_type, count = self.base.armor.popitem()
|
||||
for _ in range(count):
|
||||
destination.control_point.base.commision_units({unit_type: 1})
|
||||
destination = heapq.heappushpop(destinations, destination)
|
||||
|
||||
def capture_aircraft(self, game: Game, airframe: Type[FlyingType],
|
||||
count: int) -> None:
|
||||
try:
|
||||
value = PRICES[airframe] * count
|
||||
except KeyError:
|
||||
logging.exception(f"Unknown price for {airframe.id}")
|
||||
return
|
||||
|
||||
game.adjust_budget(value, player=not self.captured)
|
||||
game.message(
|
||||
f"No valid retreat destination in range of {self.name} for "
|
||||
f"{airframe.id}. {count} aircraft have been captured and sold for "
|
||||
f"${value}M.")
|
||||
|
||||
def aircraft_retreat_destination(
|
||||
self, game: Game,
|
||||
airframe: Type[FlyingType]) -> Optional[ControlPoint]:
|
||||
closest = ObjectiveDistanceCache.get_closest_airfields(self)
|
||||
# TODO: Should be airframe dependent.
|
||||
max_retreat_distance = nautical_miles(200)
|
||||
# Skip the first airbase because that's the airbase we're retreating
|
||||
# from.
|
||||
airfields = list(closest.airfields_within(max_retreat_distance))[1:]
|
||||
for airbase in airfields:
|
||||
if not airbase.can_operate(airframe):
|
||||
continue
|
||||
if airbase.captured != self.captured:
|
||||
continue
|
||||
if airbase.unclaimed_parking(game) > 0:
|
||||
return airbase
|
||||
return None
|
||||
|
||||
def _retreat_air_units(self, game: Game, airframe: Type[FlyingType],
|
||||
count: int) -> None:
|
||||
while count:
|
||||
logging.debug(f"Retreating {count} {airframe.id} from {self.name}")
|
||||
destination = self.aircraft_retreat_destination(game, airframe)
|
||||
if destination is None:
|
||||
self.capture_aircraft(game, airframe, count)
|
||||
return
|
||||
parking = destination.unclaimed_parking(game)
|
||||
transfer_amount = min([parking, count])
|
||||
destination.base.commision_units({airframe: transfer_amount})
|
||||
count -= transfer_amount
|
||||
|
||||
def retreat_air_units(self, game: Game) -> None:
|
||||
# TODO: Capture in order of price to retain maximum value?
|
||||
while self.base.aircraft:
|
||||
airframe, count = self.base.aircraft.popitem()
|
||||
self._retreat_air_units(game, airframe, count)
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
self.pending_unit_deliveries.refund_all(game)
|
||||
self.retreat_ground_units(game)
|
||||
self.retreat_air_units(game)
|
||||
|
||||
if for_player:
|
||||
self.captured = True
|
||||
else:
|
||||
self.captured = False
|
||||
|
||||
self.base.set_strength_to_minimum()
|
||||
|
||||
self.clear_base_defenses()
|
||||
from .start_generator import BaseDefenseGenerator
|
||||
BaseDefenseGenerator(game, self).generate()
|
||||
|
||||
@abstractmethod
|
||||
def can_operate(self, aircraft: Type[FlyingType]) -> bool:
|
||||
...
|
||||
|
||||
def aircraft_transferring(self, game: Game) -> int:
|
||||
if self.captured:
|
||||
ato = game.blue_ato
|
||||
else:
|
||||
ato = game.red_ato
|
||||
|
||||
total = 0
|
||||
for package in ato.packages:
|
||||
for flight in package.flights:
|
||||
if flight.departure == flight.arrival:
|
||||
continue
|
||||
if flight.departure == self:
|
||||
total -= flight.count
|
||||
elif flight.arrival == self:
|
||||
total += flight.count
|
||||
return total
|
||||
|
||||
def expected_aircraft_next_turn(self, game: Game) -> PendingOccupancy:
|
||||
on_order = 0
|
||||
for unit_bought in self.pending_unit_deliveries.units:
|
||||
if issubclass(unit_bought, FlyingType):
|
||||
on_order += self.pending_unit_deliveries.units[unit_bought]
|
||||
|
||||
return PendingOccupancy(self.base.total_aircraft, on_order,
|
||||
self.aircraft_transferring(game))
|
||||
|
||||
def unclaimed_parking(self, game: Game) -> int:
|
||||
return (self.total_aircraft_parking -
|
||||
self.expected_aircraft_next_turn(game).total)
|
||||
|
||||
@abstractmethod
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
...
|
||||
|
||||
@property
|
||||
def parking_slots(self) -> Iterator[ParkingSlot]:
|
||||
yield from []
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
...
|
||||
|
||||
@property
|
||||
def runway_can_be_repaired(self) -> bool:
|
||||
return self.runway_status.needs_repair
|
||||
|
||||
def begin_runway_repair(self) -> None:
|
||||
if not self.runway_can_be_repaired:
|
||||
logging.error(f"Cannot repair runway at {self}")
|
||||
return
|
||||
self.runway_status.begin_repair()
|
||||
|
||||
def process_turn(self, game: Game) -> None:
|
||||
self.pending_unit_deliveries.process(game)
|
||||
|
||||
runway_status = self.runway_status
|
||||
if runway_status is not None:
|
||||
runway_status.process_turn()
|
||||
|
||||
# Process movements for ships control points group
|
||||
if self.target_position is not None:
|
||||
delta = self.target_position - self.position
|
||||
self.position = self.target_position
|
||||
self.target_position = None
|
||||
|
||||
# Move the linked unit groups
|
||||
for ground_object in self.ground_objects:
|
||||
if isinstance(ground_object, GenericCarrierGroundObject):
|
||||
ground_object.position.x = ground_object.position.x + delta.x
|
||||
ground_object.position.y = ground_object.position.y + delta.y
|
||||
for group in ground_object.groups:
|
||||
for u in group.units:
|
||||
u.position.x = u.position.x + delta.x
|
||||
u.position.y = u.position.y + delta.y
|
||||
|
||||
@property
|
||||
def pending_frontline_aa_deliveries_count(self):
|
||||
"""
|
||||
Get number of pending frontline aa units
|
||||
"""
|
||||
if self.pending_unit_deliveries:
|
||||
return sum([v for k,v in self.pending_unit_deliveries.units.items() if k in TYPE_SHORAD])
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def pending_deliveries_count(self):
|
||||
"""
|
||||
Get number of pending units
|
||||
"""
|
||||
if self.pending_unit_deliveries:
|
||||
return sum([v for k, v in self.pending_unit_deliveries.units.items()])
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def expected_ground_units_next_turn(self) -> PendingOccupancy:
|
||||
on_order = 0
|
||||
for unit_bought in self.pending_unit_deliveries.units:
|
||||
if issubclass(unit_bought, FlyingType):
|
||||
continue
|
||||
if unit_bought in TYPE_SHORAD:
|
||||
continue
|
||||
on_order += self.pending_unit_deliveries.units[unit_bought]
|
||||
|
||||
return PendingOccupancy(self.base.total_armor, on_order,
|
||||
# Ground unit transfers not yet implemented.
|
||||
transferring=0)
|
||||
|
||||
@property
|
||||
def income_per_turn(self) -> int:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def has_active_frontline(self) -> bool:
|
||||
return any(
|
||||
not c.is_friendly(self.captured) for c in self.connected_points)
|
||||
|
||||
class Airfield(ControlPoint):
|
||||
|
||||
def __init__(self, airport: Airport, size: int,
|
||||
importance: float, has_frontline=True):
|
||||
super().__init__(airport.id, airport.name, airport.position, airport,
|
||||
size, importance, has_frontline,
|
||||
cptype=ControlPointType.AIRBASE)
|
||||
self.airport = airport
|
||||
self._runway_status = RunwayStatus()
|
||||
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
# TODO: Allow helicopters.
|
||||
# Need to implement ground spawns so the helos don't use the runway.
|
||||
# TODO: Allow harrier.
|
||||
# Needs ground spawns just like helos do, but also need to be able to
|
||||
# limit takeoff weight to ~20500 lbs or it won't be able to take off.
|
||||
return self.runway_is_operational()
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: FlightType.LOGISTICS
|
||||
]
|
||||
else:
|
||||
yield from [
|
||||
FlightType.OCA_AIRCRAFT,
|
||||
FlightType.OCA_RUNWAY,
|
||||
]
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return len(self.airport.parking_slots)
|
||||
|
||||
@property
|
||||
def heading(self) -> int:
|
||||
return self.airport.runways[0].heading
|
||||
|
||||
def runway_is_operational(self) -> bool:
|
||||
return not self.runway_status.damaged
|
||||
|
||||
@property
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
return self._runway_status
|
||||
|
||||
def damage_runway(self) -> None:
|
||||
self.runway_status.damage()
|
||||
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
assigner = RunwayAssigner(conditions)
|
||||
return assigner.get_preferred_runway(self.airport)
|
||||
|
||||
@property
|
||||
def parking_slots(self) -> Iterator[ParkingSlot]:
|
||||
yield from self.airport.parking_slots
|
||||
|
||||
@property
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def income_per_turn(self) -> int:
|
||||
return 20
|
||||
|
||||
|
||||
class NavalControlPoint(ControlPoint, ABC):
|
||||
|
||||
@property
|
||||
def is_fleet(self) -> bool:
|
||||
return True
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: Buddy tanking for the A-4?
|
||||
# TODO: Rescue chopper?
|
||||
# TODO: Inter-ship logistics?
|
||||
]
|
||||
else:
|
||||
yield FlightType.ANTISHIP
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def heading(self) -> int:
|
||||
return 0 # TODO compute heading
|
||||
|
||||
def runway_is_operational(self) -> bool:
|
||||
# Necessary because it's possible for the carrier itself to have sunk
|
||||
# while its escorts are still alive.
|
||||
for g in self.ground_objects:
|
||||
if g.dcs_identifier in ["CARRIER", "LHA"]:
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
if db.unit_type_from_name(u.type) in [
|
||||
CVN_74_John_C__Stennis, LHA_1_Tarawa,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Type_071_Amphibious_Transport_Dock]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
# TODO: Assign TACAN and ICLS earlier so we don't need this.
|
||||
fallback = RunwayData(self.full_name, runway_heading=0, runway_name="")
|
||||
return dynamic_runways.get(self.name, fallback)
|
||||
|
||||
@property
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
return RunwayStatus(damaged=not self.runway_is_operational())
|
||||
|
||||
@property
|
||||
def runway_can_be_repaired(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def moveable(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class Carrier(NavalControlPoint):
|
||||
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
import game.theater.conflicttheater
|
||||
super().__init__(cp_id, name, at, at,
|
||||
game.theater.conflicttheater.SIZE_SMALL, 1,
|
||||
has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP)
|
||||
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
raise RuntimeError("Carriers cannot be captured")
|
||||
|
||||
@property
|
||||
def is_carrier(self):
|
||||
return True
|
||||
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
return aircraft in db.CARRIER_CAPABLE
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return 90
|
||||
|
||||
|
||||
class Lha(NavalControlPoint):
|
||||
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
import game.theater.conflicttheater
|
||||
super().__init__(cp_id, name, at, at,
|
||||
game.theater.conflicttheater.SIZE_SMALL, 1,
|
||||
has_frontline=False, cptype=ControlPointType.LHA_GROUP)
|
||||
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
raise RuntimeError("LHAs cannot be captured")
|
||||
|
||||
@property
|
||||
def is_lha(self) -> bool:
|
||||
return True
|
||||
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
return aircraft in db.LHA_CAPABLE
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return 20
|
||||
|
||||
|
||||
class OffMapSpawn(ControlPoint):
|
||||
|
||||
def runway_is_operational(self) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(self, cp_id: int, name: str, position: Point):
|
||||
from . import IMPORTANCE_MEDIUM, SIZE_REGULAR
|
||||
super().__init__(cp_id, name, position, at=position,
|
||||
size=SIZE_REGULAR, importance=IMPORTANCE_MEDIUM,
|
||||
has_frontline=False, cptype=ControlPointType.OFF_MAP)
|
||||
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
raise RuntimeError("Off map control points cannot be captured")
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
yield from []
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return 1000
|
||||
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def heading(self) -> int:
|
||||
return 0
|
||||
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
logging.warning("TODO: Off map spawns have no runways.")
|
||||
return RunwayData(self.full_name, runway_heading=0, runway_name="")
|
||||
|
||||
@property
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
return RunwayStatus()
|
||||
|
||||
@property
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class Fob(ControlPoint):
|
||||
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
import game.theater.conflicttheater
|
||||
super().__init__(cp_id, name, at, at,
|
||||
game.theater.conflicttheater.SIZE_SMALL, 1,
|
||||
has_frontline=True, cptype=ControlPointType.FOB)
|
||||
self.name = name
|
||||
|
||||
def runway_is_operational(self) -> bool:
|
||||
return False
|
||||
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
logging.warning("TODO: FOBs have no runways.")
|
||||
return RunwayData(self.full_name, runway_heading=0, runway_name="")
|
||||
|
||||
@property
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
return RunwayStatus()
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.BARCAP,
|
||||
# TODO: FlightType.LOGISTICS
|
||||
]
|
||||
else:
|
||||
yield from [
|
||||
FlightType.STRIKE,
|
||||
FlightType.SWEEP,
|
||||
FlightType.ESCORT,
|
||||
FlightType.SEAD,
|
||||
]
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return 0
|
||||
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def heading(self) -> int:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def income_per_turn(self) -> int:
|
||||
return 10
|
||||
49
game/theater/landmap.py
Normal file
49
game/theater/landmap.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from dataclasses import dataclass
|
||||
import pickle
|
||||
from functools import cached_property
|
||||
from typing import Optional, Tuple, Union
|
||||
import logging
|
||||
|
||||
from shapely import geometry
|
||||
from shapely.geometry import MultiPolygon, Polygon
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Landmap:
|
||||
inclusion_zones: MultiPolygon
|
||||
exclusion_zones: MultiPolygon
|
||||
sea_zones: MultiPolygon
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.inclusion_zones.is_valid:
|
||||
raise RuntimeError("Inclusion zones not valid")
|
||||
if not self.exclusion_zones.is_valid:
|
||||
raise RuntimeError("Exclusion zones not valid")
|
||||
if not self.sea_zones.is_valid:
|
||||
raise RuntimeError("Sea zones not valid")
|
||||
|
||||
@cached_property
|
||||
def inclusion_zone_only(self) -> MultiPolygon:
|
||||
return self.inclusion_zones - self.exclusion_zones - self.sea_zones
|
||||
|
||||
|
||||
def load_landmap(filename: str) -> Optional[Landmap]:
|
||||
try:
|
||||
with open(filename, "rb") as f:
|
||||
return pickle.load(f)
|
||||
except:
|
||||
logging.exception(f"Failed to load landmap {filename}")
|
||||
return None
|
||||
|
||||
|
||||
def poly_contains(x, y, poly: Union[MultiPolygon, Polygon]):
|
||||
return poly.contains(geometry.Point(x, y))
|
||||
|
||||
|
||||
def poly_centroid(poly) -> Tuple[float, float]:
|
||||
x_list = [vertex[0] for vertex in poly]
|
||||
y_list = [vertex[1] for vertex in poly]
|
||||
x = sum(x_list) / len(poly)
|
||||
y = sum(y_list) / len(poly)
|
||||
return (x, y)
|
||||
|
||||
43
game/theater/missiontarget.py
Normal file
43
game/theater/missiontarget.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterator, TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
class MissionTarget:
|
||||
def __init__(self, name: str, position: Point) -> None:
|
||||
"""Initializes a mission target.
|
||||
|
||||
Args:
|
||||
name: The name of the mission target.
|
||||
position: The location of the mission target.
|
||||
"""
|
||||
self.name = name
|
||||
self.position = position
|
||||
|
||||
def distance_to(self, other: MissionTarget) -> int:
|
||||
"""Computes the distance to the given mission target."""
|
||||
return self.position.distance_to_point(other.position)
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
"""Returns True if the objective is in friendly territory."""
|
||||
raise NotImplementedError
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if self.is_friendly(for_player):
|
||||
yield FlightType.BARCAP
|
||||
else:
|
||||
yield from [
|
||||
FlightType.ESCORT,
|
||||
FlightType.TARCAP,
|
||||
FlightType.SEAD,
|
||||
FlightType.SWEEP,
|
||||
# TODO: FlightType.ELINT,
|
||||
# TODO: FlightType.EWAR,
|
||||
# TODO: FlightType.RECON,
|
||||
]
|
||||
739
game/theater/start_generator.py
Normal file
739
game/theater/start_generator.py
Normal file
@@ -0,0 +1,739 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import pickle
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from game import Game, db
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import Carrier, Lha, LocationType
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
EwrGroundObject,
|
||||
LhaGroundObject,
|
||||
MissileSiteGroundObject,
|
||||
SamGroundObject,
|
||||
ShipGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
)
|
||||
from game.version import VERSION
|
||||
from gen import namegen
|
||||
from gen.defenses.armor_group_generator import generate_armor_group
|
||||
from gen.fleet.ship_group_generator import (
|
||||
generate_carrier_group,
|
||||
generate_lha_group,
|
||||
generate_ship_group,
|
||||
)
|
||||
from gen.locations.preset_location_finder import MizDataLocationFinder
|
||||
from gen.missiles.missiles_group_generator import generate_missile_group
|
||||
from gen.sam.airdefensegroupgenerator import AirDefenseRange
|
||||
from gen.sam.sam_group_generator import (
|
||||
generate_anti_air_group,
|
||||
generate_ewr_group,
|
||||
)
|
||||
from . import (
|
||||
ConflictTheater,
|
||||
ControlPoint,
|
||||
ControlPointType,
|
||||
Fob,
|
||||
OffMapSpawn,
|
||||
)
|
||||
from ..settings import Settings
|
||||
|
||||
GroundObjectTemplates = Dict[str, Dict[str, Any]]
|
||||
|
||||
UNIT_VARIETY = 6
|
||||
UNIT_AMOUNT_FACTOR = 16
|
||||
UNIT_COUNT_IMPORTANCE_LOG = 1.3
|
||||
|
||||
COUNT_BY_TASK = {
|
||||
PinpointStrike: 12,
|
||||
CAP: 8,
|
||||
CAS: 4,
|
||||
AirDefence: 1,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GeneratorSettings:
|
||||
start_date: datetime
|
||||
player_budget: int
|
||||
enemy_budget: int
|
||||
midgame: bool
|
||||
inverted: bool
|
||||
no_carrier: bool
|
||||
no_lha: bool
|
||||
no_player_navy: bool
|
||||
no_enemy_navy: bool
|
||||
|
||||
|
||||
class GameGenerator:
|
||||
def __init__(self, player: str, enemy: str, theater: ConflictTheater,
|
||||
settings: Settings,
|
||||
generator_settings: GeneratorSettings) -> None:
|
||||
self.player = player
|
||||
self.enemy = enemy
|
||||
self.theater = theater
|
||||
self.settings = settings
|
||||
self.generator_settings = generator_settings
|
||||
|
||||
def generate(self) -> Game:
|
||||
# Reset name generator
|
||||
namegen.reset()
|
||||
self.prepare_theater()
|
||||
game = Game(
|
||||
player_name=self.player,
|
||||
enemy_name=self.enemy,
|
||||
theater=self.theater,
|
||||
start_date=self.generator_settings.start_date,
|
||||
settings=self.settings,
|
||||
player_budget=self.generator_settings.player_budget,
|
||||
enemy_budget=self.generator_settings.enemy_budget
|
||||
)
|
||||
|
||||
GroundObjectGenerator(game, self.generator_settings).generate()
|
||||
game.settings.version = VERSION
|
||||
return game
|
||||
|
||||
def prepare_theater(self) -> None:
|
||||
to_remove: List[ControlPoint] = []
|
||||
# Auto-capture half the bases if midgame.
|
||||
if self.generator_settings.midgame:
|
||||
control_points = self.theater.controlpoints
|
||||
for control_point in control_points[:len(control_points) // 2]:
|
||||
control_point.captured = True
|
||||
|
||||
# Remove carrier and lha, invert situation if needed
|
||||
for cp in self.theater.controlpoints:
|
||||
if isinstance(cp, Carrier) and self.generator_settings.no_carrier:
|
||||
to_remove.append(cp)
|
||||
elif isinstance(cp, Lha) and self.generator_settings.no_lha:
|
||||
to_remove.append(cp)
|
||||
|
||||
if self.generator_settings.inverted:
|
||||
cp.captured = cp.captured_invert
|
||||
|
||||
# do remove
|
||||
for cp in to_remove:
|
||||
self.theater.controlpoints.remove(cp)
|
||||
|
||||
# TODO: Fix this. This captures all bases for blue.
|
||||
# reapply midgame inverted if needed
|
||||
if self.generator_settings.midgame and self.generator_settings.inverted:
|
||||
for i, cp in enumerate(reversed(self.theater.controlpoints)):
|
||||
if i > len(self.theater.controlpoints):
|
||||
break
|
||||
else:
|
||||
cp.captured = True
|
||||
|
||||
|
||||
class LocationFinder:
|
||||
def __init__(self, game: Game, control_point: ControlPoint) -> None:
|
||||
self.game = game
|
||||
self.control_point = control_point
|
||||
self.miz_data = MizDataLocationFinder.compute_possible_locations(
|
||||
game.theater.terrain.name, control_point.full_name)
|
||||
|
||||
def location_for(self, location_type: LocationType) -> Optional[Point]:
|
||||
position = self.control_point.preset_locations.random_for(location_type)
|
||||
if position is not None:
|
||||
return position
|
||||
|
||||
logging.warning(f"No campaign location for %s at %s",
|
||||
location_type.value, self.control_point)
|
||||
position = self.random_from_miz_data(
|
||||
location_type == LocationType.OffshoreStrikeTarget)
|
||||
if position is not None:
|
||||
return position
|
||||
|
||||
logging.debug(f"No mizdata location for %s at %s", location_type.value,
|
||||
self.control_point)
|
||||
position = self.random_position(location_type)
|
||||
if position is not None:
|
||||
return position
|
||||
|
||||
logging.error(f"Could not find position for %s at %s",
|
||||
location_type.value, self.control_point)
|
||||
return None
|
||||
|
||||
def random_from_miz_data(self, offshore: bool) -> Optional[Point]:
|
||||
if offshore:
|
||||
locations = self.miz_data.offshore_locations
|
||||
else:
|
||||
locations = self.miz_data.ashore_locations
|
||||
if self.miz_data.offshore_locations:
|
||||
preset = random.choice(locations)
|
||||
locations.remove(preset)
|
||||
return preset.position
|
||||
return None
|
||||
|
||||
def random_position(self, location_type: LocationType) -> Optional[Point]:
|
||||
# TODO: Flesh out preset locations so we never hit this case.
|
||||
logging.warning("Falling back to random location for %s at %s",
|
||||
location_type.value, self.control_point)
|
||||
|
||||
is_base_defense = location_type in {
|
||||
LocationType.BaseAirDefense,
|
||||
LocationType.Garrison,
|
||||
LocationType.Shorad,
|
||||
}
|
||||
|
||||
on_land = location_type not in {
|
||||
LocationType.OffshoreStrikeTarget,
|
||||
LocationType.Ship,
|
||||
}
|
||||
|
||||
avoid_others = location_type not in {
|
||||
LocationType.Garrison,
|
||||
LocationType.MissileSite,
|
||||
LocationType.Sam,
|
||||
LocationType.Ship,
|
||||
LocationType.Shorad,
|
||||
}
|
||||
|
||||
if is_base_defense:
|
||||
min_range = 400
|
||||
max_range = 3200
|
||||
elif location_type == LocationType.Ship:
|
||||
min_range = 5000
|
||||
max_range = 40000
|
||||
elif location_type == LocationType.MissileSite:
|
||||
min_range = 2500
|
||||
max_range = 40000
|
||||
else:
|
||||
min_range = 10000
|
||||
max_range = 40000
|
||||
|
||||
position = self._find_random_position(min_range, max_range,
|
||||
on_land, is_base_defense,
|
||||
avoid_others)
|
||||
|
||||
# Retry once, searching a bit further (On some big airbases, 3200 is too
|
||||
# short (Ex : Incirlik)), but searching farther on every base would be
|
||||
# problematic, as some base defense units would end up very far away
|
||||
# from small airfields.
|
||||
if position is None and is_base_defense:
|
||||
position = self._find_random_position(3200, 4800,
|
||||
on_land, is_base_defense,
|
||||
avoid_others)
|
||||
return position
|
||||
|
||||
def _find_random_position(self, min_range: int, max_range: int,
|
||||
on_ground: bool, is_base_defense: bool,
|
||||
avoid_others: bool) -> Optional[Point]:
|
||||
"""
|
||||
Find a valid ground object location
|
||||
:param on_ground: Whether it should be on ground or on sea (True = on
|
||||
ground)
|
||||
:param min_range: Minimal range from point
|
||||
:param max_range: Max range from point
|
||||
:param is_base_defense: True if the location is for base defense.
|
||||
:return:
|
||||
"""
|
||||
near = self.control_point.position
|
||||
others = self.control_point.ground_objects
|
||||
|
||||
def is_valid(point: Optional[Point]) -> bool:
|
||||
if point is None:
|
||||
return False
|
||||
|
||||
if on_ground and not self.game.theater.is_on_land(point):
|
||||
return False
|
||||
elif not on_ground and not self.game.theater.is_in_sea(point):
|
||||
return False
|
||||
|
||||
if avoid_others:
|
||||
for other in others:
|
||||
if other.position.distance_to_point(point) < 10000:
|
||||
return False
|
||||
|
||||
if is_base_defense:
|
||||
# If it's a base defense we don't care how close it is to other
|
||||
# points.
|
||||
return True
|
||||
|
||||
# Else verify that it's not too close to another control point.
|
||||
for control_point in self.game.theater.controlpoints:
|
||||
if control_point != self.control_point:
|
||||
if control_point.position.distance_to_point(point) < 30000:
|
||||
return False
|
||||
for ground_obj in control_point.ground_objects:
|
||||
if ground_obj.position.distance_to_point(point) < 10000:
|
||||
return False
|
||||
return True
|
||||
|
||||
for _ in range(300):
|
||||
# Check if on land or sea
|
||||
p = near.random_point_within(max_range, min_range)
|
||||
if is_valid(p):
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
class ControlPointGroundObjectGenerator:
|
||||
def __init__(self, game: Game, generator_settings: GeneratorSettings,
|
||||
control_point: ControlPoint) -> None:
|
||||
self.game = game
|
||||
self.generator_settings = generator_settings
|
||||
self.control_point = control_point
|
||||
self.location_finder = LocationFinder(game, control_point)
|
||||
|
||||
@property
|
||||
def faction_name(self) -> str:
|
||||
if self.control_point.captured:
|
||||
return self.game.player_name
|
||||
else:
|
||||
return self.game.enemy_name
|
||||
|
||||
@property
|
||||
def faction(self) -> Faction:
|
||||
return db.FACTIONS[self.faction_name]
|
||||
|
||||
def generate(self) -> bool:
|
||||
self.control_point.connected_objectives = []
|
||||
if self.faction.navy_generators:
|
||||
# Even airbases can generate navies if they are close enough to the
|
||||
# water. This is not controlled by the control point definition, but
|
||||
# rather by whether or not the generator can find a valid position
|
||||
# for the ship.
|
||||
self.generate_navy()
|
||||
|
||||
return True
|
||||
|
||||
def generate_navy(self) -> None:
|
||||
skip_player_navy = self.generator_settings.no_player_navy
|
||||
if self.control_point.captured and skip_player_navy:
|
||||
return
|
||||
|
||||
skip_enemy_navy = self.generator_settings.no_enemy_navy
|
||||
if not self.control_point.captured and skip_enemy_navy:
|
||||
return
|
||||
|
||||
for _ in range(self.faction.navy_group_count):
|
||||
self.generate_ship()
|
||||
|
||||
def generate_ship(self) -> None:
|
||||
point = self.location_finder.location_for(
|
||||
LocationType.OffshoreStrikeTarget)
|
||||
if point is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = ShipGroundObject(namegen.random_objective_name(), group_id, point,
|
||||
self.control_point)
|
||||
|
||||
group = generate_ship_group(self.game, g, self.faction_name)
|
||||
g.groups = []
|
||||
if group is not None:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
|
||||
class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
def generate(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
def generate(self) -> bool:
|
||||
if not super().generate():
|
||||
return False
|
||||
|
||||
carrier_names = self.faction.carrier_names
|
||||
if not carrier_names:
|
||||
logging.info(
|
||||
f"Skipping generation of {self.control_point.name} because "
|
||||
f"{self.faction_name} has no carriers")
|
||||
return False
|
||||
|
||||
# Create ground object group
|
||||
group_id = self.game.next_group_id()
|
||||
g = CarrierGroundObject(namegen.random_objective_name(), group_id,
|
||||
self.control_point)
|
||||
group = generate_carrier_group(self.faction_name, self.game, g)
|
||||
g.groups = []
|
||||
if group is not None:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
self.control_point.name = random.choice(carrier_names)
|
||||
return True
|
||||
|
||||
|
||||
class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
def generate(self) -> bool:
|
||||
if not super().generate():
|
||||
return False
|
||||
|
||||
lha_names = self.faction.helicopter_carrier_names
|
||||
if not lha_names:
|
||||
logging.info(
|
||||
f"Skipping generation of {self.control_point.name} because "
|
||||
f"{self.faction_name} has no LHAs")
|
||||
return False
|
||||
|
||||
# Create ground object group
|
||||
group_id = self.game.next_group_id()
|
||||
g = LhaGroundObject(namegen.random_objective_name(), group_id,
|
||||
self.control_point)
|
||||
group = generate_lha_group(self.faction_name, self.game, g)
|
||||
g.groups = []
|
||||
if group is not None:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
self.control_point.name = random.choice(lha_names)
|
||||
return True
|
||||
|
||||
|
||||
class BaseDefenseGenerator:
|
||||
def __init__(self, game: Game, control_point: ControlPoint) -> None:
|
||||
self.game = game
|
||||
self.control_point = control_point
|
||||
self.location_finder = LocationFinder(game, control_point)
|
||||
|
||||
@property
|
||||
def faction_name(self) -> str:
|
||||
if self.control_point.captured:
|
||||
return self.game.player_name
|
||||
else:
|
||||
return self.game.enemy_name
|
||||
|
||||
@property
|
||||
def faction(self) -> Faction:
|
||||
return db.FACTIONS[self.faction_name]
|
||||
|
||||
def generate(self) -> None:
|
||||
self.generate_ewr()
|
||||
self.generate_garrison()
|
||||
self.generate_base_defenses()
|
||||
|
||||
def generate_ewr(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Ewr)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = EwrGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point)
|
||||
|
||||
group = generate_ewr_group(self.game, g, self.faction)
|
||||
if group is None:
|
||||
logging.error(f"Could not generate EWR at {self.control_point}")
|
||||
return
|
||||
|
||||
g.groups = [group]
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_base_defenses(self) -> None:
|
||||
# First group has a 1/2 chance of being a SAM, 1/6 chance of SHORAD,
|
||||
# and a 1/6 chance of a garrison.
|
||||
#
|
||||
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
|
||||
# being a garrison.
|
||||
for i in range(random.randint(2, 5)):
|
||||
if i == 0 and random.randint(0, 1) == 0:
|
||||
self.generate_sam()
|
||||
elif random.randint(0, 2) == 1:
|
||||
self.generate_shorad()
|
||||
else:
|
||||
self.generate_garrison()
|
||||
|
||||
def generate_garrison(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Garrison)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = VehicleGroupGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point,
|
||||
for_airbase=True)
|
||||
|
||||
group = generate_armor_group(self.faction_name, self.game, g)
|
||||
if group is None:
|
||||
logging.error(
|
||||
f"Could not generate garrison at {self.control_point}")
|
||||
return
|
||||
g.groups.append(group)
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_sam(self) -> None:
|
||||
position = self.location_finder.location_for(
|
||||
LocationType.BaseAirDefense)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point, for_airbase=True)
|
||||
|
||||
groups = generate_anti_air_group(self.game, g, self.faction)
|
||||
if not groups:
|
||||
logging.error(f"Could not generate SAM at {self.control_point}")
|
||||
return
|
||||
g.groups = groups
|
||||
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)
|
||||
|
||||
groups = generate_anti_air_group(self.game, g, self.faction,
|
||||
ranges=[{AirDefenseRange.Short}])
|
||||
if not groups:
|
||||
logging.error(
|
||||
f"Could not generate SHORAD group at {self.control_point}")
|
||||
return
|
||||
g.groups = groups
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
|
||||
class FobDefenseGenerator(BaseDefenseGenerator):
|
||||
def generate(self) -> None:
|
||||
self.generate_garrison()
|
||||
self.generate_fob_defenses()
|
||||
|
||||
def generate_fob_defenses(self):
|
||||
# First group has a 1/2 chance of being a SHORAD,
|
||||
# and a 1/2 chance of a garrison.
|
||||
#
|
||||
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
|
||||
# being a garrison.
|
||||
for i in range(random.randint(2, 5)):
|
||||
if i == 0 and random.randint(0, 1) == 0:
|
||||
self.generate_shorad()
|
||||
elif i == 0 and random.randint(0, 1) == 0:
|
||||
self.generate_garrison()
|
||||
elif random.randint(0, 2) == 1:
|
||||
self.generate_shorad()
|
||||
else:
|
||||
self.generate_garrison()
|
||||
|
||||
|
||||
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
def __init__(self, game: Game, generator_settings: GeneratorSettings,
|
||||
control_point: ControlPoint,
|
||||
templates: GroundObjectTemplates) -> None:
|
||||
super().__init__(game, generator_settings, control_point)
|
||||
self.templates = templates
|
||||
|
||||
def generate(self) -> bool:
|
||||
if not super().generate():
|
||||
return False
|
||||
|
||||
BaseDefenseGenerator(self.game, self.control_point).generate()
|
||||
self.generate_ground_points()
|
||||
|
||||
if self.faction.missiles:
|
||||
self.generate_missile_sites()
|
||||
|
||||
return True
|
||||
|
||||
def generate_ground_points(self) -> None:
|
||||
"""Generate ground objects and AA sites for the control point."""
|
||||
skip_sams = self.generate_required_aa()
|
||||
|
||||
if self.control_point.is_global:
|
||||
return
|
||||
|
||||
# Always generate at least one AA point.
|
||||
self.generate_aa_site()
|
||||
|
||||
# And between 2 and 7 other objectives.
|
||||
amount = random.randrange(2, 7)
|
||||
for i in range(amount):
|
||||
# 1 in 4 additional objectives are AA.
|
||||
if random.randint(0, 3) == 0:
|
||||
if skip_sams > 0:
|
||||
skip_sams -= 1
|
||||
else:
|
||||
self.generate_aa_site()
|
||||
else:
|
||||
self.generate_ground_point()
|
||||
|
||||
def generate_required_aa(self) -> int:
|
||||
"""Generates the AA sites that are required by the campaign.
|
||||
|
||||
Returns:
|
||||
The number of AA sites that were generated.
|
||||
"""
|
||||
presets = self.control_point.preset_locations
|
||||
for position in presets.required_long_range_sams:
|
||||
self.generate_aa_at(position, ranges=[
|
||||
{AirDefenseRange.Long},
|
||||
{AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
])
|
||||
for position in presets.required_medium_range_sams:
|
||||
self.generate_aa_at(position, ranges=[
|
||||
{AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
])
|
||||
return (len(presets.required_long_range_sams) +
|
||||
len(presets.required_medium_range_sams))
|
||||
|
||||
def generate_ground_point(self) -> None:
|
||||
try:
|
||||
category = random.choice(self.faction.building_set)
|
||||
except IndexError:
|
||||
logging.exception("Faction has no buildings defined")
|
||||
return
|
||||
|
||||
obj_name = namegen.random_objective_name()
|
||||
template = random.choice(list(self.templates[category].values()))
|
||||
|
||||
if category == "oil":
|
||||
location_type = LocationType.OffshoreStrikeTarget
|
||||
else:
|
||||
location_type = LocationType.StrikeTarget
|
||||
|
||||
# Pick from preset locations
|
||||
point = self.location_finder.location_for(location_type)
|
||||
if point is None:
|
||||
return
|
||||
|
||||
object_id = 0
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
# TODO: Create only one TGO per objective, each with multiple units.
|
||||
for unit in template:
|
||||
object_id += 1
|
||||
|
||||
template_point = Point(unit["offset"].x, unit["offset"].y)
|
||||
g = BuildingGroundObject(
|
||||
obj_name, category, group_id, object_id, point + template_point,
|
||||
unit["heading"], self.control_point, unit["type"])
|
||||
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_aa_site(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Sam)
|
||||
if position is None:
|
||||
return
|
||||
self.generate_aa_at(position, ranges=[
|
||||
# Prefer to use proper SAMs, but fall back to SHORADs if needed.
|
||||
{AirDefenseRange.Long, AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
])
|
||||
|
||||
def generate_aa_at(
|
||||
self, position: Point,
|
||||
ranges: Iterable[Set[AirDefenseRange]]) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point, for_airbase=False)
|
||||
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_missile_sites(self) -> None:
|
||||
for i in range(self.faction.missiles_group_count):
|
||||
self.generate_missile_site()
|
||||
|
||||
def generate_missile_site(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.MissileSite)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = MissileSiteGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point)
|
||||
group = generate_missile_group(self.game, g, self.faction_name)
|
||||
g.groups = []
|
||||
if group is not None:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
return
|
||||
|
||||
|
||||
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||
def generate(self) -> bool:
|
||||
self.generate_fob()
|
||||
FobDefenseGenerator(self.game, self.control_point).generate()
|
||||
self.generate_required_aa()
|
||||
return True
|
||||
|
||||
def generate_fob(self) -> None:
|
||||
try:
|
||||
category = self.faction.building_set[self.faction.building_set.index('fob')]
|
||||
except IndexError:
|
||||
logging.exception("Faction has no fob buildings defined")
|
||||
return
|
||||
|
||||
obj_name = self.control_point.name
|
||||
template = random.choice(list(self.templates[category].values()))
|
||||
point = self.control_point.position
|
||||
# Pick from preset locations
|
||||
object_id = 0
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
# TODO: Create only one TGO per objective, each with multiple units.
|
||||
for unit in template:
|
||||
object_id += 1
|
||||
|
||||
template_point = Point(unit["offset"].x, unit["offset"].y)
|
||||
g = BuildingGroundObject(
|
||||
obj_name, category, group_id, object_id, point + template_point,
|
||||
unit["heading"], self.control_point, unit["type"], airbase_group=True)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
|
||||
class GroundObjectGenerator:
|
||||
def __init__(self, game: Game,
|
||||
generator_settings: GeneratorSettings) -> None:
|
||||
self.game = game
|
||||
self.generator_settings = generator_settings
|
||||
with open("resources/groundobject_templates.p", "rb") as f:
|
||||
self.templates: GroundObjectTemplates = pickle.load(f)
|
||||
|
||||
def generate(self) -> None:
|
||||
# Copied so we can remove items from the original list without breaking
|
||||
# the iterator.
|
||||
control_points = list(self.game.theater.controlpoints)
|
||||
for control_point in control_points:
|
||||
if not self.generate_for_control_point(control_point):
|
||||
self.game.theater.controlpoints.remove(control_point)
|
||||
|
||||
def generate_for_control_point(self, control_point: ControlPoint) -> bool:
|
||||
generator: ControlPointGroundObjectGenerator
|
||||
if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
|
||||
generator = CarrierGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point)
|
||||
elif control_point.cptype == ControlPointType.LHA_GROUP:
|
||||
generator = LhaGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point)
|
||||
elif isinstance(control_point, OffMapSpawn):
|
||||
generator = NoOpGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point)
|
||||
elif isinstance(control_point, Fob):
|
||||
generator = FobGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point,
|
||||
self.templates)
|
||||
else:
|
||||
generator = AirbaseGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point,
|
||||
self.templates)
|
||||
return generator.generate()
|
||||
423
game/theater/theatergroundobject.py
Normal file
423
game/theater/theatergroundobject.py
Normal file
@@ -0,0 +1,423 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from typing import Iterator, List, TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import Group
|
||||
|
||||
from .. import db
|
||||
from ..data.radar_db import UNITS_WITH_RADAR
|
||||
from ..utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .controlpoint import ControlPoint
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
from .missiontarget import MissionTarget
|
||||
|
||||
NAME_BY_CATEGORY = {
|
||||
"power": "Power plant",
|
||||
"ammo": "Ammo depot",
|
||||
"fuel": "Fuel depot",
|
||||
"aa": "AA Defense Site",
|
||||
"ware": "Warehouse",
|
||||
"farp": "FARP",
|
||||
"fob": "FOB",
|
||||
"factory": "Factory",
|
||||
"comms": "Comms. tower",
|
||||
"oil": "Oil platform",
|
||||
"derrick": "Derrick",
|
||||
"ww2bunker": "Bunker",
|
||||
"village": "Village",
|
||||
"allycamp": "Camp",
|
||||
"EWR":"EWR",
|
||||
}
|
||||
|
||||
ABBREV_NAME = {
|
||||
"power": "PLANT",
|
||||
"ammo": "AMMO",
|
||||
"fuel": "FUEL",
|
||||
"aa": "AA",
|
||||
"ware": "WARE",
|
||||
"farp": "FARP",
|
||||
"fob": "FOB",
|
||||
"factory": "FACTORY",
|
||||
"comms": "COMMST",
|
||||
"oil": "OILP",
|
||||
"derrick": "DERK",
|
||||
"ww2bunker": "BUNK",
|
||||
"village": "VLG",
|
||||
"allycamp": "CMP",
|
||||
}
|
||||
|
||||
CATEGORY_MAP = {
|
||||
|
||||
# Special cases
|
||||
"CARRIER": ["CARRIER"],
|
||||
"LHA": ["LHA"],
|
||||
"aa": ["AA"],
|
||||
|
||||
# Buildings
|
||||
"power": ["Workshop A", "Electric power box", "Garage small A", "Farm B", "Repair workshop", "Garage B"],
|
||||
"ware": ["Warehouse", "Hangar A"],
|
||||
"fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"],
|
||||
"ammo": [".Ammunition depot", "Hangar B"],
|
||||
"farp": ["FARP Tent", "FARP Ammo Dump Coating", "FARP Fuel Depot", "FARP Command Post", "FARP CP Blindage"],
|
||||
"fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"],
|
||||
"factory": ["Tech combine", "Tech hangar A"],
|
||||
"comms": ["TV tower", "Comms tower M"],
|
||||
"oil": ["Oil platform"],
|
||||
"derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"],
|
||||
"ww2bunker": ["Siegfried Line", "Fire Control Bunker", "SK_C_28_naval_gun", "Concertina Wire", "Czech hedgehogs 1"],
|
||||
"village": ["Small house 1B", "Small House 1A", "Small warehouse 1"],
|
||||
"allycamp": [],
|
||||
}
|
||||
|
||||
|
||||
class TheaterGroundObject(MissionTarget):
|
||||
|
||||
def __init__(self, name: str, category: str, group_id: int, position: Point,
|
||||
heading: int, control_point: ControlPoint, dcs_identifier: str,
|
||||
airbase_group: bool, sea_object: bool) -> None:
|
||||
super().__init__(name, position)
|
||||
self.category = category
|
||||
self.group_id = group_id
|
||||
self.heading = heading
|
||||
self.control_point = control_point
|
||||
self.dcs_identifier = dcs_identifier
|
||||
self.airbase_group = airbase_group
|
||||
self.sea_object = sea_object
|
||||
self.groups: List[Group] = []
|
||||
|
||||
@property
|
||||
def is_dead(self) -> bool:
|
||||
return self.alive_unit_count == 0
|
||||
|
||||
@property
|
||||
def units(self) -> List[Unit]:
|
||||
"""
|
||||
:return: all the units at this location
|
||||
"""
|
||||
return list(itertools.chain.from_iterable([g.units for g in self.groups]))
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
"""The name of the unit group."""
|
||||
return f"{self.category}|{self.group_id}"
|
||||
|
||||
@property
|
||||
def waypoint_name(self) -> str:
|
||||
return f"[{self.name}] {self.category}"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return NAME_BY_CATEGORY[self.category]
|
||||
|
||||
def is_same_group(self, identifier: str) -> bool:
|
||||
return self.group_id == identifier
|
||||
|
||||
@property
|
||||
def obj_name(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def faction_color(self) -> str:
|
||||
return "BLUE" if self.control_point.captured else "RED"
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
return self.control_point.is_friendly(to_player)
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
# TODO: FlightType.LOGISTICS
|
||||
# TODO: FlightType.TROOP_TRANSPORT
|
||||
]
|
||||
else:
|
||||
yield from [
|
||||
FlightType.STRIKE,
|
||||
FlightType.BAI,
|
||||
]
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def alive_unit_count(self) -> int:
|
||||
return sum(len(g.units) for g in self.groups)
|
||||
|
||||
@property
|
||||
def might_have_aa(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def has_radar(self) -> bool:
|
||||
"""Returns True if the ground object contains a unit with radar."""
|
||||
for group in self.groups:
|
||||
for unit in group.units:
|
||||
if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR:
|
||||
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 detection_range(self, group: Group) -> Distance:
|
||||
return self._max_range_of_type(group, "detection_range")
|
||||
|
||||
def threat_range(self, group: Group) -> Distance:
|
||||
if not self.detection_range(group):
|
||||
# For simple SAMs like shilkas, the unit has both a threat and
|
||||
# detection range. For complex sites like SA-2s, the launcher has a
|
||||
# threat range and the search/track radars have detection ranges. If
|
||||
# the site has no detection range it has no radars and can't fire,
|
||||
# so it's not actually a threat even if it still has launchers.
|
||||
return meters(0)
|
||||
return self._max_range_of_type(group, "threat_range")
|
||||
|
||||
|
||||
class BuildingGroundObject(TheaterGroundObject):
|
||||
def __init__(self, name: str, category: str, group_id: int, object_id: int,
|
||||
position: Point, heading: int, control_point: ControlPoint,
|
||||
dcs_identifier: str, airbase_group=False) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category=category,
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=heading,
|
||||
control_point=control_point,
|
||||
dcs_identifier=dcs_identifier,
|
||||
airbase_group=airbase_group,
|
||||
sea_object=False
|
||||
)
|
||||
self.object_id = object_id
|
||||
# Other TGOs track deadness based on the number of alive units, but
|
||||
# buildings don't have groups assigned to the TGO.
|
||||
self._dead = False
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
"""The name of the unit group."""
|
||||
return f"{self.category}|{self.group_id}|{self.object_id}"
|
||||
|
||||
@property
|
||||
def waypoint_name(self) -> str:
|
||||
return f"{super().waypoint_name} #{self.object_id}"
|
||||
|
||||
@property
|
||||
def is_dead(self) -> bool:
|
||||
if not hasattr(self, "_dead"):
|
||||
self._dead = False
|
||||
return self._dead
|
||||
|
||||
def kill(self) -> None:
|
||||
self._dead = True
|
||||
|
||||
|
||||
class NavalGroundObject(TheaterGroundObject):
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if not self.is_friendly(for_player):
|
||||
yield FlightType.ANTISHIP
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def might_have_aa(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class GenericCarrierGroundObject(NavalGroundObject):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: Why is this both a CP and a TGO?
|
||||
class CarrierGroundObject(GenericCarrierGroundObject):
|
||||
def __init__(self, name: str, group_id: int,
|
||||
control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="CARRIER",
|
||||
group_id=group_id,
|
||||
position=control_point.position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="CARRIER",
|
||||
airbase_group=True,
|
||||
sea_object=True
|
||||
)
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them,
|
||||
# add to EWR.
|
||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||
|
||||
|
||||
# TODO: Why is this both a CP and a TGO?
|
||||
class LhaGroundObject(GenericCarrierGroundObject):
|
||||
def __init__(self, name: str, group_id: int,
|
||||
control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="LHA",
|
||||
group_id=group_id,
|
||||
position=control_point.position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="LHA",
|
||||
airbase_group=True,
|
||||
sea_object=True
|
||||
)
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them,
|
||||
# add to EWR.
|
||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||
|
||||
|
||||
class MissileSiteGroundObject(TheaterGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=False,
|
||||
sea_object=False
|
||||
)
|
||||
|
||||
|
||||
class BaseDefenseGroundObject(TheaterGroundObject):
|
||||
"""Base type for all base defenses."""
|
||||
|
||||
|
||||
# TODO: Differentiate types.
|
||||
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
|
||||
# be split into their own types.
|
||||
class SamGroundObject(BaseDefenseGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint, for_airbase: bool) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=for_airbase,
|
||||
sea_object=False
|
||||
)
|
||||
# Set by the SAM unit generator if the generated group is compatible
|
||||
# with Skynet.
|
||||
self.skynet_capable = False
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
if self.skynet_capable:
|
||||
# Prefix the group names of SAM sites with the side color so Skynet
|
||||
# can find them.
|
||||
return f"{self.faction_color}|SAM|{self.group_id}"
|
||||
else:
|
||||
return super().group_name
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if not self.is_friendly(for_player):
|
||||
yield FlightType.DEAD
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def might_have_aa(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint, for_airbase: bool) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=for_airbase,
|
||||
sea_object=False
|
||||
)
|
||||
|
||||
|
||||
class EwrGroundObject(BaseDefenseGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="EWR",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="EWR",
|
||||
airbase_group=True,
|
||||
sea_object=False
|
||||
)
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them.
|
||||
return f"{self.faction_color}|{super().group_name}"
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
if not self.is_friendly(for_player):
|
||||
yield FlightType.DEAD
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def might_have_aa(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class ShipGroundObject(NavalGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=False,
|
||||
sea_object=True
|
||||
)
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them,
|
||||
# add to EWR.
|
||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||
158
game/threatzones.py
Normal file
158
game/threatzones.py
Normal file
@@ -0,0 +1,158 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import singledispatchmethod
|
||||
from typing import Optional, TYPE_CHECKING, Union
|
||||
|
||||
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
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
ThreatPoly = Union[MultiPolygon, Polygon]
|
||||
|
||||
|
||||
class ThreatZones:
|
||||
def __init__(self, airbases: ThreatPoly, air_defenses: ThreatPoly) -> None:
|
||||
self.airbases = airbases
|
||||
self.air_defenses = air_defenses
|
||||
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)
|
||||
|
||||
@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
|
||||
)))
|
||||
|
||||
@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
|
||||
)))
|
||||
|
||||
@classmethod
|
||||
def closest_enemy_airbase(cls, location: ControlPoint,
|
||||
max_distance: Distance) -> Optional[ControlPoint]:
|
||||
airfields = ObjectiveDistanceCache.get_closest_airfields(location)
|
||||
for airfield in airfields.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.
|
||||
"""
|
||||
airbases = []
|
||||
air_defenses = []
|
||||
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)
|
||||
airbases.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)
|
||||
|
||||
return cls(
|
||||
airbases=unary_union(airbases),
|
||||
air_defenses=unary_union(air_defenses)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def dcs_to_shapely_point(point: DcsPoint) -> ShapelyPoint:
|
||||
return ShapelyPoint(point.x, point.y)
|
||||
135
game/unitmap.py
Normal file
135
game/unitmap.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""Maps generated units back to their Liberation types."""
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional, Type
|
||||
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game import db
|
||||
from game.theater import Airfield, ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import BuildingGroundObject
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrontLineUnit:
|
||||
unit_type: Type[VehicleType]
|
||||
origin: ControlPoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundObjectUnit:
|
||||
ground_object: TheaterGroundObject
|
||||
group: Group
|
||||
unit: Unit
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Building:
|
||||
ground_object: BuildingGroundObject
|
||||
|
||||
|
||||
class UnitMap:
|
||||
def __init__(self) -> None:
|
||||
self.aircraft: Dict[str, Flight] = {}
|
||||
self.airfields: Dict[str, Airfield] = {}
|
||||
self.front_line_units: Dict[str, FrontLineUnit] = {}
|
||||
self.ground_object_units: Dict[str, GroundObjectUnit] = {}
|
||||
self.buildings: Dict[str, Building] = {}
|
||||
|
||||
def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None:
|
||||
for unit in group.units:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(unit.name)
|
||||
if name in self.aircraft:
|
||||
raise RuntimeError(f"Duplicate unit name: {name}")
|
||||
self.aircraft[name] = flight
|
||||
|
||||
def flight(self, unit_name: str) -> Optional[Flight]:
|
||||
return self.aircraft.get(unit_name, None)
|
||||
|
||||
def add_airfield(self, airfield: Airfield) -> None:
|
||||
if airfield.name in self.airfields:
|
||||
raise RuntimeError(f"Duplicate airfield: {airfield.name}")
|
||||
self.airfields[airfield.name] = airfield
|
||||
|
||||
def airfield(self, name: str) -> Optional[Airfield]:
|
||||
return self.airfields.get(name, None)
|
||||
|
||||
def add_front_line_units(self, group: Group, origin: ControlPoint) -> None:
|
||||
for unit in group.units:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(unit.name)
|
||||
if name in self.front_line_units:
|
||||
raise RuntimeError(f"Duplicate front line unit: {name}")
|
||||
unit_type = db.unit_type_from_name(unit.type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(f"Unknown unit type: {unit.type}")
|
||||
if not issubclass(unit_type, VehicleType):
|
||||
raise RuntimeError(
|
||||
f"{name} is a {unit_type.__name__}, expected a VehicleType")
|
||||
self.front_line_units[name] = FrontLineUnit(unit_type, origin)
|
||||
|
||||
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
|
||||
return self.front_line_units.get(name, None)
|
||||
|
||||
def add_ground_object_units(self, ground_object: TheaterGroundObject,
|
||||
persistence_group: Group,
|
||||
miz_group: Group) -> None:
|
||||
"""Adds a group associated with a TGO to the unit map.
|
||||
|
||||
Args:
|
||||
ground_object: The TGO the group is associated with.
|
||||
persistence_group: The Group tracked by the TGO itself.
|
||||
miz_group: The Group spawned for the miz to match persistence_group.
|
||||
"""
|
||||
# Deaths for units at TGOs are recorded in the Group that is contained
|
||||
# by the TGO, but when groundobjectsgen populates the miz it creates new
|
||||
# groups based on that template, so the units and groups in the miz are
|
||||
# not a direct match for the units and groups that persist in the TGO.
|
||||
#
|
||||
# This means that we need to map the spawned unit names back to the
|
||||
# original TGO units, not the ones in the miz.
|
||||
if len(persistence_group.units) != len(miz_group.units):
|
||||
raise ValueError("Persistent group does not match generated group")
|
||||
unit_pairs = zip(persistence_group.units, miz_group.units)
|
||||
for persistent_unit, miz_unit in unit_pairs:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(miz_unit.name)
|
||||
if name in self.ground_object_units:
|
||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||
self.ground_object_units[name] = GroundObjectUnit(
|
||||
ground_object, persistence_group, persistent_unit)
|
||||
|
||||
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
|
||||
return self.ground_object_units.get(name, None)
|
||||
|
||||
def add_building(self, ground_object: BuildingGroundObject,
|
||||
group: Group) -> None:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
# The name of the initiator in the DCS dead event will have " object"
|
||||
# appended for statics.
|
||||
name = f"{group.name} object"
|
||||
if name in self.buildings:
|
||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||
self.buildings[name] = Building(ground_object)
|
||||
|
||||
def add_fortification(self, ground_object: BuildingGroundObject,
|
||||
group: VehicleGroup) -> None:
|
||||
if len(group.units) != 1:
|
||||
raise ValueError("Fortification groups must have exactly one unit.")
|
||||
unit = group.units[0]
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(unit.name)
|
||||
if name in self.buildings:
|
||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||
self.buildings[name] = Building(ground_object)
|
||||
|
||||
def building_or_fortification(self, name: str) -> Optional[Building]:
|
||||
return self.buildings.get(name, None)
|
||||
182
game/utils.py
182
game/utils.py
@@ -1,14 +1,180 @@
|
||||
def meter_to_feet(value_in_meter):
|
||||
return int(3.28084 * value_in_meter)
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
METERS_TO_FEET = 3.28084
|
||||
FEET_TO_METERS = 1 / METERS_TO_FEET
|
||||
NM_TO_METERS = 1852
|
||||
METERS_TO_NM = 1 / NM_TO_METERS
|
||||
|
||||
KNOTS_TO_KPH = 1.852
|
||||
KPH_TO_KNOTS = 1 / KNOTS_TO_KPH
|
||||
MS_TO_KPH = 3.6
|
||||
KPH_TO_MS = 1 / MS_TO_KPH
|
||||
|
||||
|
||||
def feet_to_meter(value_in_feet):
|
||||
return int(float(value_in_feet)/3.048)
|
||||
def heading_sum(h, a) -> int:
|
||||
h += a
|
||||
if h > 360:
|
||||
return h - 360
|
||||
elif h < 0:
|
||||
return 360 + h
|
||||
else:
|
||||
return h
|
||||
|
||||
|
||||
def meter_to_nm(value_in_meter):
|
||||
return int(float(value_in_meter)*0.000539957)
|
||||
def opposite_heading(h):
|
||||
return heading_sum(h, 180)
|
||||
|
||||
|
||||
def nm_to_meter(value_in_nm):
|
||||
return int(float(value_in_nm)*1852)
|
||||
@dataclass(frozen=True, order=True)
|
||||
class Distance:
|
||||
distance_in_meters: float
|
||||
|
||||
@property
|
||||
def feet(self) -> float:
|
||||
return self.distance_in_meters * METERS_TO_FEET
|
||||
|
||||
@property
|
||||
def meters(self) -> float:
|
||||
return self.distance_in_meters
|
||||
|
||||
@property
|
||||
def nautical_miles(self) -> float:
|
||||
return self.distance_in_meters * METERS_TO_NM
|
||||
|
||||
@classmethod
|
||||
def from_feet(cls, value: float) -> Distance:
|
||||
return cls(value * FEET_TO_METERS)
|
||||
|
||||
@classmethod
|
||||
def from_meters(cls, value: float) -> Distance:
|
||||
return cls(value)
|
||||
|
||||
@classmethod
|
||||
def from_nautical_miles(cls, value: float) -> Distance:
|
||||
return cls(value * NM_TO_METERS)
|
||||
|
||||
def __add__(self, other: Distance) -> Distance:
|
||||
return meters(self.meters + other.meters)
|
||||
|
||||
def __sub__(self, other: Distance) -> Distance:
|
||||
return meters(self.meters - other.meters)
|
||||
|
||||
def __mul__(self, other: Union[float, int]) -> Distance:
|
||||
return meters(self.meters * other)
|
||||
|
||||
def __truediv__(self, other: Union[float, int]) -> Distance:
|
||||
return meters(self.meters / other)
|
||||
|
||||
def __floordiv__(self, other: Union[float, int]) -> Distance:
|
||||
return meters(self.meters // other)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return not math.isclose(self.meters, 0.0)
|
||||
|
||||
|
||||
def feet(value: float) -> Distance:
|
||||
return Distance.from_feet(value)
|
||||
|
||||
|
||||
def meters(value: float) -> Distance:
|
||||
return Distance.from_meters(value)
|
||||
|
||||
|
||||
def nautical_miles(value: float) -> Distance:
|
||||
return Distance.from_nautical_miles(value)
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class Speed:
|
||||
speed_in_kph: float
|
||||
|
||||
@property
|
||||
def knots(self) -> float:
|
||||
return self.speed_in_kph * KPH_TO_KNOTS
|
||||
|
||||
@property
|
||||
def kph(self) -> float:
|
||||
return self.speed_in_kph
|
||||
|
||||
@property
|
||||
def meters_per_second(self) -> float:
|
||||
return self.speed_in_kph * KPH_TO_MS
|
||||
|
||||
def mach(self, altitude: Distance = meters(0)) -> float:
|
||||
c_sound = mach(1, altitude)
|
||||
return self.speed_in_kph / c_sound.kph
|
||||
|
||||
@classmethod
|
||||
def from_knots(cls, value: float) -> Speed:
|
||||
return cls(value * KNOTS_TO_KPH)
|
||||
|
||||
@classmethod
|
||||
def from_kph(cls, value: float) -> Speed:
|
||||
return cls(value)
|
||||
|
||||
@classmethod
|
||||
def from_meters_per_second(cls, value: float) -> Speed:
|
||||
return cls(value * MS_TO_KPH)
|
||||
|
||||
@classmethod
|
||||
def from_mach(cls, value: float, altitude: Distance) -> Speed:
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
|
||||
if altitude <= feet(36152):
|
||||
temperature_f = 59 - 0.00356 * altitude.feet
|
||||
else:
|
||||
# There's another formula for altitudes over 82k feet, but we better
|
||||
# not be planning waypoints that high...
|
||||
temperature_f = -70
|
||||
|
||||
temperature_k = (temperature_f + 459.67) * (5 / 9)
|
||||
|
||||
# https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html
|
||||
# Dependent on temperature, but varies very little (+/-0.001)
|
||||
# between -40F and 180F.
|
||||
heat_capacity_ratio = 1.4
|
||||
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html
|
||||
gas_constant = 286 # m^2/s^2/K
|
||||
c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k)
|
||||
return mps(c_sound) * value
|
||||
|
||||
def __add__(self, other: Speed) -> Speed:
|
||||
return kph(self.kph + other.kph)
|
||||
|
||||
def __sub__(self, other: Speed) -> Speed:
|
||||
return kph(self.kph - other.kph)
|
||||
|
||||
def __mul__(self, other: Union[float, int]) -> Speed:
|
||||
return kph(self.kph * other)
|
||||
|
||||
def __truediv__(self, other: Union[float, int]) -> Speed:
|
||||
return kph(self.kph / other)
|
||||
|
||||
def __floordiv__(self, other: Union[float, int]) -> Speed:
|
||||
return kph(self.kph // other)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return not math.isclose(self.kph, 0.0)
|
||||
|
||||
|
||||
def knots(value: float) -> Speed:
|
||||
return Speed.from_knots(value)
|
||||
|
||||
|
||||
def kph(value: float) -> Speed:
|
||||
return Speed.from_kph(value)
|
||||
|
||||
|
||||
def mps(value: float) -> Speed:
|
||||
return Speed.from_meters_per_second(value)
|
||||
|
||||
|
||||
def mach(value: float, altitude: Distance) -> Speed:
|
||||
return Speed.from_mach(value, altitude)
|
||||
|
||||
|
||||
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
|
||||
|
||||
18
game/version.py
Normal file
18
game/version.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _build_version_string() -> str:
|
||||
components = ["2.4.2"]
|
||||
build_number_path = Path("resources/buildnumber")
|
||||
if build_number_path.exists():
|
||||
with build_number_path.open("r") as build_number_file:
|
||||
components.append(build_number_file.readline())
|
||||
|
||||
if not Path("resources/final").exists():
|
||||
components.append("preview")
|
||||
|
||||
return "-".join(components)
|
||||
|
||||
|
||||
#: Current version of Liberation.
|
||||
VERSION = _build_version_string()
|
||||
186
game/weather.py
Normal file
186
game/weather.py
Normal file
@@ -0,0 +1,186 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.weather import Weather as PydcsWeather, Wind
|
||||
|
||||
from game.settings import Settings
|
||||
from game.utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater
|
||||
|
||||
|
||||
class TimeOfDay(Enum):
|
||||
Dawn = "dawn"
|
||||
Day = "day"
|
||||
Dusk = "dusk"
|
||||
Night = "night"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WindConditions:
|
||||
at_0m: Wind
|
||||
at_2000m: Wind
|
||||
at_8000m: Wind
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Clouds:
|
||||
base: int
|
||||
density: int
|
||||
thickness: int
|
||||
precipitation: PydcsWeather.Preceptions
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Fog:
|
||||
visibility: Distance
|
||||
thickness: int
|
||||
|
||||
|
||||
class Weather:
|
||||
def __init__(self) -> None:
|
||||
self.clouds = self.generate_clouds()
|
||||
self.fog = self.generate_fog()
|
||||
self.wind = self.generate_wind()
|
||||
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
raise NotImplementedError
|
||||
|
||||
def generate_fog(self) -> Optional[Fog]:
|
||||
if random.randrange(5) != 0:
|
||||
return None
|
||||
return Fog(
|
||||
visibility=meters(random.randint(2500, 5000)),
|
||||
thickness=random.randint(100, 500)
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def random_wind(minimum: int, maximum) -> WindConditions:
|
||||
wind_direction = random.randint(0, 360)
|
||||
at_0m_factor = 1
|
||||
at_2000m_factor = 2
|
||||
at_8000m_factor = 3
|
||||
base_wind = random.randint(minimum, maximum)
|
||||
|
||||
return WindConditions(
|
||||
# Always some wind to make the smoke move a bit.
|
||||
at_0m=Wind(wind_direction, max(1, base_wind * at_0m_factor)),
|
||||
at_2000m=Wind(wind_direction, base_wind * at_2000m_factor),
|
||||
at_8000m=Wind(wind_direction, base_wind * at_8000m_factor)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def random_cloud_base() -> int:
|
||||
return random.randint(2000, 3000)
|
||||
|
||||
@staticmethod
|
||||
def random_cloud_thickness() -> int:
|
||||
return random.randint(100, 400)
|
||||
|
||||
|
||||
class ClearSkies(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return None
|
||||
|
||||
def generate_fog(self) -> Optional[Fog]:
|
||||
return None
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 0)
|
||||
|
||||
|
||||
class Cloudy(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds(
|
||||
base=self.random_cloud_base(),
|
||||
density=random.randint(1, 8),
|
||||
thickness=self.random_cloud_thickness(),
|
||||
precipitation=PydcsWeather.Preceptions.None_
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 4)
|
||||
|
||||
|
||||
class Raining(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds(
|
||||
base=self.random_cloud_base(),
|
||||
density=random.randint(5, 8),
|
||||
thickness=self.random_cloud_thickness(),
|
||||
precipitation=PydcsWeather.Preceptions.Rain
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 6)
|
||||
|
||||
|
||||
class Thunderstorm(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds(
|
||||
base=self.random_cloud_base(),
|
||||
density=random.randint(9, 10),
|
||||
thickness=self.random_cloud_thickness(),
|
||||
precipitation=PydcsWeather.Preceptions.Thunderstorm
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 8)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Conditions:
|
||||
time_of_day: TimeOfDay
|
||||
start_time: datetime.datetime
|
||||
weather: Weather
|
||||
|
||||
@classmethod
|
||||
def generate(cls, theater: ConflictTheater, day: datetime.date,
|
||||
time_of_day: TimeOfDay, settings: Settings) -> Conditions:
|
||||
return cls(
|
||||
time_of_day=time_of_day,
|
||||
start_time=cls.generate_start_time(
|
||||
theater, day, time_of_day, settings.night_disabled
|
||||
),
|
||||
weather=cls.generate_weather()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate_start_time(cls, theater: ConflictTheater, day: datetime.date,
|
||||
time_of_day: TimeOfDay,
|
||||
night_disabled: bool) -> datetime.datetime:
|
||||
if night_disabled:
|
||||
logging.info("Skip Night mission due to user settings")
|
||||
time_range = {
|
||||
TimeOfDay.Dawn: (8, 9),
|
||||
TimeOfDay.Day: (10, 12),
|
||||
TimeOfDay.Dusk: (12, 14),
|
||||
TimeOfDay.Night: (14, 17),
|
||||
}[time_of_day]
|
||||
else:
|
||||
time_range = theater.daytime_map[time_of_day.value]
|
||||
|
||||
time = datetime.time(hour=random.randint(*time_range))
|
||||
return datetime.datetime.combine(day, time)
|
||||
|
||||
@classmethod
|
||||
def generate_weather(cls) -> Weather:
|
||||
chances = {
|
||||
Thunderstorm: 1,
|
||||
Raining: 20,
|
||||
Cloudy: 60,
|
||||
ClearSkies: 20,
|
||||
}
|
||||
weather_type = random.choices(list(chances.keys()),
|
||||
weights=list(chances.values()))[0]
|
||||
return weather_type()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user