mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
414 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
97ea67d01d | ||
|
|
d6376c3a91 | ||
|
|
793b356c01 | ||
|
|
25efdd3d4f | ||
|
|
0b2483ea15 | ||
|
|
4d26ec0789 | ||
|
|
7d907aac0f | ||
|
|
d6981550a8 | ||
|
|
8b0636367b | ||
|
|
a6c9d0f9bc | ||
|
|
61ebe9780e | ||
|
|
e887082501 | ||
|
|
80778aa267 | ||
|
|
445cb4f146 | ||
|
|
95db2aa14f | ||
|
|
4b0fc637eb | ||
|
|
48d6b4cfa1 | ||
|
|
fc11182bbe | ||
|
|
1848338ef7 | ||
|
|
08ceb57c31 | ||
|
|
affb332eb9 | ||
|
|
d5276c9d4a | ||
|
|
07e5c568c4 | ||
|
|
31fdd24c3e | ||
|
|
bfa0e4ba49 | ||
|
|
c89ff2c3d6 | ||
|
|
4ec88d524a | ||
|
|
48c218b430 | ||
|
|
d316836e90 | ||
|
|
8443f61f0a | ||
|
|
67806f3d76 | ||
|
|
7744f84e85 |
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.
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ resources/payloads/*.lua
|
||||
venv
|
||||
logs.txt
|
||||
.DS_Store
|
||||
.vscode/settings.json
|
||||
dist/**
|
||||
a.py
|
||||
resources/tools/a.miz
|
||||
|
||||
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.
|
||||
10
README.md
10
README.md
@@ -21,6 +21,16 @@ It is an external program that generates full and complex DCS missions and manag
|
||||
|
||||
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/)
|
||||
|
||||
153
changelog.md
153
changelog.md
@@ -1,16 +1,162 @@
|
||||
# 2.4.0
|
||||
|
||||
Saves from 2.3 are not compatible with 2.4.
|
||||
|
||||
## Highlights
|
||||
|
||||
* Improved flight plan generation to avoid loitering in or traveling through threatened areas when practical.
|
||||
* Improved AI aircraft purchasing behavior.
|
||||
* Era-restricted weapons (work in progress).
|
||||
* Tons of UI polish.
|
||||
* Rebalanced economy to keep opfor competitive over the course of the game.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Flight Planner]** Air-to-air and SEAD escorts will no longer be automatically planned for packages that are not in range of threats.
|
||||
* **[Flight Planner]** Non-custom flight plans will now navigate around threat areas en route to the target area when practical.
|
||||
* **[Flight Planner]** Flight plans along front lines now ensure that the race track start is closer to the departure airfield than the race track end.
|
||||
* **[Campaign AI]** Auto-purchase now prefers airfields that are not within range of the enemy.
|
||||
* **[Campaign AI]** Auto-purchase now prefers the best aircraft for the task, but will attempt to maintain some variety.
|
||||
* **[Campaign AI]** Opfor now sells off odd aircraft since they're unlikely to be used.
|
||||
* **[Campaign AI]** Multiple rounds of CAP will be planned (roughly 90 minutes of coverage). Default starting budget has increased to account for the increased need for aircraft.
|
||||
* **[Mission Generator]** Multiple groups are created for complex SAM sites (SAMs with additional point defense or SHORADS), improving Skynet behavior.
|
||||
* **[Mission Generator]** Default start type can now be chosen in the settings. This replaces the non-functional "AI Parking Start" option. **Selecting any type other than cold will break OCA/Aircraft missions.**
|
||||
* **[Cheat Menu]** Added ability to toggle base capture and frontline advance/retreat cheats.
|
||||
* **[Skynet]** Updated to 2.0.1.
|
||||
* **[Skynet]** Point defenses are now configured to remain on to protect the site they accompany.
|
||||
* **[Hercules]** Updated the Hercules Cargo list file.
|
||||
* **[Balance]** Opfor now gains income using the same rules as the player, significantly increasing their income relative to the player for most campaigns.
|
||||
* **[Balance]** Units now retreat from captured bases when able. Units with no retreat path will be captured and sold.
|
||||
* **[Economy]** FOBs generate only $10M per turn (previously $20M like airbases).
|
||||
* **[Economy]** Carriers and off-map spawns generate no income (previously $20M like airbases).
|
||||
* **[Economy]** Sales of aircraft and ground vehicles can now be cancelled before the next turn begins.
|
||||
* **[UI]** Multi-SAM objectives now show threat and detection rings per group.
|
||||
* **[UI]** New icon for AA sites with no active threat.
|
||||
* **[UI]** Unit names are now prettier and more accurate, and can now be set per-country for added historical flavour.
|
||||
* **[UI]** Default loadout is now shown for flights with no custom loadout selected.
|
||||
* **[UI]** Aircraft for a new flight are now only selectable if they match the task type for that flight.
|
||||
* **[UI]** WIP - There is now a unit info button for each unit in the recruitment list, that should help newer players learn what each unit does.
|
||||
* **[UI]** Docs for time-on-target and creating new theaters/factions/loadouts are now linked in the UI at the appropriate places.
|
||||
* **[UI]** ASAP is now a checkbox rather than a button. Enabling this will disable the TOT selector but changes to the package structure will automatically re-ASAP the package.
|
||||
* **[UI]** Arrival airfield is now shown in the flight list if it differs from the departure airfield.
|
||||
* **[UI]** Start type can now be selected when creating a flight.
|
||||
* **[UI]** Arrival and divert airfields can be edited after the flight is created.
|
||||
* **[Factions]** Added option for date-based loadout restriction. Active radar homing missiles are handled, patches welcome for the other thousand weapons.
|
||||
* **[Factions]** Added Poland 2010 faction.
|
||||
* **[Factions]** Added Greece 2005 faction.
|
||||
* **[Factions]** Added Iran 1988 faction.
|
||||
* **[Units]** Support for E-2 Hawkeye, SH-60B Seahawk, S-3B Viking (thanks to awinterquest) and SpGH Dana - these are now being used by appropriate factions.
|
||||
* **[Culling]** Missile sites are no longer culled.
|
||||
* **[Campaigns]** Added campaign "Black Sea Lite" by Starfire
|
||||
* **[Campaigns]** Added campaign "Exercise Vegas Nerve" by Starfire
|
||||
* **[New game Wizard]** The theater page is now the first page of the campaign wizard, recommended factions will be selected automatically on the faction selection page
|
||||
* **[New game Wizard]** Added information text about the selected campaign performance.
|
||||
* **[Mod Support]** Added support for High Digit SAMs mod 1.4.0
|
||||
* **[Mod Support]** Added SAMs sites generator : KS19Generator, SA10BGenerator, SA12Generator, SA17Generator, SA20Generator, SA20BGenerator, SA23Generator
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Hercules]** Updated the default Hercules radio frequency.
|
||||
* **[Economy]** Pending unit orders at captured bases will be refunded.
|
||||
* **[UI]** Carrier group SAM threat rings now move with the carrier.
|
||||
* **[UI]** Base intel menu no longer compresses text, and is now scrollable.
|
||||
* **[UI]** Edit Flight window is now dynamically sized to adapt to the width of waypoint names, so they no longer get truncated.
|
||||
* **[UI]** Budget income display is now rounded to 2 decimal places.
|
||||
* **[UI]** Fixed incorrect income per turn displayed for strike target tooltip.
|
||||
* **[Factions]** USA with C-130 faction now links to the required mod.
|
||||
* **[Campaign]** Fixed issue where destroyed buildings would sometimes not count as destroyed and thus respawn.
|
||||
* **[Campaign]** Fixed issue where destroyed runways were not registered.
|
||||
* **[Units]** J-11A is no longer spawned with empty loadout.
|
||||
* **[Units]** F-14B is no longer spawned with empty loadout for fighter sweep tasks.
|
||||
* **[Units]** Pyotr Velikiy cruiser has been removed for now as it's nearly unkillable.
|
||||
* **[Units]** Submarines have been removed for now as they aren't wholly functional.
|
||||
* **[Units]** Fixed "FACTION ERROR : Unable to find OliverHazardPerryGroupGenerator in pydcs" error at startup.
|
||||
* **[Mission Generator]** Fixed a bug where units set to Aggressive stance sometimes did not move.
|
||||
* **[Mission Generator]** Flyover points for OCA/Aircraft missions are now generated correctly.
|
||||
* **[Flight Planner]** Fixed not being able to create custom waypoints for buildings.
|
||||
* **[Flight Planner]** Strike missions will no longer be automatically planned against SAMs.
|
||||
* **[Flight Planner]** Strike missions will no longer be automatically planned against FOB structures.
|
||||
|
||||
# 2.3.4
|
||||
|
||||
## Fixes:
|
||||
[Mission Generator] Mission generator would crash when generating fire missions for destroyed SCUD sites - fixed
|
||||
|
||||
# 2.3.3
|
||||
|
||||
## Features/Improvements
|
||||
* **[Campaigns]** Reworked Golan Heights campaign on Syria, (Added FOB and preset locations for SAMS)
|
||||
* **[Campaigns]** Added a lite version of the Golan Heights campaign
|
||||
* **[Campaigns]** Reworked Syrian Civil War campaign (Added FOB and preset locations for SAMS)
|
||||
* **[Campaigns]** Reworked Emirates campaign
|
||||
* **[Campaigns]** AA units added to frontlines and updated all factions to include some frontline AA units.
|
||||
* **[Mission Generator]** Infantry will only be generated for APC and IFV groups
|
||||
* **[Mission Generator]** Infantry squads size is not randomized anymore
|
||||
* **[Mission Generator]** Infantry squads can have a mortar.
|
||||
* **[Mission Generator]** SCUD missiles sites will now fire on enemy controls points in range when possible
|
||||
* **[Factions]** Updated Nato Desert Storm to include F-14A
|
||||
* **[Factions]** Updated Iraq 1991 factions to include Zsu-57 and Mig-29A
|
||||
* **[Factions]** Germany 1944, added Stug III and Stug IV
|
||||
* **[Factions]** Added factions Insurgents (Hard) with better and more weapons
|
||||
* **[Plugins]** [The EWRS plugin](https://github.com/Bob7heBuilder/EWRS) is now included.
|
||||
* **[UI]** Added enemy intelligence summary and details window.
|
||||
|
||||
## Fixes:
|
||||
* **[Factions]** AI would never buy artillery units for the frontline - fixed
|
||||
* **[Factions]** Removed the F-111 unit from the NATO desert storm faction. (Recruiting it would cause crashes in DCS, since it is not a valid unit)
|
||||
* **[Campaign]** Automatic redeployment of ground units would sometimes fail - fixed
|
||||
* **[Mission Generator]** Artillery groups would retreat in the wrong direction - fixed
|
||||
* **[Units]** Fixed SPG_Stryker_M1128_MGS not being in db
|
||||
* **[UI]** Fixed and added many missing ground units icons
|
||||
* **[UI]** Ship groups could be replaced by SAM sites in the UI, which would lead to broken mission being generated - fixed
|
||||
* **[New Game Wizard]** Removed the "mid game" campaign generator option which is currently broken
|
||||
* **[Mission Generator]** Empty navy groups will no longer be generated
|
||||
* **[Mission Generator]** Fixed BAI, SEAD, and DEAD flights ocassionally being assigned the wrong targets.
|
||||
* **[Flight Planner]** Fixed not being able to plan packages against opfor carriers
|
||||
* **[UI]** Repaired SAMs no longer show as dead.
|
||||
* **[UI]** Fixed not being able to manage a disbanded site after disbanding and closing the base menu.
|
||||
|
||||
# 2.3.2
|
||||
|
||||
## Features/Improvements
|
||||
* **[Units]** Support for newly added BTR-82A, T-72B3
|
||||
* **[Units]** Added ZSU-57 AAA sites
|
||||
* **[Culling]** BARCAP missions no longer create culling exclusion zones.
|
||||
* **[Flight Planner]** Improved TOT planning. Negative start times no longer occur with TARCAPs and hold times no longer affect planning for flight plans without hold points.
|
||||
* **[Factions]** Added Iraq 1991 faction (thanks again to Hawkmoon!)
|
||||
|
||||
## Fixes:
|
||||
* **[Mission Generator]** Fix mission generation error when there are too many radio frequency to setup for the Mig-21
|
||||
* **[Mission Generator]** Fix ground units not moving forward
|
||||
* **[Mission Generator]** Fixed assigned radio channels overlapping with beacons.
|
||||
* **[Flight Planner]** Fix creation of custom waypoints.
|
||||
* **[Campaigns]** Fixed many cases of SAMs spawning on the runways/taxiways in Syria Full.
|
||||
|
||||
# 2.3.1
|
||||
|
||||
## Features/Improvements
|
||||
* **[UX]** Added a warning message when the player is attempting to buy more planes at an already full airbase.
|
||||
* **[Campaigns]** Migrated Syria full map to new format. (Thanks to Hawkmoon)
|
||||
* **[Faction]** Added NATO desert Storm faction (Thanks to Hawkmoon)
|
||||
|
||||
## Fixes:
|
||||
* **[AI]** CAP flights will engage enemies again.
|
||||
* **[Campaigns]** Fixed a missing path on the Caucasus Full Map campaign
|
||||
|
||||
# 2.3.0
|
||||
|
||||
# Features/Improvements
|
||||
## Features/Improvements
|
||||
* **[Campaign Map]** Overhauled the campaign model
|
||||
* **[Campaign Map]** Possible to add FOB as control points
|
||||
* **[Campaign Map]** Added off-map spawn locations
|
||||
* **[Campaign AI]** Overhauled AI recruiting behaviour
|
||||
* **[Campaign AI]** Added AI proucurement for Blue
|
||||
* **[Campaign AI]** Added AI procurement for Blue
|
||||
* **[Campaign]** New Campaign: "Black Sea"
|
||||
* **[Mission Planner]** Possible to move carrier and tarawa on the campaign map
|
||||
* **[Mission Generator]** Infantry squads on frontline can have manpads
|
||||
* **[Mission Generator]** Unused aircraft now spawned to allow for OCA strikes
|
||||
* **[Mission Generator]** Opfor now obeys parking limits
|
||||
* **[Mission Generator]** Support for Anubis C-130 Hercules mod
|
||||
* **[Flight Planner]** Added fighter sweep missions.
|
||||
* **[Flight Planner]** Added BAI missions.
|
||||
* **[Flight Planner]** Added anti-ship missions.
|
||||
@@ -21,6 +167,7 @@
|
||||
* **[QOL]** On liberation startup, your latest save game is loaded automatically
|
||||
* **[Units]** Reduced starting fuel load for C101
|
||||
* **[UI]** Inform the user of the weather
|
||||
* **[UI]** Added toolbar buttons to change map display settings
|
||||
* **[Game]** Added new Economy options for adjusting income multipliers and starting budgets.
|
||||
|
||||
## Fixes :
|
||||
@@ -34,7 +181,7 @@
|
||||
|
||||
# 2.2.1
|
||||
|
||||
# Features/Improvements
|
||||
## Features/Improvements
|
||||
* **[Factions]** Added factions : Georgia 2008, USN 1985, France 2005 Frenchpack by HerrTom
|
||||
* **[Factions]** Added map Persian Gulf full by Plob
|
||||
* **[Flight Planner]** Player flights with start delays under ten minutes will spawn immediately.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
|
||||
from game.utils import nm_to_meter, feet_to_meter
|
||||
from game.utils import Distance, feet, nautical_miles
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -12,31 +12,43 @@ class Doctrine:
|
||||
strike: bool
|
||||
antiship: bool
|
||||
|
||||
strike_max_range: int
|
||||
sead_max_range: int
|
||||
rendezvous_altitude: Distance
|
||||
hold_distance: Distance
|
||||
push_distance: Distance
|
||||
join_distance: Distance
|
||||
split_distance: Distance
|
||||
ingress_egress_distance: Distance
|
||||
ingress_altitude: Distance
|
||||
egress_altitude: Distance
|
||||
|
||||
rendezvous_altitude: int
|
||||
hold_distance: int
|
||||
push_distance: int
|
||||
join_distance: int
|
||||
split_distance: int
|
||||
ingress_egress_distance: int
|
||||
ingress_altitude: int
|
||||
egress_altitude: int
|
||||
|
||||
min_patrol_altitude: int
|
||||
max_patrol_altitude: int
|
||||
pattern_altitude: int
|
||||
min_patrol_altitude: Distance
|
||||
max_patrol_altitude: Distance
|
||||
pattern_altitude: Distance
|
||||
|
||||
#: The duration that CAP flights will remain on-station.
|
||||
cap_duration: timedelta
|
||||
cap_min_track_length: int
|
||||
cap_max_track_length: int
|
||||
cap_min_distance_from_cp: int
|
||||
cap_max_distance_from_cp: int
|
||||
|
||||
#: The minimum length of the CAP race track.
|
||||
cap_min_track_length: Distance
|
||||
|
||||
#: The maximum length of the CAP race track.
|
||||
cap_max_track_length: Distance
|
||||
|
||||
#: The minimum distance between the defended position and the *end* of the
|
||||
#: CAP race track.
|
||||
cap_min_distance_from_cp: Distance
|
||||
|
||||
#: The maximum distance between the defended position and the *end* of the
|
||||
#: CAP race track.
|
||||
cap_max_distance_from_cp: Distance
|
||||
|
||||
#: The engagement range of CAP flights. Any enemy aircraft within this range
|
||||
#: of the CAP's current position will be engaged by the CAP.
|
||||
cap_engagement_range: Distance
|
||||
|
||||
cas_duration: timedelta
|
||||
|
||||
sweep_distance: int
|
||||
sweep_distance: Distance
|
||||
|
||||
|
||||
MODERN_DOCTRINE = Doctrine(
|
||||
@@ -45,26 +57,25 @@ MODERN_DOCTRINE = Doctrine(
|
||||
sead=True,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
strike_max_range=1500000,
|
||||
sead_max_range=1500000,
|
||||
rendezvous_altitude=feet_to_meter(25000),
|
||||
hold_distance=nm_to_meter(15),
|
||||
push_distance=nm_to_meter(20),
|
||||
join_distance=nm_to_meter(20),
|
||||
split_distance=nm_to_meter(20),
|
||||
ingress_egress_distance=nm_to_meter(45),
|
||||
ingress_altitude=feet_to_meter(20000),
|
||||
egress_altitude=feet_to_meter(20000),
|
||||
min_patrol_altitude=feet_to_meter(15000),
|
||||
max_patrol_altitude=feet_to_meter(33000),
|
||||
pattern_altitude=feet_to_meter(5000),
|
||||
rendezvous_altitude=feet(25000),
|
||||
hold_distance=nautical_miles(15),
|
||||
push_distance=nautical_miles(20),
|
||||
join_distance=nautical_miles(20),
|
||||
split_distance=nautical_miles(20),
|
||||
ingress_egress_distance=nautical_miles(45),
|
||||
ingress_altitude=feet(20000),
|
||||
egress_altitude=feet(20000),
|
||||
min_patrol_altitude=feet(15000),
|
||||
max_patrol_altitude=feet(33000),
|
||||
pattern_altitude=feet(5000),
|
||||
cap_duration=timedelta(minutes=30),
|
||||
cap_min_track_length=nm_to_meter(15),
|
||||
cap_max_track_length=nm_to_meter(40),
|
||||
cap_min_distance_from_cp=nm_to_meter(10),
|
||||
cap_max_distance_from_cp=nm_to_meter(40),
|
||||
cap_min_track_length=nautical_miles(15),
|
||||
cap_max_track_length=nautical_miles(40),
|
||||
cap_min_distance_from_cp=nautical_miles(10),
|
||||
cap_max_distance_from_cp=nautical_miles(40),
|
||||
cap_engagement_range=nautical_miles(50),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nm_to_meter(60),
|
||||
sweep_distance=nautical_miles(60),
|
||||
)
|
||||
|
||||
COLDWAR_DOCTRINE = Doctrine(
|
||||
@@ -73,26 +84,25 @@ COLDWAR_DOCTRINE = Doctrine(
|
||||
sead=True,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
strike_max_range=1500000,
|
||||
sead_max_range=1500000,
|
||||
rendezvous_altitude=feet_to_meter(22000),
|
||||
hold_distance=nm_to_meter(10),
|
||||
push_distance=nm_to_meter(10),
|
||||
join_distance=nm_to_meter(10),
|
||||
split_distance=nm_to_meter(10),
|
||||
ingress_egress_distance=nm_to_meter(30),
|
||||
ingress_altitude=feet_to_meter(18000),
|
||||
egress_altitude=feet_to_meter(18000),
|
||||
min_patrol_altitude=feet_to_meter(10000),
|
||||
max_patrol_altitude=feet_to_meter(24000),
|
||||
pattern_altitude=feet_to_meter(5000),
|
||||
rendezvous_altitude=feet(22000),
|
||||
hold_distance=nautical_miles(10),
|
||||
push_distance=nautical_miles(10),
|
||||
join_distance=nautical_miles(10),
|
||||
split_distance=nautical_miles(10),
|
||||
ingress_egress_distance=nautical_miles(30),
|
||||
ingress_altitude=feet(18000),
|
||||
egress_altitude=feet(18000),
|
||||
min_patrol_altitude=feet(10000),
|
||||
max_patrol_altitude=feet(24000),
|
||||
pattern_altitude=feet(5000),
|
||||
cap_duration=timedelta(minutes=30),
|
||||
cap_min_track_length=nm_to_meter(12),
|
||||
cap_max_track_length=nm_to_meter(24),
|
||||
cap_min_distance_from_cp=nm_to_meter(8),
|
||||
cap_max_distance_from_cp=nm_to_meter(25),
|
||||
cap_min_track_length=nautical_miles(12),
|
||||
cap_max_track_length=nautical_miles(24),
|
||||
cap_min_distance_from_cp=nautical_miles(8),
|
||||
cap_max_distance_from_cp=nautical_miles(25),
|
||||
cap_engagement_range=nautical_miles(35),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nm_to_meter(40),
|
||||
sweep_distance=nautical_miles(40),
|
||||
)
|
||||
|
||||
WWII_DOCTRINE = Doctrine(
|
||||
@@ -101,24 +111,23 @@ WWII_DOCTRINE = Doctrine(
|
||||
sead=False,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
strike_max_range=1500000,
|
||||
sead_max_range=1500000,
|
||||
hold_distance=nm_to_meter(5),
|
||||
push_distance=nm_to_meter(5),
|
||||
join_distance=nm_to_meter(5),
|
||||
split_distance=nm_to_meter(5),
|
||||
rendezvous_altitude=feet_to_meter(10000),
|
||||
ingress_egress_distance=nm_to_meter(7),
|
||||
ingress_altitude=feet_to_meter(8000),
|
||||
egress_altitude=feet_to_meter(8000),
|
||||
min_patrol_altitude=feet_to_meter(4000),
|
||||
max_patrol_altitude=feet_to_meter(15000),
|
||||
pattern_altitude=feet_to_meter(5000),
|
||||
hold_distance=nautical_miles(5),
|
||||
push_distance=nautical_miles(5),
|
||||
join_distance=nautical_miles(5),
|
||||
split_distance=nautical_miles(5),
|
||||
rendezvous_altitude=feet(10000),
|
||||
ingress_egress_distance=nautical_miles(7),
|
||||
ingress_altitude=feet(8000),
|
||||
egress_altitude=feet(8000),
|
||||
min_patrol_altitude=feet(4000),
|
||||
max_patrol_altitude=feet(15000),
|
||||
pattern_altitude=feet(5000),
|
||||
cap_duration=timedelta(minutes=30),
|
||||
cap_min_track_length=nm_to_meter(8),
|
||||
cap_max_track_length=nm_to_meter(18),
|
||||
cap_min_distance_from_cp=nm_to_meter(0),
|
||||
cap_max_distance_from_cp=nm_to_meter(5),
|
||||
cap_min_track_length=nautical_miles(8),
|
||||
cap_max_track_length=nautical_miles(18),
|
||||
cap_min_distance_from_cp=nautical_miles(0),
|
||||
cap_max_distance_from_cp=nautical_miles(5),
|
||||
cap_engagement_range=nautical_miles(20),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nm_to_meter(10),
|
||||
sweep_distance=nautical_miles(10),
|
||||
)
|
||||
|
||||
326
game/data/weapons.py
Normal file
326
game/data/weapons.py
Normal file
@@ -0,0 +1,326 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import inspect
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Iterator, Optional, Set, Tuple, Type, Union, cast
|
||||
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
from dcs.unittype import FlyingType
|
||||
from dcs.weapons_data import Weapons, weapon_ids
|
||||
|
||||
|
||||
PydcsWeapon = Dict[str, Union[int, str]]
|
||||
PydcsWeaponAssignment = Tuple[int, PydcsWeapon]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Weapon:
|
||||
"""Wraps a pydcs weapon dict in a hashable type."""
|
||||
|
||||
cls_id: str
|
||||
name: str
|
||||
weight: int
|
||||
|
||||
def available_on(self, date: datetime.date) -> bool:
|
||||
introduction_year = WEAPON_INTRODUCTION_YEARS.get(self)
|
||||
if introduction_year is None:
|
||||
logging.warning(
|
||||
f"No introduction year for {self}, assuming always available")
|
||||
return True
|
||||
return date >= datetime.date(introduction_year, 1, 1)
|
||||
|
||||
@property
|
||||
def as_pydcs(self) -> PydcsWeapon:
|
||||
return {
|
||||
"clsid": self.cls_id,
|
||||
"name": self.name,
|
||||
"weight": self.weight,
|
||||
}
|
||||
|
||||
@property
|
||||
def fallbacks(self) -> Iterator[Weapon]:
|
||||
yield self
|
||||
fallback = WEAPON_FALLBACK_MAP[self]
|
||||
if fallback is not None:
|
||||
yield from fallback.fallbacks
|
||||
|
||||
@classmethod
|
||||
def from_pydcs(cls, weapon_data: PydcsWeapon) -> Weapon:
|
||||
return cls(
|
||||
cast(str, weapon_data["clsid"]),
|
||||
cast(str, weapon_data["name"]),
|
||||
cast(int, weapon_data["weight"])
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_clsid(cls, clsid: str) -> Optional[Weapon]:
|
||||
data = weapon_ids.get(clsid)
|
||||
if data is None:
|
||||
return None
|
||||
return cls.from_pydcs(data)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Pylon:
|
||||
number: int
|
||||
allowed: Set[Weapon]
|
||||
|
||||
def can_equip(self, weapon: Weapon) -> bool:
|
||||
return weapon in self.allowed
|
||||
|
||||
def equip(self, group: FlyingGroup, weapon: Weapon) -> None:
|
||||
if not self.can_equip(weapon):
|
||||
raise ValueError(f"Pylon {self.number} cannot equip {weapon.name}")
|
||||
group.load_pylon(self.make_pydcs_assignment(weapon), self.number)
|
||||
|
||||
def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment:
|
||||
return self.number, weapon.as_pydcs
|
||||
|
||||
def available_on(self, date: datetime.date) -> Iterator[Weapon]:
|
||||
for weapon in self.allowed:
|
||||
if weapon.available_on(date):
|
||||
yield weapon
|
||||
|
||||
@classmethod
|
||||
def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon:
|
||||
# In pydcs these are all arbitrary inner classes of the aircraft type.
|
||||
# The only way to identify them is by their name.
|
||||
pylons = [v for v in aircraft.__dict__.values() if
|
||||
inspect.isclass(v) and v.__name__.startswith("Pylon")]
|
||||
|
||||
# And that Pylon class has members with irrelevant names that have
|
||||
# values of (pylon number, allowed weapon).
|
||||
allowed = set()
|
||||
for pylon in pylons:
|
||||
for key, value in pylon.__dict__.items():
|
||||
if key.startswith("__"):
|
||||
continue
|
||||
pylon_number, weapon = value
|
||||
if pylon_number != number:
|
||||
continue
|
||||
allowed.add(Weapon.from_pydcs(weapon))
|
||||
|
||||
return cls(number, allowed)
|
||||
|
||||
@classmethod
|
||||
def iter_pylons(cls, aircraft: Type[FlyingType]) -> Iterator[Pylon]:
|
||||
for pylon in sorted(list(aircraft.pylons)):
|
||||
yield cls.for_aircraft(aircraft, pylon)
|
||||
|
||||
|
||||
_WEAPON_FALLBACKS = [
|
||||
# AIM-120C
|
||||
(Weapons.AIM_120C, Weapons.AIM_120B),
|
||||
(Weapons.LAU_115___AIM_120C, Weapons.LAU_115___AIM_120B),
|
||||
(Weapons.LAU_115_2_LAU_127_AIM_120C, Weapons.LAU_115_2_LAU_127_AIM_120B),
|
||||
|
||||
# AIM-120B
|
||||
(Weapons.AIM_120B, Weapons.AIM_7MH),
|
||||
(Weapons.LAU_115___AIM_120B, Weapons.LAU_115C_AIM_7MH),
|
||||
(Weapons.LAU_115_2_LAU_127_AIM_120B, Weapons.LAU_115C_AIM_7MH),
|
||||
|
||||
# AIM-7MH
|
||||
(Weapons.AIM_7MH, Weapons.AIM_7M),
|
||||
(Weapons.AIM_7MH_, Weapons.AIM_7M_),
|
||||
(Weapons.AIM_7MH__, Weapons.AIM_7M__),
|
||||
(Weapons.LAU_115C_AIM_7MH, Weapons.LAU_115___AIM_7M),
|
||||
|
||||
# AIM-7M
|
||||
(Weapons.AIM_7M, Weapons.AIM_7F),
|
||||
(Weapons.AIM_7M_, None),
|
||||
(Weapons.AIM_7M__, None),
|
||||
(Weapons.LAU_115___AIM_7M, Weapons.LAU_115C_AIM_7F),
|
||||
|
||||
# AIM-7F
|
||||
(Weapons.AIM_7F, Weapons.AIM_7E),
|
||||
(Weapons.AIM_7F_, Weapons.AIM_7E),
|
||||
(Weapons.AIM_7F__, Weapons.AIM_7E),
|
||||
(Weapons.LAU_115C_AIM_7F, Weapons.LAU_115C_AIM_7E),
|
||||
|
||||
# AIM-7E
|
||||
(Weapons.AIM_7E, Weapons.AIM_9X_Sidewinder_IR_AAM),
|
||||
(Weapons.LAU_115C_AIM_7E, Weapons.LAU_115_LAU_127_AIM_9X),
|
||||
|
||||
# AIM-9X
|
||||
(Weapons.AIM_9X_Sidewinder_IR_AAM, Weapons.AIM_9P5_Sidewinder_IR_AAM),
|
||||
(Weapons.LAU_7_AIM_9X_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM),
|
||||
(Weapons.LAU_115_LAU_127_AIM_9X, Weapons.LAU_115_LAU_127_AIM_9M),
|
||||
(Weapons.LAU_115_2_LAU_127_AIM_9X, Weapons.LAU_115_2_LAU_127_AIM_9M),
|
||||
(Weapons.LAU_127_AIM_9X, Weapons.LAU_127_AIM_9M),
|
||||
|
||||
# AIM-9P5
|
||||
(Weapons.AIM_9P5_Sidewinder_IR_AAM, Weapons.AIM_9P_Sidewinder_IR_AAM),
|
||||
(Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM),
|
||||
|
||||
# AIM-9P
|
||||
(Weapons.AIM_9P_Sidewinder_IR_AAM, Weapons.AIM_9M_Sidewinder_IR_AAM),
|
||||
(Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM),
|
||||
|
||||
# AIM-9M
|
||||
(Weapons.AIM_9M_Sidewinder_IR_AAM, Weapons.AIM_9L_Sidewinder_IR_AAM),
|
||||
(Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9L),
|
||||
|
||||
# AIM-9L
|
||||
(Weapons.AIM_9L_Sidewinder_IR_AAM, None),
|
||||
(Weapons.LAU_7_AIM_9L, None),
|
||||
|
||||
# AIM-54C Mk47
|
||||
(Weapons.AIM_54C_Mk47, Weapons.AIM_54A_Mk60),
|
||||
(Weapons.AIM_54C_Mk47_, Weapons.AIM_54A_Mk60_),
|
||||
(Weapons.AIM_54C_Mk47__, Weapons.AIM_54A_Mk60__),
|
||||
|
||||
# AIM-54A Mk60
|
||||
(Weapons.AIM_54A_Mk60, Weapons.AIM_54A_Mk47),
|
||||
(Weapons.AIM_54A_Mk60_, Weapons.AIM_54A_Mk47_),
|
||||
(Weapons.AIM_54A_Mk60__, Weapons.AIM_54A_Mk47__),
|
||||
|
||||
# R-27 (AA-10 Alamo)
|
||||
(Weapons.R_27ER, Weapons.R_27R),
|
||||
(Weapons.R_27ET, Weapons.R_27T),
|
||||
|
||||
# R-77 (AA-12)
|
||||
(Weapons.R_77, Weapons.R_27ER),
|
||||
(Weapons.R_77_, Weapons.R_27ER),
|
||||
|
||||
# R-73 (AA-11)
|
||||
(Weapons.R_73, Weapons.R_60M),
|
||||
(Weapons.R_73_, Weapons.R_60M_),
|
||||
|
||||
# GBU-38 (JDAM)
|
||||
(Weapons.GBU_38, Weapons.GBU_12),
|
||||
(Weapons.GBU_38_16, Weapons.MK_82_28), # B1-B only
|
||||
(Weapons._2_GBU_38_, Weapons._2_GBU_12),
|
||||
(Weapons._2_GBU_38, Weapons._2_GBU_12),
|
||||
(Weapons._3_GBU_38, Weapons._3_GBU_12),
|
||||
(Weapons.BRU_55___2_x_GBU_38, Weapons.BRU_33___2_x_GBU_12),
|
||||
(Weapons.BRU_57___2_x_GBU_38, Weapons.BRU_33___2_x_GBU_12),
|
||||
|
||||
# AGM-154A (JSOW)
|
||||
(Weapons.AGM_154A, Weapons.GBU_12),
|
||||
(Weapons.BRU_55___2_x_AGM_154A, Weapons.BRU_33___2_x_GBU_12),
|
||||
(Weapons.BRU_57___2_x_AGM_154A, Weapons.BRU_33___2_x_GBU_12),
|
||||
|
||||
# AGM-154C (JSOW)
|
||||
(Weapons.AGM_154C, Weapons.GBU_12),
|
||||
(Weapons.AGM_154C_4, Weapons.MK_82_28), # B1-B only
|
||||
(Weapons.BRU_55___2_x_AGM_154C, Weapons.BRU_33___2_x_GBU_12),
|
||||
|
||||
# AGM-84E (SLAM)
|
||||
(Weapons.AGM_84E, Weapons.LAU_117_AGM_65F),
|
||||
|
||||
# CBU-97
|
||||
(Weapons.CBU_97, Weapons.GBU_12),
|
||||
(Weapons.TER_9A___2_x_CBU_97, Weapons.TER_9A___2_x_GBU_12),
|
||||
(Weapons.TER_9A___2_x_CBU_97_, Weapons.TER_9A___2_x_GBU_12),
|
||||
(Weapons.TER_9A___3_x_CBU_97, Weapons.TER_9A___2_x_GBU_12),
|
||||
|
||||
]
|
||||
|
||||
WEAPON_FALLBACK_MAP: Dict[Weapon, Optional[Weapon]] = defaultdict(
|
||||
lambda: cast(Optional[Weapon], None),
|
||||
((Weapon.from_pydcs(a), b if b is None else Weapon.from_pydcs(b))
|
||||
for a, b in _WEAPON_FALLBACKS))
|
||||
|
||||
|
||||
WEAPON_INTRODUCTION_YEARS = {
|
||||
# AIM-120C
|
||||
Weapon.from_pydcs(Weapons.AIM_120C): 1996,
|
||||
Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_120C): 1996,
|
||||
Weapon.from_pydcs(Weapons.LAU_115___AIM_120C): 1996,
|
||||
|
||||
# AIM-120B
|
||||
Weapon.from_pydcs(Weapons.AIM_120B): 1994,
|
||||
Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_120B): 1994,
|
||||
Weapon.from_pydcs(Weapons.LAU_115___AIM_120B): 1994,
|
||||
|
||||
# AIM-7MH
|
||||
Weapon.from_pydcs(Weapons.AIM_7MH): 1987,
|
||||
Weapon.from_pydcs(Weapons.AIM_7MH_): 1987,
|
||||
Weapon.from_pydcs(Weapons.AIM_7MH__): 1987,
|
||||
Weapon.from_pydcs(Weapons.LAU_115C_AIM_7MH): 1987,
|
||||
|
||||
# AIM-7M
|
||||
Weapon.from_pydcs(Weapons.AIM_7M): 1982,
|
||||
Weapon.from_pydcs(Weapons.AIM_7M_): 1982,
|
||||
Weapon.from_pydcs(Weapons.AIM_7M__): 1982,
|
||||
Weapon.from_pydcs(Weapons.LAU_115___AIM_7M): 1982,
|
||||
|
||||
# AIM-7F
|
||||
Weapon.from_pydcs(Weapons.AIM_7F): 1976,
|
||||
Weapon.from_pydcs(Weapons.AIM_7F_): 1976,
|
||||
Weapon.from_pydcs(Weapons.AIM_7F__): 1976,
|
||||
Weapon.from_pydcs(Weapons.LAU_115C_AIM_7F): 1976,
|
||||
|
||||
# AIM-7E
|
||||
Weapon.from_pydcs(Weapons.AIM_7E): 1963,
|
||||
Weapon.from_pydcs(Weapons.LAU_115C_AIM_7E): 1963,
|
||||
|
||||
# AIM-9X
|
||||
Weapon.from_pydcs(Weapons.AIM_9X_Sidewinder_IR_AAM): 2003,
|
||||
Weapon.from_pydcs(Weapons.LAU_7_AIM_9X_Sidewinder_IR_AAM): 2003,
|
||||
Weapon.from_pydcs(Weapons.LAU_115_LAU_127_AIM_9X): 2003,
|
||||
Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_9X): 2003,
|
||||
Weapon.from_pydcs(Weapons.LAU_127_AIM_9X): 2003,
|
||||
|
||||
# AIM-9P5
|
||||
Weapon.from_pydcs(Weapons.AIM_9P5_Sidewinder_IR_AAM): 1963,
|
||||
Weapon.from_pydcs(Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM): 1963,
|
||||
|
||||
# AIM-9P
|
||||
Weapon.from_pydcs(Weapons.AIM_9P_Sidewinder_IR_AAM): 1978,
|
||||
Weapon.from_pydcs(Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM): 1978,
|
||||
|
||||
# AIM-9M
|
||||
Weapon.from_pydcs(Weapons.AIM_9M_Sidewinder_IR_AAM): 1983,
|
||||
Weapon.from_pydcs(Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM): 1983,
|
||||
|
||||
# AIM-9L
|
||||
Weapon.from_pydcs(Weapons.AIM_9L_Sidewinder_IR_AAM): 1977,
|
||||
Weapon.from_pydcs(Weapons.LAU_7_AIM_9L): 1977,
|
||||
|
||||
# AIM-54C-Mk47
|
||||
Weapon.from_pydcs(Weapons.AIM_54C_Mk47): 1986,
|
||||
Weapon.from_pydcs(Weapons.AIM_54C_Mk47_): 1986,
|
||||
Weapon.from_pydcs(Weapons.AIM_54C_Mk47__): 1986,
|
||||
Weapon.from_pydcs(Weapons.AIM_54C): 1986, # this weapon id is unused (legacy F-14A)
|
||||
|
||||
# R-77 (AA-12)
|
||||
Weapon.from_pydcs(Weapons.R_77): 2002,
|
||||
Weapon.from_pydcs(Weapons.R_77_): 2002,
|
||||
|
||||
# R-73 (AA-11)
|
||||
Weapon.from_pydcs(Weapons.R_73): 1989,
|
||||
Weapon.from_pydcs(Weapons.R_73_): 1989,
|
||||
|
||||
# GBU-38 (JDAM)
|
||||
Weapon.from_pydcs(Weapons.GBU_38): 1998,
|
||||
Weapon.from_pydcs(Weapons.GBU_38_16): 1998, # B1-B only
|
||||
Weapon.from_pydcs(Weapons._2_GBU_38_): 1998,
|
||||
Weapon.from_pydcs(Weapons._2_GBU_38): 1998,
|
||||
Weapon.from_pydcs(Weapons._3_GBU_38): 1998,
|
||||
Weapon.from_pydcs(Weapons.BRU_55___2_x_GBU_38): 1998,
|
||||
Weapon.from_pydcs(Weapons.BRU_57___2_x_GBU_38): 1998,
|
||||
|
||||
# AGM-154A (JSOW)
|
||||
Weapon.from_pydcs(Weapons.AGM_154A): 1999,
|
||||
Weapon.from_pydcs(Weapons.BRU_55___2_x_AGM_154A): 1999,
|
||||
Weapon.from_pydcs(Weapons.BRU_57___2_x_AGM_154A): 1999,
|
||||
|
||||
# AGM-154C (JSOW)
|
||||
Weapon.from_pydcs(Weapons.AGM_154C): 2005,
|
||||
Weapon.from_pydcs(Weapons.AGM_154C_4): 2005, # B1-B only
|
||||
Weapon.from_pydcs(Weapons.BRU_55___2_x_AGM_154C): 2005,
|
||||
|
||||
# AGM-84E
|
||||
Weapon.from_pydcs(Weapons.AGM_84E): 1990,
|
||||
|
||||
# CBU-97
|
||||
Weapon.from_pydcs(Weapons.CBU_97): 1995,
|
||||
Weapon.from_pydcs(Weapons.TER_9A___2_x_CBU_97): 1995,
|
||||
Weapon.from_pydcs(Weapons.TER_9A___2_x_CBU_97_): 1995,
|
||||
Weapon.from_pydcs(Weapons.TER_9A___3_x_CBU_97): 1995
|
||||
|
||||
}
|
||||
268
game/db.py
268
game/db.py
@@ -1,6 +1,8 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Tuple, Type, Union
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from dcs.countries import country_dict
|
||||
from dcs.helicopters import (
|
||||
@@ -17,6 +19,7 @@ from dcs.helicopters import (
|
||||
SA342M,
|
||||
SA342Minigun,
|
||||
SA342Mistral,
|
||||
SH_60B,
|
||||
UH_1H,
|
||||
UH_60A,
|
||||
helicopter_map,
|
||||
@@ -40,12 +43,14 @@ from dcs.planes import (
|
||||
C_101CC,
|
||||
C_130,
|
||||
E_3A,
|
||||
E_2C,
|
||||
FA_18C_hornet,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_117A,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_111F,
|
||||
F_15C,
|
||||
F_15E,
|
||||
F_16A,
|
||||
@@ -61,6 +66,7 @@ from dcs.planes import (
|
||||
Ju_88A4,
|
||||
KC130,
|
||||
KC_135,
|
||||
KC135MPRS,
|
||||
KJ_2000,
|
||||
L_39C,
|
||||
L_39ZA,
|
||||
@@ -84,6 +90,7 @@ from dcs.planes import (
|
||||
P_51D_30_NA,
|
||||
PlaneType,
|
||||
RQ_1A_Predator,
|
||||
S_3B,
|
||||
S_3B_Tanker,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
@@ -130,6 +137,7 @@ from dcs.task import (
|
||||
CargoTransportation,
|
||||
Embarking,
|
||||
Escort,
|
||||
FighterSweep,
|
||||
GroundAttack,
|
||||
Intercept,
|
||||
MainTask,
|
||||
@@ -157,6 +165,7 @@ from dcs.vehicles import (
|
||||
)
|
||||
|
||||
import pydcs_extensions.frenchpack.frenchpack as frenchpack
|
||||
import pydcs_extensions.highdigitsams.highdigitsams as highdigitsams
|
||||
# PATCH pydcs data with MODS
|
||||
from game.factions.faction_loader import FactionLoader
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
@@ -166,6 +175,8 @@ from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
|
||||
UNITINFOTEXT_PATH = Path("./resources/units/unit_info_text.json")
|
||||
|
||||
plane_map["A-4E-C"] = A_4E_C
|
||||
plane_map["MB-339PAN"] = MB_339PAN
|
||||
plane_map["Rafale_M"] = Rafale_M
|
||||
@@ -209,6 +220,49 @@ vehicle_map["Toyota_vert"] = frenchpack.DIM__TOYOTA_GREEN
|
||||
vehicle_map["Toyota_desert"] = frenchpack.DIM__TOYOTA_DESERT
|
||||
vehicle_map["Kamikaze"] = frenchpack.DIM__KAMIKAZE
|
||||
|
||||
vehicle_map[highdigitsams.AAA_SON_9_Fire_Can.id] = highdigitsams.AAA_SON_9_Fire_Can
|
||||
vehicle_map[highdigitsams.AAA_100mm_KS_19.id] = highdigitsams.AAA_100mm_KS_19
|
||||
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_54K6_CP.id] = highdigitsams.SAM_SA_10B_S_300PS_54K6_CP
|
||||
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN.id] = highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN
|
||||
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN.id] = highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN
|
||||
vehicle_map[highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE.id] = highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE
|
||||
vehicle_map[highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE.id] = highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE
|
||||
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_30N6_TR.id] = highdigitsams.SAM_SA_10B_S_300PS_30N6_TR
|
||||
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR.id] = highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR
|
||||
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR.id] = highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR
|
||||
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR.id] = highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR
|
||||
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6.id] = highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6
|
||||
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E.id] = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E
|
||||
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck.id] = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck
|
||||
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E.id] = highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E
|
||||
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E.id] = highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E
|
||||
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE.id] = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE
|
||||
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE.id] = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE
|
||||
vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2.id] = highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2
|
||||
vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck.id] = highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck
|
||||
vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2.id] = highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2
|
||||
vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2.id] = highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2
|
||||
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S457_CP.id] = highdigitsams.SAM_SA_12_S_300V_9S457_CP
|
||||
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9A82_LN.id] = highdigitsams.SAM_SA_12_S_300V_9A82_LN
|
||||
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9A83_LN.id] = highdigitsams.SAM_SA_12_S_300V_9A83_LN
|
||||
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S15_SR.id] = highdigitsams.SAM_SA_12_S_300V_9S15_SR
|
||||
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S19_SR.id] = highdigitsams.SAM_SA_12_S_300V_9S19_SR
|
||||
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S32_TR.id] = highdigitsams.SAM_SA_12_S_300V_9S32_TR
|
||||
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP.id] = highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP
|
||||
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR.id] = highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR
|
||||
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR.id] = highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR
|
||||
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR.id] = highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR
|
||||
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN.id] = highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN
|
||||
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN.id] = highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN
|
||||
vehicle_map[highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2.id] = highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2
|
||||
vehicle_map[highdigitsams.SAM_SA_2__V759__LN_SM_90.id] = highdigitsams.SAM_SA_2__V759__LN_SM_90
|
||||
vehicle_map[highdigitsams.SAM_HQ_2_LN_SM_90.id] = highdigitsams.SAM_HQ_2_LN_SM_90
|
||||
vehicle_map[highdigitsams.SAM_SA_3__V_601P__LN_5P73.id] = highdigitsams.SAM_SA_3__V_601P__LN_5P73
|
||||
vehicle_map[highdigitsams.SAM_SA_24_Igla_S_manpad.id] = highdigitsams.SAM_SA_24_Igla_S_manpad
|
||||
vehicle_map[highdigitsams.SAM_SA_14_Strela_3_manpad.id] = highdigitsams.SAM_SA_14_Strela_3_manpad
|
||||
vehicle_map[highdigitsams.Polyana_D4M1_C2_node.id] = highdigitsams.Polyana_D4M1_C2_node
|
||||
vehicle_map[highdigitsams._34Ya6E_Gazetchik_E_decoy.id] = highdigitsams._34Ya6E_Gazetchik_E_decoy
|
||||
|
||||
"""
|
||||
---------- BEGINNING OF CONFIGURATION SECTION
|
||||
"""
|
||||
@@ -302,6 +356,7 @@ PRICES = {
|
||||
A_10A: 16,
|
||||
A_10C: 22,
|
||||
A_10C_2: 24,
|
||||
S_3B: 10,
|
||||
|
||||
# heli
|
||||
Ka_50: 13,
|
||||
@@ -317,6 +372,7 @@ PRICES = {
|
||||
AH_64A: 24,
|
||||
AH_64D: 30,
|
||||
OH_58D: 6,
|
||||
SH_60B: 6,
|
||||
|
||||
# Bombers
|
||||
B_52H: 35,
|
||||
@@ -325,6 +381,7 @@ PRICES = {
|
||||
Tu_160: 50,
|
||||
Tu_22M3: 40,
|
||||
Tu_95MS: 35,
|
||||
F_111F: 21,
|
||||
|
||||
# special
|
||||
IL_76MD: 30,
|
||||
@@ -335,10 +392,12 @@ PRICES = {
|
||||
IL_78M: 25,
|
||||
KC_135: 25,
|
||||
KC130: 25,
|
||||
KC135MPRS: 25,
|
||||
|
||||
A_50: 50,
|
||||
KJ_2000: 50,
|
||||
E_3A: 50,
|
||||
E_2C: 50,
|
||||
C_130: 25,
|
||||
Hercules: 25,
|
||||
|
||||
@@ -362,12 +421,14 @@ PRICES = {
|
||||
|
||||
# armor
|
||||
Armor.APC_MTLB: 4,
|
||||
Armor.FDDM_Grad: 5,
|
||||
Armor.FDDM_Grad: 4,
|
||||
Armor.ARV_BRDM_2: 6,
|
||||
Armor.ARV_BTR_RD: 8,
|
||||
Armor.ARV_BTR_RD: 6,
|
||||
Armor.APC_BTR_80: 8,
|
||||
Armor.APC_BTR_82A: 10,
|
||||
Armor.MBT_T_55: 18,
|
||||
Armor.MBT_T_72B: 22,
|
||||
Armor.MBT_T_72B: 20,
|
||||
Armor.MBT_T_72B3: 25,
|
||||
Armor.MBT_T_80U: 25,
|
||||
Armor.MBT_T_90: 30,
|
||||
Armor.IFV_BMD_1: 8,
|
||||
@@ -383,6 +444,7 @@ PRICES = {
|
||||
Armor.ATGM_M1045_HMMWV_TOW: 8,
|
||||
Armor.IFV_M2A2_Bradley: 12,
|
||||
Armor.APC_M1126_Stryker_ICV: 10,
|
||||
Armor.SPG_M1128_Stryker_MGS: 14,
|
||||
Armor.ATGM_M1134_Stryker: 12,
|
||||
Armor.MBT_M60A3_Patton: 16,
|
||||
Armor.MBT_M1A2_Abrams: 25,
|
||||
@@ -406,6 +468,8 @@ PRICES = {
|
||||
Artillery.MLRS_BM_21_Grad: 15,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27: 50,
|
||||
Artillery.MLRS_9A52_Smerch: 40,
|
||||
Artillery._2B11_mortar: 4,
|
||||
Artillery.SpGH_Dana: 26,
|
||||
|
||||
Unarmed.Transport_UAZ_469: 3,
|
||||
Unarmed.Transport_Ural_375: 3,
|
||||
@@ -434,6 +498,7 @@ PRICES = {
|
||||
Armor.LAC_M8_Greyhound: 8,
|
||||
Armor.TD_M10_GMC: 14,
|
||||
Armor.StuG_III_Ausf__G: 12,
|
||||
Armor.StuG_IV: 14,
|
||||
Artillery.M12_GMC: 10,
|
||||
Artillery.Sturmpanzer_IV_Brummbär: 10,
|
||||
Armor.Daimler_Armoured_Car: 8,
|
||||
@@ -454,14 +519,14 @@ PRICES = {
|
||||
AirDefence.SAM_SA_19_Tunguska_2S6: 30,
|
||||
AirDefence.SAM_SA_6_Kub_LN_2P25: 20,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73: 6,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85C: 22,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85D: 22,
|
||||
|
||||
AirDefence.SAM_SA_11_Buk_LN_9A310M1: 30,
|
||||
AirDefence.SAM_SA_11_Buk_CC_9S470M1: 25,
|
||||
AirDefence.SAM_SA_11_Buk_SR_9S18M1: 28,
|
||||
AirDefence.SAM_SA_8_Osa_9A33: 28,
|
||||
AirDefence.SAM_SA_15_Tor_9A331: 40,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_9A35M3: 16,
|
||||
AirDefence.SAM_SA_9_Strela_1_9P31: 12,
|
||||
AirDefence.SAM_SA_11_Buk_CC_9S470M1: 25,
|
||||
AirDefence.SAM_SA_8_Osa_LD_9T217: 22,
|
||||
AirDefence.SAM_Patriot_AMG_AN_MRC_137: 35,
|
||||
AirDefence.SAM_Patriot_ECS_AN_MSQ_104: 30,
|
||||
@@ -476,16 +541,16 @@ PRICES = {
|
||||
AirDefence.SAM_Patriot_EPP_III: 15,
|
||||
AirDefence.SAM_Patriot_ICC: 18,
|
||||
AirDefence.SAM_Roland_ADS: 12,
|
||||
AirDefence.SAM_SA_10_S_300PS_CP_54K6: 18,
|
||||
AirDefence.Stinger_MANPADS: 6,
|
||||
AirDefence.SAM_Stinger_comm_dsr: 4,
|
||||
AirDefence.SAM_Stinger_comm: 4,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka: 10,
|
||||
AirDefence.AAA_ZSU_57_2: 12,
|
||||
AirDefence.AAA_ZU_23_Closed: 6,
|
||||
AirDefence.AAA_ZU_23_Emplacement: 6,
|
||||
AirDefence.AAA_ZU_23_on_Ural_375: 8,
|
||||
AirDefence.AAA_ZU_23_on_Ural_375: 7,
|
||||
AirDefence.AAA_ZU_23_Insurgent_Closed: 6,
|
||||
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375: 8,
|
||||
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375: 7,
|
||||
AirDefence.AAA_ZU_23_Insurgent: 6,
|
||||
AirDefence.SAM_SA_18_Igla_MANPADS: 10,
|
||||
AirDefence.SAM_SA_18_Igla_comm: 8,
|
||||
@@ -493,11 +558,8 @@ PRICES = {
|
||||
AirDefence.SAM_SA_18_Igla_S_comm: 8,
|
||||
AirDefence.EWR_1L13: 30,
|
||||
AirDefence.SAM_SA_6_Kub_STR_9S91: 22,
|
||||
AirDefence.SAM_SA_10_S_300PS_TR_30N6: 24,
|
||||
AirDefence.SAM_SA_10_S_300PS_SR_5N66M: 30,
|
||||
|
||||
AirDefence.EWR_55G6: 30,
|
||||
AirDefence.SAM_SA_10_S_300PS_SR_64H6E: 30,
|
||||
AirDefence.SAM_SA_11_Buk_SR_9S18M1: 28,
|
||||
AirDefence.CP_9S80M1_Sborka: 10,
|
||||
AirDefence.SAM_Hawk_TR_AN_MPQ_46: 14,
|
||||
AirDefence.SAM_Hawk_SR_AN_MPQ_50: 18,
|
||||
@@ -558,6 +620,57 @@ PRICES = {
|
||||
frenchpack.DIM__TOYOTA_DESERT: 2,
|
||||
frenchpack.DIM__KAMIKAZE: 6,
|
||||
|
||||
# SA-10
|
||||
AirDefence.SAM_SA_10_S_300PS_CP_54K6: 18,
|
||||
AirDefence.SAM_SA_10_S_300PS_TR_30N6: 24,
|
||||
AirDefence.SAM_SA_10_S_300PS_SR_5N66M: 30,
|
||||
AirDefence.SAM_SA_10_S_300PS_SR_64H6E: 30,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85C: 22,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85D: 22,
|
||||
|
||||
# High digit sams mod
|
||||
highdigitsams.AAA_SON_9_Fire_Can: 8,
|
||||
highdigitsams.AAA_100mm_KS_19: 10,
|
||||
|
||||
highdigitsams.SAM_SA_10B_S_300PS_54K6_CP: 20,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN: 24,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN: 24,
|
||||
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE: 24,
|
||||
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE: 24,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_30N6_TR: 26,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR: 26,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR: 32,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR: 32,
|
||||
|
||||
highdigitsams.SAM_SA_12_S_300V_9S457_CP: 22,
|
||||
highdigitsams.SAM_SA_12_S_300V_9A82_LN: 26,
|
||||
highdigitsams.SAM_SA_12_S_300V_9A83_LN: 26,
|
||||
highdigitsams.SAM_SA_12_S_300V_9S15_SR: 34,
|
||||
highdigitsams.SAM_SA_12_S_300V_9S19_SR: 34,
|
||||
highdigitsams.SAM_SA_12_S_300V_9S32_TR: 28,
|
||||
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6: 26,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E: 30,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck: 32,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E: 38,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E: 38,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE: 28,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE: 28,
|
||||
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2: 27,
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck: 33,
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2: 40,
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2: 30,
|
||||
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP: 30,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR: 45,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR: 45,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR: 35,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN: 32,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN: 32,
|
||||
|
||||
highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2: 40,
|
||||
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -568,6 +681,7 @@ Following tasks are present:
|
||||
* CAS - CAS aircraft
|
||||
* Transport - transport aircraft (used as targets in intercept operations)
|
||||
* AWACS - awacs
|
||||
* AntishipStrike - units that will engage shipping
|
||||
* PinpointStrike - armor that will engage in ground war
|
||||
* AirDefense - AA units
|
||||
* Reconnaissance - units that will be used as targets in destroy insurgents operations
|
||||
@@ -627,6 +741,7 @@ UNIT_BY_TASK = {
|
||||
A_10C_2,
|
||||
A_20G,
|
||||
B_17G,
|
||||
F_111F,
|
||||
B_1B,
|
||||
B_52H,
|
||||
F_117A,
|
||||
@@ -649,6 +764,7 @@ UNIT_BY_TASK = {
|
||||
RQ_1A_Predator,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
S_3B,
|
||||
SA342L,
|
||||
SA342M,
|
||||
SA342Minigun,
|
||||
@@ -663,7 +779,8 @@ UNIT_BY_TASK = {
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
UH_1H,
|
||||
UH_1H,
|
||||
SH_60B,
|
||||
WingLoong_I,
|
||||
Hercules
|
||||
],
|
||||
@@ -679,8 +796,14 @@ UNIT_BY_TASK = {
|
||||
KC_135,
|
||||
KC130,
|
||||
S_3B_Tanker,
|
||||
KC135MPRS,
|
||||
],
|
||||
AWACS: [
|
||||
E_3A,
|
||||
E_2C,
|
||||
A_50,
|
||||
KJ_2000
|
||||
],
|
||||
AWACS: [E_3A, A_50, KJ_2000],
|
||||
PinpointStrike: [
|
||||
Armor.APC_MTLB,
|
||||
Armor.APC_MTLB,
|
||||
@@ -704,6 +827,8 @@ UNIT_BY_TASK = {
|
||||
Armor.APC_BTR_80,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.APC_BTR_82A,
|
||||
Armor.APC_BTR_82A,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.IFV_BMP_1,
|
||||
@@ -720,6 +845,8 @@ UNIT_BY_TASK = {
|
||||
Armor.MBT_T_55,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.MBT_T_72B3,
|
||||
Armor.MBT_T_72B3,
|
||||
Armor.MBT_T_80U,
|
||||
Armor.MBT_T_80U,
|
||||
Armor.MBT_T_90,
|
||||
@@ -748,6 +875,7 @@ UNIT_BY_TASK = {
|
||||
Armor.APC_M1126_Stryker_ICV,
|
||||
Armor.APC_M1126_Stryker_ICV,
|
||||
Armor.APC_M1126_Stryker_ICV,
|
||||
Armor.SPG_M1128_Stryker_MGS,
|
||||
Armor.IFV_MCV_80,
|
||||
Armor.IFV_MCV_80,
|
||||
Armor.IFV_MCV_80,
|
||||
@@ -812,6 +940,7 @@ UNIT_BY_TASK = {
|
||||
Armor.TD_M10_GMC,
|
||||
Armor.TD_M10_GMC,
|
||||
Armor.StuG_III_Ausf__G,
|
||||
Armor.StuG_IV,
|
||||
Artillery.M12_GMC,
|
||||
Artillery.Sturmpanzer_IV_Brummbär,
|
||||
Armor.Daimler_Armoured_Car,
|
||||
@@ -827,9 +956,34 @@ UNIT_BY_TASK = {
|
||||
Artillery.MLRS_BM_21_Grad,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27,
|
||||
Artillery.MLRS_9A52_Smerch,
|
||||
Artillery.SpGH_Dana,
|
||||
Artillery.M12_GMC,
|
||||
Artillery.Sturmpanzer_IV_Brummbär,
|
||||
|
||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
|
||||
AirDefence.AAA_ZSU_57_2,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
AirDefence.SAM_SA_8_Osa_9A33,
|
||||
AirDefence.SAM_SA_9_Strela_1_9P31,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
|
||||
AirDefence.SAM_SA_15_Tor_9A331,
|
||||
AirDefence.SAM_SA_19_Tunguska_2S6,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
AirDefence.SAM_Linebacker_M6,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
AirDefence.AAA_8_8cm_Flak_37,
|
||||
AirDefence.AAA_8_8cm_Flak_41,
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
AirDefence.AAA_M1_37mm,
|
||||
AirDefence.AA_gun_QF_3_7,
|
||||
|
||||
frenchpack.DIM__TOYOTA_BLUE,
|
||||
frenchpack.DIM__TOYOTA_DESERT,
|
||||
frenchpack.DIM__TOYOTA_GREEN,
|
||||
@@ -855,23 +1009,6 @@ UNIT_BY_TASK = {
|
||||
|
||||
],
|
||||
AirDefence: [
|
||||
|
||||
# those are listed multiple times here to balance prioritization more into lower tier AAs
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
AirDefence.SAM_Linebacker_M6,
|
||||
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
AirDefence.AAA_ZU_23_Closed,
|
||||
AirDefence.SAM_SA_9_Strela_1_9P31,
|
||||
AirDefence.SAM_SA_8_Osa_9A33,
|
||||
AirDefence.SAM_SA_19_Tunguska_2S6,
|
||||
AirDefence.SAM_SA_6_Kub_LN_2P25,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73,
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_SA_2_LN_SM_90,
|
||||
AirDefence.SAM_SA_11_Buk_LN_9A310M1,
|
||||
],
|
||||
Reconnaissance: [Unarmed.Transport_M818, Unarmed.Transport_Ural_375, Unarmed.Transport_UAZ_469],
|
||||
Nothing: [Infantry.Infantry_M4, Infantry.Soldier_AK, ],
|
||||
@@ -979,7 +1116,33 @@ COMMON_OVERRIDE = {
|
||||
AntishipStrike: "ANTISHIP",
|
||||
GroundAttack: "STRIKE",
|
||||
Escort: "CAP",
|
||||
RunwayAttack: "RUNWAY_ATTACK"
|
||||
RunwayAttack: "RUNWAY_ATTACK",
|
||||
FighterSweep: "CAP"
|
||||
}
|
||||
|
||||
"""
|
||||
This is a list of mappings from the FlightType of a Flight to the type of payload defined in the
|
||||
resources/payloads/UNIT_TYPE.lua file. A Flight has no concept of a PyDCS task, so COMMON_OVERRIDE cannot be
|
||||
used here. This is used in the payload editor, for setting the default loadout of an object.
|
||||
The left element is the FlightType name, and the right element is a tuple containing what is used in the lua file.
|
||||
Some aircraft differ from the standard loadout names, so those have been included here too.
|
||||
The priority goes from first to last - the first element in the tuple will be tried first, then the second, etc.
|
||||
"""
|
||||
|
||||
EXPANDED_TASK_PAYLOAD_OVERRIDE = {
|
||||
"TARCAP": ("CAP HEAVY", "CAP"),
|
||||
"BARCAP": ("CAP HEAVY", "CAP"),
|
||||
"CAS": ("CAS MAVERICK F", "CAS"),
|
||||
"INTERCEPTION": ("CAP HEAVY", "CAP"),
|
||||
"STRIKE": ("STRIKE",),
|
||||
"ANTISHIP": ("ANTISHIP",),
|
||||
"SEAD": ("SEAD",),
|
||||
"DEAD": ("SEAD",),
|
||||
"ESCORT": ("CAP HEAVY", "CAP"),
|
||||
"BAI": ( "BAI", "CAS MAVERICK F", "CAS"),
|
||||
"SWEEP": ("CAP HEAVY", "CAP"),
|
||||
"OCA_RUNWAY": ("RUNWAY_ATTACK","RUNWAY_STRIKE","STRIKE"),
|
||||
"OCA_AIRCRAFT": ("OCA","CAS MAVERICK F", "CAS")
|
||||
}
|
||||
|
||||
PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
@@ -997,6 +1160,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
AntishipStrike: "ANTISHIP",
|
||||
GroundAttack: "STRIKE",
|
||||
Escort: "CAP HEAVY",
|
||||
FighterSweep: "CAP HEAVY",
|
||||
},
|
||||
F_A_18C: {
|
||||
CAP: "CAP HEAVY",
|
||||
@@ -1007,6 +1171,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
AntishipStrike: "ANTISHIP",
|
||||
GroundAttack: "STRIKE",
|
||||
Escort: "CAP HEAVY",
|
||||
FighterSweep: "CAP HEAVY",
|
||||
},
|
||||
Tu_160: {
|
||||
PinpointStrike: "Kh-65*12",
|
||||
@@ -1022,6 +1187,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
F_14A_135_GR: COMMON_OVERRIDE,
|
||||
F_14B: COMMON_OVERRIDE,
|
||||
F_15C: COMMON_OVERRIDE,
|
||||
F_111F: COMMON_OVERRIDE,
|
||||
F_22A: COMMON_OVERRIDE,
|
||||
F_16C_50: COMMON_OVERRIDE,
|
||||
JF_17: COMMON_OVERRIDE,
|
||||
@@ -1047,6 +1213,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
Tornado_IDS: COMMON_OVERRIDE,
|
||||
Mirage_2000_5: COMMON_OVERRIDE,
|
||||
MiG_31: COMMON_OVERRIDE,
|
||||
S_3B: COMMON_OVERRIDE,
|
||||
SA342M: COMMON_OVERRIDE,
|
||||
SA342L: COMMON_OVERRIDE,
|
||||
SA342Mistral: COMMON_OVERRIDE,
|
||||
@@ -1084,6 +1251,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
AH_1W: COMMON_OVERRIDE,
|
||||
AH_64D: COMMON_OVERRIDE,
|
||||
AH_64A: COMMON_OVERRIDE,
|
||||
SH_60B: COMMON_OVERRIDE,
|
||||
Hercules: COMMON_OVERRIDE,
|
||||
|
||||
Su_25TM: {
|
||||
@@ -1151,9 +1319,6 @@ REWARDS = {
|
||||
"derrick": 8
|
||||
}
|
||||
|
||||
# Base post-turn bonus value
|
||||
PLAYER_BUDGET_BASE = 20
|
||||
|
||||
CARRIER_CAPABLE = [
|
||||
FA_18C_hornet,
|
||||
F_14A_135_GR,
|
||||
@@ -1162,6 +1327,7 @@ CARRIER_CAPABLE = [
|
||||
Su_33,
|
||||
A_4E_C,
|
||||
Rafale_M,
|
||||
S_3B,
|
||||
|
||||
UH_1H,
|
||||
Mi_8MT,
|
||||
@@ -1169,6 +1335,7 @@ CARRIER_CAPABLE = [
|
||||
AH_1W,
|
||||
OH_58D,
|
||||
UH_60A,
|
||||
SH_60B,
|
||||
|
||||
SA342L,
|
||||
SA342M,
|
||||
@@ -1185,6 +1352,7 @@ LHA_CAPABLE = [
|
||||
AH_1W,
|
||||
OH_58D,
|
||||
UH_60A,
|
||||
SH_60B,
|
||||
|
||||
SA342L,
|
||||
SA342M,
|
||||
@@ -1255,6 +1423,7 @@ INFANTRY: List[VehicleType] = [
|
||||
Infantry.Soldier_RPG,
|
||||
Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
Artillery._2B11_mortar,
|
||||
Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
|
||||
@@ -1288,6 +1457,33 @@ def unit_type_name(unit_type) -> str:
|
||||
def unit_type_name_2(unit_type) -> str:
|
||||
return unit_type.name and unit_type.name or unit_type.id
|
||||
|
||||
def unit_get_expanded_info(country_name: str, unit_type, request_type: str) -> str:
|
||||
original_name = unit_type.name and unit_type.name or unit_type.id
|
||||
default_value = None
|
||||
faction_value = None
|
||||
with UNITINFOTEXT_PATH.open("r", encoding="utf-8") as fdata:
|
||||
data = json.load(fdata, encoding="utf-8")
|
||||
type_exists = data.get(original_name)
|
||||
if type_exists is not None:
|
||||
for faction in type_exists:
|
||||
if default_value is None:
|
||||
default_exists = faction.get("default")
|
||||
if default_exists is not None:
|
||||
default_value = default_exists.get(request_type)
|
||||
if faction_value is None:
|
||||
faction_exists = faction.get(country_name)
|
||||
if faction_exists is not None:
|
||||
faction_value = faction_exists.get(request_type)
|
||||
if default_value is None:
|
||||
if request_type == "text":
|
||||
return "WIP - This unit doesn't have any description text yet."
|
||||
if request_type == "name":
|
||||
return original_name
|
||||
else:
|
||||
return "Unknown"
|
||||
if faction_value is None:
|
||||
return default_value
|
||||
return faction_value
|
||||
|
||||
def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
|
||||
if name in vehicle_map:
|
||||
|
||||
@@ -197,6 +197,11 @@ class Debriefing:
|
||||
continue
|
||||
|
||||
building = self.unit_map.building_or_fortification(unit_name)
|
||||
# Try appending object to the name, because we do this for building statics.
|
||||
if building is None:
|
||||
building = self.unit_map.building_or_fortification(
|
||||
f"{unit_name} object"
|
||||
)
|
||||
if building is not None:
|
||||
if building.ground_object.control_point.captured:
|
||||
losses.player_buildings.append(building)
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, List, TYPE_CHECKING, Type
|
||||
from typing import Dict, Iterator, List, TYPE_CHECKING, Tuple, Type
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import Task
|
||||
@@ -15,16 +15,13 @@ from game.operation.operation import Operation
|
||||
from game.theater import ControlPoint
|
||||
from gen import AirTaskingOrder
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from ..db import PRICES
|
||||
from ..unitmap import UnitMap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..game import Game
|
||||
|
||||
|
||||
DIFFICULTY_LOG_BASE = 1.1
|
||||
EVENT_DEPARTURE_MAX_DISTANCE = 340000
|
||||
|
||||
|
||||
MINOR_DEFEAT_INFLUENCE = 0.1
|
||||
DEFEAT_INFLUENCE = 0.3
|
||||
STRONG_DEFEAT_INFLUENCE = 0.5
|
||||
@@ -39,7 +36,6 @@ class Event:
|
||||
from_cp = None # type: ControlPoint
|
||||
to_cp = None # type: ControlPoint
|
||||
difficulty = 1 # type: int
|
||||
BONUS_BASE = 5
|
||||
|
||||
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
|
||||
self.game = game
|
||||
@@ -57,9 +53,6 @@ class Event:
|
||||
def tasks(self) -> List[Type[Task]]:
|
||||
return []
|
||||
|
||||
def bonus(self) -> int:
|
||||
return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE)
|
||||
|
||||
def generate(self) -> UnitMap:
|
||||
Operation.prepare(self.game)
|
||||
unit_map = Operation.generate()
|
||||
@@ -152,12 +145,10 @@ class Event:
|
||||
|
||||
loss.group.units.remove(loss.unit)
|
||||
loss.group.units_losts.append(loss.unit)
|
||||
if not loss.ground_object.alive_unit_count:
|
||||
loss.ground_object.is_dead = True
|
||||
|
||||
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.building_losses:
|
||||
loss.ground_object.is_dead = True
|
||||
loss.ground_object.kill()
|
||||
self.game.informations.append(Information(
|
||||
"Building destroyed",
|
||||
f"{loss.ground_object.dcs_identifier} has been destroyed at "
|
||||
@@ -305,9 +296,6 @@ class Event:
|
||||
self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
|
||||
def skip(self):
|
||||
pass
|
||||
|
||||
def redeploy_units(self, cp):
|
||||
""""
|
||||
Auto redeploy units to newly captured base
|
||||
@@ -349,36 +337,63 @@ class Event:
|
||||
logging.info(info.text)
|
||||
|
||||
|
||||
class UnitsDeliveryEvent(Event):
|
||||
|
||||
informational = True
|
||||
|
||||
def __init__(self, attacker_name: str, defender_name: str,
|
||||
from_cp: ControlPoint, to_cp: ControlPoint,
|
||||
game: Game) -> None:
|
||||
super(UnitsDeliveryEvent, self).__init__(game=game,
|
||||
location=to_cp.position,
|
||||
from_cp=from_cp,
|
||||
target_cp=to_cp,
|
||||
attacker_name=attacker_name,
|
||||
defender_name=defender_name)
|
||||
class UnitsDeliveryEvent:
|
||||
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
self.to_cp = control_point
|
||||
self.units: Dict[Type[UnitType], int] = {}
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "Pending delivery to {}".format(self.to_cp)
|
||||
|
||||
def deliver(self, units: Dict[Type[UnitType], int]) -> None:
|
||||
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) -> None:
|
||||
for k, v in self.units.items():
|
||||
if self.to_cp.captured:
|
||||
name = "Ally "
|
||||
else:
|
||||
name = "Enemy "
|
||||
self.game.message(
|
||||
f"{name} reinforcements: {k.id} x {v} at {self.to_cp.name}")
|
||||
def sell(self, units: Dict[Type[UnitType], int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] = self.units.get(k, 0) - v
|
||||
|
||||
self.to_cp.base.commision_units(self.units)
|
||||
def consume_each_order(self) -> Iterator[Tuple[Type[UnitType], int]]:
|
||||
while self.units:
|
||||
yield self.units.popitem()
|
||||
|
||||
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 = {}
|
||||
|
||||
142
game/game.py
142
game/game.py
@@ -1,9 +1,10 @@
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from dcs.action import Coalition
|
||||
from dcs.mapping import Point
|
||||
@@ -11,7 +12,6 @@ from dcs.task import CAP, CAS, PinpointStrike
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from game import db
|
||||
from game.db import PLAYER_BUDGET_BASE, REWARDS
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
from game.models.game_stats import GameStats
|
||||
from game.plugins import LuaPluginManager
|
||||
@@ -19,16 +19,21 @@ from gen.ato import AirTaskingOrder
|
||||
from gen.conflictgen import Conflict
|
||||
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import FlightType
|
||||
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
||||
from . import persistency
|
||||
from .debriefing import Debriefing
|
||||
from .event.event import Event, UnitsDeliveryEvent
|
||||
from .event.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
|
||||
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
|
||||
|
||||
@@ -65,16 +70,18 @@ AWACS_BUDGET_COST = 4
|
||||
# Bonus multiplier logarithm base
|
||||
PLAYER_BUDGET_IMPORTANCE_LOG = 2
|
||||
|
||||
|
||||
class TurnState(Enum):
|
||||
WIN = 0
|
||||
LOSS = 1
|
||||
CONTINUE = 2
|
||||
|
||||
|
||||
class Game:
|
||||
def __init__(self, player_name: str, enemy_name: str,
|
||||
theater: ConflictTheater, start_date: datetime,
|
||||
settings: Settings, player_budget: int,
|
||||
enemy_budget: int) -> None:
|
||||
settings: Settings, player_budget: float,
|
||||
enemy_budget: float) -> None:
|
||||
self.settings = settings
|
||||
self.events: List[Event] = []
|
||||
self.theater = theater
|
||||
@@ -106,9 +113,6 @@ class Game:
|
||||
self.theater.controlpoints
|
||||
)
|
||||
|
||||
for cp in self.theater.controlpoints:
|
||||
cp.pending_unit_deliveries = self.units_delivery_event(cp)
|
||||
|
||||
self.sanitize_sides()
|
||||
|
||||
self.on_load()
|
||||
@@ -124,6 +128,21 @@ class Game:
|
||||
|
||||
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)
|
||||
@@ -149,6 +168,11 @@ class Game:
|
||||
def enemy_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.enemy_name]
|
||||
|
||||
def faction_for(self, player: bool) -> Faction:
|
||||
if player:
|
||||
return self.player_faction
|
||||
return self.enemy_faction
|
||||
|
||||
def _roll(self, prob, mult):
|
||||
if self.settings.version == "dev":
|
||||
# always generate all events for dev
|
||||
@@ -165,39 +189,20 @@ class Game:
|
||||
front_line.control_point_a,
|
||||
front_line.control_point_b)
|
||||
|
||||
@property
|
||||
def budget_reward_amount(self) -> int:
|
||||
reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
|
||||
for cp in self.theater.player_points():
|
||||
for g in cp.ground_objects:
|
||||
if g.category in REWARDS.keys() and not g.is_dead:
|
||||
reward += REWARDS[g.category]
|
||||
return int(reward * self.settings.player_income_multiplier)
|
||||
def adjust_budget(self, amount: float, player: bool) -> None:
|
||||
if player:
|
||||
self.budget += amount
|
||||
else:
|
||||
self.enemy_budget += amount
|
||||
|
||||
def process_player_income(self):
|
||||
self.budget += self.budget_reward_amount
|
||||
self.budget += Income(self, player=True).total
|
||||
|
||||
def process_enemy_income(self):
|
||||
# TODO: Clean up save compat.
|
||||
if not hasattr(self, "enemy_budget"):
|
||||
self.enemy_budget = 0
|
||||
|
||||
production = 0.0
|
||||
for enemy_point in self.theater.enemy_points():
|
||||
for g in enemy_point.ground_objects:
|
||||
if g.category in REWARDS.keys() and not g.is_dead:
|
||||
production = production + REWARDS[g.category]
|
||||
|
||||
self.enemy_budget += production * self.settings.enemy_income_multiplier
|
||||
|
||||
def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent:
|
||||
event = UnitsDeliveryEvent(attacker_name=self.player_name,
|
||||
defender_name=self.player_name,
|
||||
from_cp=to_cp,
|
||||
to_cp=to_cp,
|
||||
game=self)
|
||||
self.events.append(event)
|
||||
return event
|
||||
self.enemy_budget += Income(self, player=False).total
|
||||
|
||||
def initiate_event(self, event: Event) -> UnitMap:
|
||||
#assert event in self.events
|
||||
@@ -207,8 +212,6 @@ class Game:
|
||||
def finish_event(self, event: Event, debriefing: Debriefing):
|
||||
logging.info("Finishing event {}".format(event))
|
||||
event.commit(debriefing)
|
||||
self.budget += int(event.bonus() *
|
||||
self.settings.player_income_multiplier)
|
||||
|
||||
if event in self.events:
|
||||
self.events.remove(event)
|
||||
@@ -225,22 +228,15 @@ class Game:
|
||||
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 += 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()
|
||||
control_point.process_turn(self)
|
||||
|
||||
self.process_enemy_income()
|
||||
|
||||
@@ -278,7 +274,6 @@ class Game:
|
||||
|
||||
self.aircraft_inventory.reset()
|
||||
for cp in self.theater.controlpoints:
|
||||
cp.pending_unit_deliveries = self.units_delivery_event(cp)
|
||||
self.aircraft_inventory.set_from_control_point(cp)
|
||||
|
||||
# Check for win or loss condition
|
||||
@@ -288,6 +283,7 @@ class Game:
|
||||
|
||||
# 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()
|
||||
@@ -308,13 +304,20 @@ class Game:
|
||||
|
||||
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
|
||||
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
||||
front_line_budget_share=ground_portion
|
||||
).spend_budget(self.budget, blue_planner.procurement_requests)
|
||||
|
||||
self.enemy_budget = ProcurementAi(
|
||||
@@ -323,7 +326,8 @@ class Game:
|
||||
faction=self.enemy_faction,
|
||||
manage_runways=True,
|
||||
manage_front_line=True,
|
||||
manage_aircraft=True
|
||||
manage_aircraft=True,
|
||||
front_line_budget_share=ground_portion
|
||||
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
|
||||
|
||||
def message(self, text: str) -> None:
|
||||
@@ -351,6 +355,24 @@ 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
|
||||
@@ -367,9 +389,14 @@ class Game:
|
||||
points.append(front_line.control_point_a.position)
|
||||
points.append(front_line.control_point_b.position)
|
||||
|
||||
# If do_not_cull_carrier is enabled, add carriers as culling point
|
||||
if self.settings.perf_do_not_cull_carrier:
|
||||
for cp in self.theater.controlpoints:
|
||||
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(cp.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:
|
||||
points.append(cp.position)
|
||||
|
||||
@@ -391,9 +418,16 @@ class Game:
|
||||
if cpoint is not None:
|
||||
points.append(cpoint)
|
||||
|
||||
for package in self.blue_ato.packages:
|
||||
points.append(package.target.position)
|
||||
for package in self.red_ato.packages:
|
||||
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
|
||||
points.append(package.target.position)
|
||||
|
||||
# Else 0,0, since we need a default value
|
||||
|
||||
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)
|
||||
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)
|
||||
@@ -26,12 +26,12 @@ from gen.environmentgen import EnvironmentGenerator
|
||||
from gen.forcedoptionsgen import ForcedOptionsGenerator
|
||||
from gen.groundobjectsgen import GroundObjectsGenerator
|
||||
from gen.kneeboard import KneeboardGenerator
|
||||
from gen.naming import namegen
|
||||
from gen.radios import RadioFrequency, RadioRegistry
|
||||
from gen.tacan import TacanRegistry
|
||||
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
|
||||
|
||||
from .. import db
|
||||
from ..debriefing import Debriefing
|
||||
from ..theater import Airfield
|
||||
from ..unitmap import UnitMap
|
||||
|
||||
@@ -86,7 +86,7 @@ class Operation:
|
||||
cls.game.enemy_country,
|
||||
frontline.position
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def air_conflict(cls) -> Conflict:
|
||||
assert cls.game
|
||||
@@ -103,7 +103,7 @@ class Operation:
|
||||
cls.game.enemy_name,
|
||||
cls.game.player_country,
|
||||
cls.game.enemy_country,
|
||||
mid_point
|
||||
mid_point
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -199,10 +199,14 @@ class Operation:
|
||||
|
||||
@classmethod
|
||||
def create_radio_registries(cls) -> None:
|
||||
unique_map_frequencies = set() # type: Set[RadioFrequency]
|
||||
unique_map_frequencies: Set[RadioFrequency] = set()
|
||||
cls._create_tacan_registry(unique_map_frequencies)
|
||||
cls._create_radio_registry(unique_map_frequencies)
|
||||
|
||||
assert cls.radio_registry is not None
|
||||
for frequency in unique_map_frequencies:
|
||||
cls.radio_registry.reserve(frequency)
|
||||
|
||||
@classmethod
|
||||
def assign_channels_to_flights(cls, flights: List[FlightData],
|
||||
air_support: AirSupport) -> None:
|
||||
@@ -256,8 +260,8 @@ class Operation:
|
||||
unique_map_frequencies.add(data.atc.vhf_fm)
|
||||
unique_map_frequencies.add(data.atc.vhf_am)
|
||||
unique_map_frequencies.add(data.atc.uhf)
|
||||
# No need to reserve ILS or TACAN because those are in the
|
||||
# beacon list.
|
||||
# No need to reserve ILS or TACAN because those are in the
|
||||
# beacon list.
|
||||
|
||||
@classmethod
|
||||
def _generate_ground_units(cls):
|
||||
@@ -291,7 +295,7 @@ class Operation:
|
||||
heading=d["orientation"],
|
||||
dead=True,
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def generate(cls) -> UnitMap:
|
||||
"""Build the final Mission to be exported"""
|
||||
@@ -345,7 +349,7 @@ class Operation:
|
||||
cls.jtacs,
|
||||
cls.airgen
|
||||
)
|
||||
|
||||
cls.reset_naming_ids()
|
||||
return cls.unit_map
|
||||
|
||||
@classmethod
|
||||
@@ -407,6 +411,10 @@ class Operation:
|
||||
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,
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator, List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.task import CAP, CAS
|
||||
from dcs.unittype import FlyingType, UnitType, VehicleType
|
||||
from dcs.unittype import FlyingType, VehicleType
|
||||
|
||||
from game import db
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from gen.flights.ai_flight_planner_db import (
|
||||
capable_aircraft_for_task,
|
||||
preferred_aircraft_for_task,
|
||||
)
|
||||
from 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
|
||||
@@ -25,36 +23,75 @@ if TYPE_CHECKING:
|
||||
@dataclass(frozen=True)
|
||||
class AircraftProcurementRequest:
|
||||
near: MissionTarget
|
||||
range: int
|
||||
range: Distance
|
||||
task_capability: FlightType
|
||||
number: int
|
||||
|
||||
def __str__(self) -> str:
|
||||
task = self.task_capability.value
|
||||
distance = self.range.nautical_miles
|
||||
target = self.near.name
|
||||
return f"{self.number} ship {task} within {distance} nm of {target}"
|
||||
|
||||
|
||||
class ProcurementAi:
|
||||
def __init__(self, game: Game, for_player: bool, faction: Faction,
|
||||
manage_runways: bool, manage_front_line: bool,
|
||||
manage_aircraft: bool) -> None:
|
||||
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: int,
|
||||
aircraft_requests: List[AircraftProcurementRequest]) -> int:
|
||||
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 / 2)
|
||||
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 repair_runways(self, budget: int) -> int:
|
||||
def sell_incomplete_squadrons(self) -> float:
|
||||
# Selling incomplete squadrons gives us more money to spend on the next
|
||||
# turn. This serves as a short term fix for
|
||||
# https://github.com/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
|
||||
@@ -74,15 +111,26 @@ class ProcurementAi:
|
||||
return budget
|
||||
|
||||
def random_affordable_ground_unit(
|
||||
self, budget: int) -> Optional[Type[VehicleType]]:
|
||||
affordable_units = [u for u in self.faction.frontline_units if
|
||||
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: int) -> int:
|
||||
if not self.faction.frontline_units:
|
||||
def reinforce_front_line(self, budget: float) -> float:
|
||||
if not self.faction.frontline_units and not self.faction.artillery_units:
|
||||
return budget
|
||||
|
||||
while budget > 0:
|
||||
@@ -91,49 +139,44 @@ class ProcurementAi:
|
||||
break
|
||||
|
||||
cp = random.choice(candidates)
|
||||
unit = self.random_affordable_ground_unit(budget)
|
||||
unit = self.random_affordable_ground_unit(budget, cp)
|
||||
if unit is None:
|
||||
# Can't afford any more units.
|
||||
break
|
||||
|
||||
budget -= db.PRICES[unit]
|
||||
assert cp.pending_unit_deliveries is not None
|
||||
cp.pending_unit_deliveries.deliver({unit: 1})
|
||||
cp.pending_unit_deliveries.order({unit: 1})
|
||||
|
||||
return budget
|
||||
|
||||
def _affordable_aircraft_of_types(
|
||||
self, types: List[Type[FlyingType]], airbase: ControlPoint,
|
||||
number: int, max_price: int) -> Optional[Type[FlyingType]]:
|
||||
unit_pool = [u for u in self.faction.aircrafts if u in types]
|
||||
affordable_units = [
|
||||
u for u in unit_pool
|
||||
if db.PRICES[u] * number <= max_price and airbase.can_operate(u)
|
||||
]
|
||||
if not affordable_units:
|
||||
return None
|
||||
return random.choice(affordable_units)
|
||||
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: int) -> Optional[Type[FlyingType]]:
|
||||
aircraft = self._affordable_aircraft_of_types(
|
||||
preferred_aircraft_for_task(request.task_capability),
|
||||
airbase, request.number, budget)
|
||||
if aircraft is not None:
|
||||
return aircraft
|
||||
airbase: ControlPoint, budget: float) -> Optional[Type[FlyingType]]:
|
||||
return self._affordable_aircraft_of_types(
|
||||
capable_aircraft_for_task(request.task_capability),
|
||||
aircraft_for_task(request.task_capability),
|
||||
airbase, request.number, budget)
|
||||
|
||||
def purchase_aircraft(
|
||||
self, budget: int,
|
||||
aircraft_requests: List[AircraftProcurementRequest]) -> int:
|
||||
unit_pool = [u for u in self.faction.aircrafts
|
||||
if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
|
||||
if not unit_pool:
|
||||
return budget
|
||||
|
||||
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)
|
||||
@@ -146,8 +189,7 @@ class ProcurementAi:
|
||||
continue
|
||||
|
||||
budget -= db.PRICES[unit] * request.number
|
||||
assert airbase.pending_unit_deliveries is not None
|
||||
airbase.pending_unit_deliveries.deliver({unit: request.number})
|
||||
airbase.pending_unit_deliveries.order({unit: request.number})
|
||||
|
||||
return budget
|
||||
|
||||
@@ -164,6 +206,7 @@ class ProcurementAi:
|
||||
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
|
||||
@@ -171,7 +214,10 @@ class ProcurementAi:
|
||||
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 = []
|
||||
@@ -179,7 +225,7 @@ class ProcurementAi:
|
||||
# Prefer to buy front line units at active front lines that are not
|
||||
# already overloaded.
|
||||
for cp in self.owned_points:
|
||||
if cp.base.total_armor >= 30:
|
||||
if cp.expected_ground_units_next_turn.total >= 30:
|
||||
# Control point is already sufficiently defended.
|
||||
continue
|
||||
for connected in cp.connected_points:
|
||||
@@ -187,8 +233,23 @@ class ProcurementAi:
|
||||
candidates.append(cp)
|
||||
|
||||
if not candidates:
|
||||
# Otherwise buy them anywhere valid.
|
||||
candidates = [p for p in self.owned_points
|
||||
if p.can_deploy_ground_units]
|
||||
# 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
|
||||
|
||||
@@ -19,15 +19,17 @@ class Settings:
|
||||
supercarrier: bool = False
|
||||
generate_marks: bool = True
|
||||
manpads: bool = True
|
||||
cold_start: bool = False # Legacy parameter do not use
|
||||
version: Optional[str] = None
|
||||
player_income_multiplier: float = 1.0
|
||||
enemy_income_multiplier: float = 1.0
|
||||
|
||||
default_start_type: str = "Cold"
|
||||
|
||||
# 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 oriented
|
||||
perf_red_alert_state: bool = True
|
||||
@@ -35,7 +37,6 @@ class Settings:
|
||||
perf_artillery: bool = True
|
||||
perf_moving_units: bool = True
|
||||
perf_infantry: bool = True
|
||||
perf_ai_parking_start: bool = True
|
||||
perf_destroyed_units: bool = True
|
||||
|
||||
# Performance culling
|
||||
@@ -48,6 +49,8 @@ class Settings:
|
||||
|
||||
# Cheating
|
||||
show_red_ato: bool = False
|
||||
enable_frontline_cheats: bool = False
|
||||
enable_base_capture_cheat: bool = False
|
||||
|
||||
never_delay_player_flights: bool = False
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ from dcs.unittype import FlyingType, UnitType, VehicleType
|
||||
from dcs.vehicles import AirDefence, Armor
|
||||
|
||||
from game import db
|
||||
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
|
||||
@@ -36,6 +38,20 @@ class Base:
|
||||
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())
|
||||
@@ -98,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
|
||||
|
||||
@@ -55,7 +55,7 @@ from .controlpoint import (
|
||||
Fob,
|
||||
)
|
||||
from .landmap import Landmap, load_landmap, poly_contains
|
||||
from ..utils import nm_to_meter
|
||||
from ..utils import Distance, meters, nautical_miles
|
||||
|
||||
Numeric = Union[int, float]
|
||||
|
||||
@@ -115,7 +115,7 @@ class MizCampaignLoader:
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73.id,
|
||||
}
|
||||
|
||||
BASE_DEFENSE_RADIUS = nm_to_meter(2)
|
||||
BASE_DEFENSE_RADIUS = nautical_miles(2)
|
||||
|
||||
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
|
||||
self.theater = theater
|
||||
@@ -317,9 +317,9 @@ class MizCampaignLoader:
|
||||
self.control_points[origin.id])
|
||||
return front_lines
|
||||
|
||||
def objective_info(self, group: Group) -> Tuple[ControlPoint, int]:
|
||||
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
|
||||
closest = self.theater.closest_control_point(group.position)
|
||||
distance = closest.position.distance_to_point(group.position)
|
||||
distance = meters(closest.position.distance_to_point(group.position))
|
||||
return closest, distance
|
||||
|
||||
def add_preset_locations(self) -> None:
|
||||
@@ -447,11 +447,11 @@ class ConflictTheater:
|
||||
if self.is_on_land(point):
|
||||
return False
|
||||
|
||||
for exclusion_zone in self.landmap[1]:
|
||||
for exclusion_zone in self.landmap.exclusion_zones:
|
||||
if poly_contains(point.x, point.y, exclusion_zone):
|
||||
return False
|
||||
|
||||
for sea in self.landmap[2]:
|
||||
for sea in self.landmap.sea_zones:
|
||||
if poly_contains(point.x, point.y, sea):
|
||||
return True
|
||||
|
||||
@@ -462,14 +462,13 @@ class ConflictTheater:
|
||||
return True
|
||||
|
||||
is_point_included = False
|
||||
for inclusion_zone in self.landmap[0]:
|
||||
if poly_contains(point.x, point.y, inclusion_zone):
|
||||
is_point_included = True
|
||||
if 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[1]:
|
||||
for exclusion_zone in self.landmap.exclusion_zones:
|
||||
if poly_contains(point.x, point.y, exclusion_zone):
|
||||
return False
|
||||
|
||||
@@ -484,14 +483,14 @@ class ConflictTheater:
|
||||
nearest_points = []
|
||||
if not self.landmap:
|
||||
raise RuntimeError("Landmap not initialized")
|
||||
for inclusion_zone in self.landmap[0]:
|
||||
for inclusion_zone in self.landmap.inclusion_zones:
|
||||
nearest_pair = ops.nearest_points(point, inclusion_zone)
|
||||
nearest_points.append(nearest_pair[1])
|
||||
min_distance = None # type: Optional[geometry.Point]
|
||||
nearest_point = None # type: Optional[geometry.Point]
|
||||
for pt in nearest_points:
|
||||
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 not min_distance or distance < min_distance:
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
nearest_point = pt
|
||||
assert isinstance(nearest_point, geometry.Point)
|
||||
@@ -503,8 +502,13 @@ class ConflictTheater:
|
||||
)
|
||||
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 [point for point in self.controlpoints if point.captured]
|
||||
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]:
|
||||
@@ -512,7 +516,7 @@ class ConflictTheater:
|
||||
yield FrontLine(cp, connected_point, self)
|
||||
|
||||
def enemy_points(self) -> List[ControlPoint]:
|
||||
return [point for point in self.controlpoints if not point.captured]
|
||||
return list(self.control_points_for(player=False))
|
||||
|
||||
def closest_control_point(self, point: Point) -> ControlPoint:
|
||||
closest = self.controlpoints[0]
|
||||
@@ -523,6 +527,26 @@ class ConflictTheater:
|
||||
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]:
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
@@ -7,7 +8,8 @@ import re
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Type
|
||||
from functools import total_ordering
|
||||
from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.ships import (
|
||||
@@ -20,23 +22,27 @@ from dcs.terrain.terrain import Airport, ParkingSlot
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from gen.runways import RunwayAssigner, RunwayData
|
||||
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, GenericCarrierGroundObject,
|
||||
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
|
||||
from ..event import UnitsDeliveryEvent
|
||||
|
||||
|
||||
class ControlPointType(Enum):
|
||||
@@ -190,6 +196,28 @@ class RunwayStatus:
|
||||
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
|
||||
@@ -207,7 +235,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
at: db.StartingPosition, size: int,
|
||||
importance: float, has_frontline=True,
|
||||
cptype=ControlPointType.AIRBASE):
|
||||
super().__init__(" ".join(re.split(r"[ \-]", name)[:2]), position)
|
||||
super().__init__(name, position)
|
||||
# TODO: Should be Airbase specific.
|
||||
self.id = cp_id
|
||||
self.full_name = name
|
||||
@@ -228,7 +256,8 @@ class ControlPoint(MissionTarget, ABC):
|
||||
self.cptype = cptype
|
||||
# TODO: Should be Airbase specific.
|
||||
self.stances: Dict[int, CombatStance] = {}
|
||||
self.pending_unit_deliveries: Optional[UnitsDeliveryEvent] = None
|
||||
from ..event import UnitsDeliveryEvent
|
||||
self.pending_unit_deliveries = UnitsDeliveryEvent(self)
|
||||
|
||||
self.target_position: Optional[Point] = None
|
||||
|
||||
@@ -363,8 +392,90 @@ class ControlPoint(MissionTarget, ABC):
|
||||
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:
|
||||
@@ -372,9 +483,6 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
self.base.set_strength_to_minimum()
|
||||
|
||||
self.base.aircraft = {}
|
||||
self.base.armor = {}
|
||||
|
||||
self.clear_base_defenses()
|
||||
from .start_generator import BaseDefenseGenerator
|
||||
BaseDefenseGenerator(game, self).generate()
|
||||
@@ -401,7 +509,6 @@ class ControlPoint(MissionTarget, ABC):
|
||||
return total
|
||||
|
||||
def expected_aircraft_next_turn(self, game: Game) -> PendingOccupancy:
|
||||
assert self.pending_unit_deliveries
|
||||
on_order = 0
|
||||
for unit_bought in self.pending_unit_deliveries.units:
|
||||
if issubclass(unit_bought, FlyingType):
|
||||
@@ -438,7 +545,9 @@ class ControlPoint(MissionTarget, ABC):
|
||||
return
|
||||
self.runway_status.begin_repair()
|
||||
|
||||
def process_turn(self) -> None:
|
||||
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()
|
||||
@@ -452,11 +561,51 @@ class ControlPoint(MissionTarget, ABC):
|
||||
# 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
|
||||
|
||||
|
||||
class Airfield(ControlPoint):
|
||||
|
||||
@@ -521,6 +670,10 @@ class Airfield(ControlPoint):
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def income_per_turn(self) -> int:
|
||||
return 20
|
||||
|
||||
|
||||
class NavalControlPoint(ControlPoint, ABC):
|
||||
|
||||
@@ -529,7 +682,6 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
return True
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
yield from super().mission_types(for_player)
|
||||
from gen.flights.flight import FlightType
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
@@ -540,6 +692,7 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
]
|
||||
else:
|
||||
yield FlightType.ANTISHIP
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def heading(self) -> int:
|
||||
@@ -720,3 +873,7 @@ class Fob(ControlPoint):
|
||||
@property
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def income_per_turn(self) -> int:
|
||||
return 10
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
from dataclasses import dataclass
|
||||
import pickle
|
||||
from typing import Collection, Optional, Tuple
|
||||
from functools import cached_property
|
||||
from typing import Optional, Tuple, Union
|
||||
import logging
|
||||
|
||||
from shapely import geometry
|
||||
from shapely.geometry import MultiPolygon, Polygon
|
||||
|
||||
Zone = Collection[Tuple[float, float]]
|
||||
Landmap = Tuple[Collection[geometry.Polygon], Collection[geometry.Polygon], Collection[geometry.Polygon]]
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Landmap:
|
||||
inclusion_zones: MultiPolygon
|
||||
exclusion_zones: MultiPolygon
|
||||
sea_zones: MultiPolygon
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.inclusion_zones.is_valid:
|
||||
raise RuntimeError("Inclusion zones not valid")
|
||||
if not self.exclusion_zones.is_valid:
|
||||
raise RuntimeError("Exclusion zones not valid")
|
||||
if not self.sea_zones.is_valid:
|
||||
raise RuntimeError("Sea zones not valid")
|
||||
|
||||
@cached_property
|
||||
def inclusion_zone_only(self) -> MultiPolygon:
|
||||
return self.inclusion_zones - self.exclusion_zones - self.sea_zones
|
||||
|
||||
|
||||
def load_landmap(filename: str) -> Optional[Landmap]:
|
||||
@@ -17,7 +36,7 @@ def load_landmap(filename: str) -> Optional[Landmap]:
|
||||
return None
|
||||
|
||||
|
||||
def poly_contains(x, y, poly:geometry.Polygon):
|
||||
def poly_contains(x, y, poly: Union[MultiPolygon, Polygon]):
|
||||
return poly.contains(geometry.Point(x, y))
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import pickle
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
@@ -15,7 +14,6 @@ from dcs.vehicles import AirDefence
|
||||
from game import Game, db
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import Carrier, Lha, LocationType
|
||||
from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
@@ -479,11 +477,11 @@ class BaseDefenseGenerator:
|
||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point, for_airbase=True)
|
||||
|
||||
group = generate_anti_air_group(self.game, g, self.faction)
|
||||
if group is None:
|
||||
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.append(group)
|
||||
g.groups = groups
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_shorad(self) -> None:
|
||||
@@ -497,13 +495,13 @@ class BaseDefenseGenerator:
|
||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point, for_airbase=True)
|
||||
|
||||
group = generate_anti_air_group(self.game, g, self.faction,
|
||||
ranges=[{AirDefenseRange.Short}])
|
||||
if group is None:
|
||||
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.append(group)
|
||||
g.groups = groups
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
|
||||
@@ -642,12 +640,12 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
|
||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point, for_airbase=False)
|
||||
group = generate_anti_air_group(self.game, g, self.faction, ranges)
|
||||
if group is None:
|
||||
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 = [group]
|
||||
g.groups = groups
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_missile_sites(self) -> None:
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
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
|
||||
@@ -85,10 +90,12 @@ class TheaterGroundObject(MissionTarget):
|
||||
self.dcs_identifier = dcs_identifier
|
||||
self.airbase_group = airbase_group
|
||||
self.sea_object = sea_object
|
||||
self.is_dead = False
|
||||
# TODO: There is never more than one group.
|
||||
self.groups: List[Group] = []
|
||||
|
||||
@property
|
||||
def is_dead(self) -> bool:
|
||||
return self.alive_unit_count == 0
|
||||
|
||||
@property
|
||||
def units(self) -> List[Unit]:
|
||||
"""
|
||||
@@ -144,6 +151,46 @@ class TheaterGroundObject(MissionTarget):
|
||||
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,
|
||||
@@ -161,6 +208,9 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
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:
|
||||
@@ -171,6 +221,15 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
def waypoint_name(self) -> str:
|
||||
return f"{super().waypoint_name} #{self.object_id}"
|
||||
|
||||
@property
|
||||
def is_dead(self) -> bool:
|
||||
if not hasattr(self, "_dead"):
|
||||
self._dead = False
|
||||
return self._dead
|
||||
|
||||
def kill(self) -> None:
|
||||
self._dead = True
|
||||
|
||||
|
||||
class NavalGroundObject(TheaterGroundObject):
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
|
||||
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)
|
||||
@@ -112,7 +112,9 @@ class UnitMap:
|
||||
group: Group) -> None:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(group.name)
|
||||
# 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)
|
||||
|
||||
225
game/utils.py
225
game/utils.py
@@ -1,65 +1,18 @@
|
||||
def meter_to_feet(value_in_meter: float) -> int:
|
||||
"""Converts meters to feets
|
||||
from __future__ import annotations
|
||||
|
||||
:arg value_in_meter Value in meters
|
||||
"""
|
||||
return int(3.28084 * value_in_meter)
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
METERS_TO_FEET = 3.28084
|
||||
FEET_TO_METERS = 1 / METERS_TO_FEET
|
||||
NM_TO_METERS = 1852
|
||||
METERS_TO_NM = 1 / NM_TO_METERS
|
||||
|
||||
def feet_to_meter(value_in_feet: float) -> int:
|
||||
"""Converts feets to meters
|
||||
|
||||
:arg value_in_feet Value in feets
|
||||
"""
|
||||
return int(value_in_feet / 3.28084)
|
||||
|
||||
|
||||
def meter_to_nm(value_in_meter: float) -> int:
|
||||
"""Converts meters to nautic miles
|
||||
|
||||
:arg value_in_meter Value in meters
|
||||
"""
|
||||
return int(value_in_meter / 1852)
|
||||
|
||||
|
||||
def nm_to_meter(value_in_nm: float) -> int:
|
||||
"""Converts nautic miles to meters
|
||||
|
||||
:arg value_in_nm Value in nautic miles
|
||||
"""
|
||||
return int(value_in_nm * 1852)
|
||||
|
||||
|
||||
def knots_to_kph(value_in_knots: float) -> int:
|
||||
"""Converts Knots to Kilometer Per Hour
|
||||
|
||||
:arg value_in_knots Knots
|
||||
"""
|
||||
return int(value_in_knots * 1.852)
|
||||
|
||||
|
||||
def mps_to_knots(value_in_mps: float) -> int:
|
||||
"""Converts Meters Per Second To Knots
|
||||
|
||||
:arg value_in_mps Meters Per Second
|
||||
"""
|
||||
return int(value_in_mps * 1.943)
|
||||
|
||||
|
||||
def mps_to_kph(speed: float) -> int:
|
||||
"""Converts meters per second to kilometers per hour.
|
||||
|
||||
:arg speed Speed in m/s.
|
||||
"""
|
||||
return int(speed * 3.6)
|
||||
|
||||
|
||||
def kph_to_mps(speed: float) -> int:
|
||||
"""Converts kilometers per hour to meters per second.
|
||||
|
||||
:arg speed Speed in KPH.
|
||||
"""
|
||||
return int(speed / 3.6)
|
||||
KNOTS_TO_KPH = 1.852
|
||||
KPH_TO_KNOTS = 1 / KNOTS_TO_KPH
|
||||
MS_TO_KPH = 3.6
|
||||
KPH_TO_MS = 1 / MS_TO_KPH
|
||||
|
||||
|
||||
def heading_sum(h, a) -> int:
|
||||
@@ -71,5 +24,157 @@ def heading_sum(h, a) -> int:
|
||||
else:
|
||||
return h
|
||||
|
||||
|
||||
def opposite_heading(h):
|
||||
return heading_sum(h, 180)
|
||||
return heading_sum(h, 180)
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class Distance:
|
||||
distance_in_meters: float
|
||||
|
||||
@property
|
||||
def feet(self) -> float:
|
||||
return self.distance_in_meters * METERS_TO_FEET
|
||||
|
||||
@property
|
||||
def meters(self) -> float:
|
||||
return self.distance_in_meters
|
||||
|
||||
@property
|
||||
def nautical_miles(self) -> float:
|
||||
return self.distance_in_meters * METERS_TO_NM
|
||||
|
||||
@classmethod
|
||||
def from_feet(cls, value: float) -> Distance:
|
||||
return cls(value * FEET_TO_METERS)
|
||||
|
||||
@classmethod
|
||||
def from_meters(cls, value: float) -> Distance:
|
||||
return cls(value)
|
||||
|
||||
@classmethod
|
||||
def from_nautical_miles(cls, value: float) -> Distance:
|
||||
return cls(value * NM_TO_METERS)
|
||||
|
||||
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)
|
||||
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
def _build_version_string() -> str:
|
||||
components = ["2.3.0"]
|
||||
components = ["2.4"]
|
||||
build_number_path = Path("resources/buildnumber")
|
||||
if build_number_path.exists():
|
||||
with build_number_path.open("r") as build_number_file:
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
@@ -39,7 +40,7 @@ class Clouds:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Fog:
|
||||
visibility: int
|
||||
visibility: Distance
|
||||
thickness: int
|
||||
|
||||
|
||||
@@ -56,7 +57,7 @@ class Weather:
|
||||
if random.randrange(5) != 0:
|
||||
return None
|
||||
return Fog(
|
||||
visibility=random.randint(2500, 5000),
|
||||
visibility=meters(random.randint(2500, 5000)),
|
||||
thickness=random.randint(100, 500)
|
||||
)
|
||||
|
||||
|
||||
181
gen/aircraft.py
181
gen/aircraft.py
@@ -20,20 +20,20 @@ from dcs.planes import (
|
||||
B_17G,
|
||||
B_52H,
|
||||
Bf_109K_4,
|
||||
C_101EB,
|
||||
C_101CC,
|
||||
C_101EB,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_14B,
|
||||
I_16,
|
||||
JF_17,
|
||||
Ju_88A4,
|
||||
PlaneType,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
PlaneType,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
Su_33,
|
||||
@@ -59,22 +59,22 @@ from dcs.task import (
|
||||
OptReactOnThreat,
|
||||
OptRestrictJettison,
|
||||
OrbitAction,
|
||||
PinpointStrike,
|
||||
RunwayAttack,
|
||||
SEAD,
|
||||
StartCommand,
|
||||
Targets,
|
||||
Task,
|
||||
WeaponType,
|
||||
PinpointStrike,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
||||
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.data.cap_capabilities_db import GUNFIGHTERS
|
||||
from game.data.weapons import Pylon, Weapon
|
||||
from game.factions.faction import Faction
|
||||
from game.settings import Settings
|
||||
from game.theater.controlpoint import (
|
||||
@@ -86,7 +86,7 @@ from game.theater.controlpoint import (
|
||||
)
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import knots_to_kph, nm_to_meter
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from gen.airsupportgen import AirSupport
|
||||
from gen.ato import AirTaskingOrder, Package
|
||||
from gen.callsigns import create_group_callsign_from_unit
|
||||
@@ -99,7 +99,6 @@ from gen.flights.flight import (
|
||||
)
|
||||
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
||||
from gen.runways import RunwayData
|
||||
from .conflictgen import Conflict
|
||||
from .flights.flightplan import (
|
||||
CasFlightPlan,
|
||||
LoiterFlightPlan,
|
||||
@@ -108,17 +107,14 @@ from .flights.flightplan import (
|
||||
)
|
||||
from .flights.traveltime import GroundSpeed, TotEstimator
|
||||
from .naming import namegen
|
||||
from .runways import RunwayAssigner
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
WARM_START_HELI_AIRSPEED = 120
|
||||
WARM_START_HELI_ALT = 500
|
||||
WARM_START_ALTITUDE = 3000
|
||||
WARM_START_AIRSPEED = 550
|
||||
WARM_START_HELI_ALT = meters(500)
|
||||
WARM_START_ALTITUDE = meters(3000)
|
||||
|
||||
RTB_ALTITUDE = 800
|
||||
RTB_ALTITUDE = meters(800)
|
||||
RTB_DISTANCE = 5000
|
||||
HELI_ALT = 500
|
||||
|
||||
@@ -266,6 +262,9 @@ class FlightData:
|
||||
#: The package that the flight belongs to.
|
||||
package: Package
|
||||
|
||||
#: The country that the flight belongs to.
|
||||
country: str
|
||||
|
||||
flight_type: FlightType
|
||||
|
||||
#: All units in the flight.
|
||||
@@ -303,7 +302,7 @@ class FlightData:
|
||||
|
||||
joker_fuel: Optional[int]
|
||||
|
||||
def __init__(self, package: Package, flight_type: FlightType,
|
||||
def __init__(self, package: Package, country: str, flight_type: FlightType,
|
||||
units: List[FlyingUnit], size: int, friendly: bool,
|
||||
departure_delay: timedelta, departure: RunwayData,
|
||||
arrival: RunwayData, divert: Optional[RunwayData],
|
||||
@@ -312,6 +311,7 @@ class FlightData:
|
||||
bingo_fuel: Optional[int],
|
||||
joker_fuel: Optional[int]) -> None:
|
||||
self.package = package
|
||||
self.country = country
|
||||
self.flight_type = flight_type
|
||||
self.units = units
|
||||
self.size = size
|
||||
@@ -782,6 +782,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
self.flights.append(FlightData(
|
||||
package=package,
|
||||
country=faction.country,
|
||||
flight_type=flight.flight_type,
|
||||
units=group.units,
|
||||
size=len(group.units),
|
||||
@@ -835,19 +836,21 @@ class AircraftConflictGenerator:
|
||||
else:
|
||||
alt = WARM_START_ALTITUDE
|
||||
|
||||
speed = knots_to_kph(GroundSpeed.for_flight(flight, alt))
|
||||
speed = GroundSpeed.for_flight(flight, alt)
|
||||
|
||||
pos = Point(at.x + random.randint(100, 1000), at.y + random.randint(100, 1000))
|
||||
|
||||
logging.info("airgen: {} for {} at {} at {}".format(flight.unit_type, side.id, alt, speed))
|
||||
logging.info(
|
||||
"airgen: {} for {} at {} at {}".format(flight.unit_type, side.id,
|
||||
alt, int(speed.kph)))
|
||||
group = self.m.flight_group(
|
||||
country=side,
|
||||
name=name,
|
||||
aircraft_type=flight.unit_type,
|
||||
airport=None,
|
||||
position=pos,
|
||||
altitude=alt,
|
||||
speed=speed,
|
||||
altitude=alt.meters,
|
||||
speed=speed.kph,
|
||||
maintask=None,
|
||||
group_size=flight.count)
|
||||
|
||||
@@ -870,8 +873,10 @@ class AircraftConflictGenerator:
|
||||
start_type=self._start_type(start_type),
|
||||
group_size=count)
|
||||
|
||||
def _add_radio_waypoint(self, group: FlyingGroup, position, altitude: int, airspeed: int = 600):
|
||||
point = group.add_waypoint(position, altitude, airspeed)
|
||||
def _add_radio_waypoint(self, group: FlyingGroup, position,
|
||||
altitude: Distance,
|
||||
airspeed: int = 600) -> MovingPoint:
|
||||
point = group.add_waypoint(position, altitude.meters, airspeed)
|
||||
point.alt_type = "RADIO"
|
||||
return point
|
||||
|
||||
@@ -887,7 +892,8 @@ class AircraftConflictGenerator:
|
||||
tod_location = position.point_from_heading(heading, RTB_DISTANCE)
|
||||
self._add_radio_waypoint(group, tod_location, last_waypoint.alt)
|
||||
|
||||
destination_waypoint = self._add_radio_waypoint(group, position, RTB_ALTITUDE)
|
||||
destination_waypoint = self._add_radio_waypoint(group, position,
|
||||
RTB_ALTITUDE)
|
||||
if isinstance(at, Airport):
|
||||
group.land_at(at)
|
||||
return destination_waypoint
|
||||
@@ -902,22 +908,39 @@ class AircraftConflictGenerator:
|
||||
else:
|
||||
assert False
|
||||
|
||||
def _setup_custom_payload(self, flight, group:FlyingGroup):
|
||||
if flight.use_custom_loadout:
|
||||
@staticmethod
|
||||
def _setup_custom_payload(flight: Flight, group: FlyingGroup) -> None:
|
||||
if not flight.use_custom_loadout:
|
||||
return
|
||||
|
||||
logging.info("Custom loadout for flight : " + flight.__repr__())
|
||||
for p in group.units:
|
||||
p.pylons.clear()
|
||||
logging.info("Custom loadout for flight : " + flight.__repr__())
|
||||
for p in group.units:
|
||||
p.pylons.clear()
|
||||
|
||||
for key in flight.loadout.keys():
|
||||
if "Pylon" + key in flight.unit_type.__dict__.keys():
|
||||
print(flight.loadout)
|
||||
weapon_dict = flight.unit_type.__dict__["Pylon" + key].__dict__
|
||||
if flight.loadout[key] in weapon_dict.keys():
|
||||
weapon = weapon_dict[flight.loadout[key]]
|
||||
group.load_pylon(weapon, int(key))
|
||||
else:
|
||||
logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type))
|
||||
for pylon_number, weapon in flight.loadout.items():
|
||||
if weapon is None:
|
||||
continue
|
||||
pylon = Pylon.for_aircraft(flight.unit_type, pylon_number)
|
||||
pylon.equip(group, weapon)
|
||||
|
||||
def _degrade_payload_to_era(self, flight: Flight,
|
||||
group: FlyingGroup) -> None:
|
||||
loadout = dict(group.units[0].pylons)
|
||||
for pylon_number, clsid in loadout.items():
|
||||
weapon = Weapon.from_clsid(clsid["CLSID"])
|
||||
if weapon is None:
|
||||
logging.error(f"Could not find weapon for clsid {clsid}")
|
||||
continue
|
||||
|
||||
if not weapon.available_on(self.game.date):
|
||||
pylon = Pylon.for_aircraft(flight.unit_type, pylon_number)
|
||||
for fallback in weapon.fallbacks:
|
||||
if not pylon.can_equip(fallback):
|
||||
continue
|
||||
if not fallback.available_on(self.game.date):
|
||||
continue
|
||||
pylon.equip(group, fallback)
|
||||
break
|
||||
|
||||
def clear_parking_slots(self) -> None:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
@@ -966,13 +989,13 @@ class AircraftConflictGenerator:
|
||||
# Creating a flight even those this isn't a fragged mission lets us
|
||||
# reuse the existing debriefing code.
|
||||
# TODO: Special flight type?
|
||||
flight = Flight(Package(control_point), aircraft, 1,
|
||||
flight = Flight(Package(control_point), faction.country, aircraft, 1,
|
||||
FlightType.BARCAP, "Cold", departure=control_point,
|
||||
arrival=control_point, divert=None)
|
||||
|
||||
group = self._generate_at_airport(
|
||||
name=namegen.next_unit_name(country, control_point.id,
|
||||
aircraft),
|
||||
name=namegen.next_aircraft_name(country, control_point.id,
|
||||
flight),
|
||||
side=country,
|
||||
unit_type=aircraft,
|
||||
count=1,
|
||||
@@ -1036,17 +1059,18 @@ class AircraftConflictGenerator:
|
||||
CoalitionHasAirdrome(coalition, flight.from_cp.id))
|
||||
|
||||
def generate_planned_flight(self, cp, country, flight:Flight):
|
||||
name = namegen.next_aircraft_name(country, cp.id, flight)
|
||||
try:
|
||||
if flight.start_type == "In Flight":
|
||||
group = self._generate_inflight(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
name=name,
|
||||
side=country,
|
||||
flight=flight,
|
||||
origin=cp)
|
||||
elif isinstance(cp, NavalControlPoint):
|
||||
group_name = cp.get_carrier_group_name()
|
||||
group = self._generate_at_group(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
name=name,
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
@@ -1057,8 +1081,7 @@ class AircraftConflictGenerator:
|
||||
raise RuntimeError(
|
||||
f"Attempted to spawn at airfield for non-airfield {cp}")
|
||||
group = self._generate_at_airport(
|
||||
name=namegen.next_unit_name(country, cp.id,
|
||||
flight.unit_type),
|
||||
name=name,
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
@@ -1070,7 +1093,7 @@ class AircraftConflictGenerator:
|
||||
logging.warning("No room on runway or parking slots. Starting from the air.")
|
||||
flight.start_type = "In Flight"
|
||||
group = self._generate_inflight(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
name=name,
|
||||
side=country,
|
||||
flight=flight,
|
||||
origin=cp)
|
||||
@@ -1315,6 +1338,8 @@ class AircraftConflictGenerator:
|
||||
# have their TOTs set.
|
||||
self.flights[-1].waypoints = [takeoff_point] + flight.points
|
||||
self._setup_custom_payload(flight, group)
|
||||
if self.game.settings.restrict_weapons_by_date:
|
||||
self._degrade_payload_to_era(flight, group)
|
||||
|
||||
def should_delay_flight(self, flight: Flight,
|
||||
start_time: timedelta) -> bool:
|
||||
@@ -1349,7 +1374,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
# And setting *our* waypoint TOT causes the takeoff time to show up in
|
||||
# the player's kneeboard.
|
||||
waypoint.tot = estimator.takeoff_time_for_flight(flight)
|
||||
waypoint.tot = flight.flight_plan.takeoff_time()
|
||||
# And finally assign it to the FlightData info so it shows correctly in
|
||||
# the briefing.
|
||||
self.flights[-1].departure_delay = start_time
|
||||
@@ -1383,11 +1408,15 @@ class PydcsWaypointBuilder:
|
||||
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = self.group.add_waypoint(
|
||||
Point(self.waypoint.x, self.waypoint.y), self.waypoint.alt,
|
||||
Point(self.waypoint.x, self.waypoint.y),
|
||||
self.waypoint.alt.meters,
|
||||
name=self.mission.string(self.waypoint.name))
|
||||
|
||||
if self.waypoint.flyover:
|
||||
waypoint.type = PointAction.FlyOverPoint.value
|
||||
waypoint.action = PointAction.FlyOverPoint
|
||||
# It seems we need to leave waypoint.type exactly as it is even
|
||||
# though it's set to "Turning Point". If I set this to "Fly Over
|
||||
# Point" and then save the mission in the ME DCS resets it.
|
||||
|
||||
waypoint.alt_type = self.waypoint.alt_type
|
||||
tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint)
|
||||
@@ -1477,10 +1506,7 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
|
||||
|
||||
target_group = self.package.target
|
||||
if isinstance(target_group, TheaterGroundObject):
|
||||
# Match search is used due to TheaterGroundObject.name not matching
|
||||
# the Mission group name because of SkyNet prefixes.
|
||||
tgroup = self.mission.find_group(target_group.group_name,
|
||||
search="match")
|
||||
tgroup = self.mission.find_group(target_group.group_name)
|
||||
if tgroup is not None:
|
||||
task = AttackGroup(tgroup.id, weapon_type=WeaponType.Auto)
|
||||
task.params["attackQtyLimit"] = False
|
||||
@@ -1503,7 +1529,7 @@ class CasIngressBuilder(PydcsWaypointBuilder):
|
||||
if isinstance(self.flight.flight_plan, CasFlightPlan):
|
||||
waypoint.add_task(EngageTargetsInZone(
|
||||
position=self.flight.flight_plan.target,
|
||||
radius=FRONTLINE_LENGTH / 2,
|
||||
radius=int(self.flight.flight_plan.engagement_distance.meters),
|
||||
targets=[
|
||||
Targets.All.GroundUnits.GroundVehicles,
|
||||
Targets.All.GroundUnits.AirDefence.AAA,
|
||||
@@ -1514,7 +1540,7 @@ class CasIngressBuilder(PydcsWaypointBuilder):
|
||||
logging.error(
|
||||
"No CAS waypoint found. Falling back to search and engage")
|
||||
waypoint.add_task(EngageTargets(
|
||||
max_distance=nm_to_meter(10),
|
||||
max_distance=int(nautical_miles(10).meters),
|
||||
targets=[
|
||||
Targets.All.GroundUnits.GroundVehicles,
|
||||
Targets.All.GroundUnits.AirDefence.AAA,
|
||||
@@ -1530,10 +1556,7 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
|
||||
|
||||
target_group = self.package.target
|
||||
if isinstance(target_group, TheaterGroundObject):
|
||||
# Match search is used due to TheaterGroundObject.name not matching
|
||||
# the Mission group name because of SkyNet prefixes.
|
||||
tgroup = self.mission.find_group(target_group.group_name,
|
||||
search="match")
|
||||
tgroup = self.mission.find_group(target_group.group_name)
|
||||
if tgroup is not None:
|
||||
task = AttackGroup(tgroup.id, weapon_type=WeaponType.Guided)
|
||||
task.params["expend"] = "All"
|
||||
@@ -1563,7 +1586,7 @@ class OcaAircraftIngressBuilder(PydcsWaypointBuilder):
|
||||
position=target.position,
|
||||
# Al Dhafra is 4 nm across at most. Add a little wiggle room in case
|
||||
# the airport position from DCS is not centered.
|
||||
radius=nm_to_meter(3),
|
||||
radius=int(nautical_miles(3).meters),
|
||||
targets=[Targets.All.Air]
|
||||
)
|
||||
task.params["attackQtyLimit"] = False
|
||||
@@ -1596,14 +1619,11 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
|
||||
|
||||
target_group = self.package.target
|
||||
if isinstance(target_group, TheaterGroundObject):
|
||||
# Match search is used due to TheaterGroundObject.name not matching
|
||||
# the Mission group name because of SkyNet prefixes.
|
||||
tgroup = self.mission.find_group(target_group.group_name,
|
||||
search="match")
|
||||
tgroup = self.mission.find_group(target_group.group_name)
|
||||
if tgroup is not None:
|
||||
waypoint.add_task(EngageTargetsInZone(
|
||||
position=tgroup.position,
|
||||
radius=nm_to_meter(30),
|
||||
radius=int(nautical_miles(30).meters),
|
||||
targets=[
|
||||
Targets.All.GroundUnits.AirDefence,
|
||||
])
|
||||
@@ -1685,7 +1705,7 @@ class SweepIngressBuilder(PydcsWaypointBuilder):
|
||||
return waypoint
|
||||
|
||||
waypoint.tasks.append(EngageTargets(
|
||||
max_distance=nm_to_meter(50),
|
||||
max_distance=int(nautical_miles(50).meters),
|
||||
targets=[Targets.All.Air.Planes.Fighters]))
|
||||
|
||||
return waypoint
|
||||
@@ -1728,7 +1748,7 @@ class JoinPointBuilder(PydcsWaypointBuilder):
|
||||
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/250183-task-follow-and-escort-temporarily-aborted
|
||||
waypoint.add_task(ControlledTask(EngageTargets(
|
||||
# TODO: From doctrine.
|
||||
max_distance=nm_to_meter(30),
|
||||
max_distance=int(nautical_miles(30).meters),
|
||||
targets=[Targets.All.Air.Planes.Fighters]
|
||||
)))
|
||||
|
||||
@@ -1749,22 +1769,20 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
|
||||
if not isinstance(self.flight.flight_plan, PatrollingFlightPlan):
|
||||
flight_plan_type = self.flight.flight_plan.__class__.__name__
|
||||
flight_plan = self.flight.flight_plan
|
||||
|
||||
if not isinstance(flight_plan, PatrollingFlightPlan):
|
||||
flight_plan_type = flight_plan.__class__.__name__
|
||||
logging.error(
|
||||
f"Cannot create race track for {self.flight} because "
|
||||
f"{flight_plan_type} does not define a patrol.")
|
||||
return waypoint
|
||||
|
||||
racetrack = ControlledTask(OrbitAction(
|
||||
altitude=waypoint.alt,
|
||||
pattern=OrbitAction.OrbitPattern.RaceTrack
|
||||
))
|
||||
self.set_waypoint_tot(
|
||||
waypoint, self.flight.flight_plan.patrol_start_time)
|
||||
racetrack.stop_after_time(
|
||||
int(self.flight.flight_plan.patrol_end_time.total_seconds()))
|
||||
waypoint.add_task(racetrack)
|
||||
# NB: It's important that the engage task comes before the orbit task.
|
||||
# Though they're on the same waypoint, if the orbit task comes first it
|
||||
# is their first priority and they will not engage any targets because
|
||||
# they're fully focused on orbiting. If the STE task is first, they will
|
||||
# engage targets if available and orbit if they find nothing to shoot.
|
||||
|
||||
# TODO: Move the properties of this task into the flight plan?
|
||||
# CAP is the only current user of this so it's not a big deal, but might
|
||||
@@ -1772,8 +1790,19 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
# later.
|
||||
cap_types = {FlightType.BARCAP, FlightType.TARCAP}
|
||||
if self.flight.flight_type in cap_types:
|
||||
waypoint.tasks.append(EngageTargets(max_distance=nm_to_meter(50),
|
||||
targets=[Targets.All.Air]))
|
||||
engagement_distance = int(flight_plan.engagement_distance.meters)
|
||||
waypoint.tasks.append(
|
||||
EngageTargets(max_distance=engagement_distance,
|
||||
targets=[Targets.All.Air]))
|
||||
|
||||
racetrack = ControlledTask(OrbitAction(
|
||||
altitude=waypoint.alt,
|
||||
pattern=OrbitAction.OrbitPattern.RaceTrack
|
||||
))
|
||||
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
|
||||
racetrack.stop_after_time(
|
||||
int(flight_plan.patrol_end_time.total_seconds()))
|
||||
waypoint.add_task(racetrack)
|
||||
|
||||
return waypoint
|
||||
|
||||
|
||||
@@ -410,7 +410,10 @@ AIRFIELD_DATA = {
|
||||
icao="OMLW",
|
||||
elevation=400,
|
||||
runway_length=10768,
|
||||
atc=AtcData(MHz(4, 175), MHz(39, 250), MHz(119, 300), MHz(250, 850)),
|
||||
tacan=TacanChannel(121, TacanBand.X),
|
||||
tacan_callsign="OMLW",
|
||||
vor=("OMLW", MHz(117,400)),
|
||||
atc=AtcData(MHz(4, 225), MHz(39, 350), MHz(119, 300), MHz(250, 950)),
|
||||
),
|
||||
|
||||
"Al Dhafra AB": AirfieldData(
|
||||
|
||||
40
gen/armor.py
40
gen/armor.py
@@ -50,6 +50,8 @@ FIGHT_DISTANCE = 3500
|
||||
|
||||
RANDOM_OFFSET_ATTACK = 250
|
||||
|
||||
INFANTRY_GROUP_SIZE = 5
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JtacInfo:
|
||||
@@ -206,7 +208,7 @@ class GroundConflictGenerator:
|
||||
u = random.choice(manpads)
|
||||
self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp, u), u,
|
||||
namegen.next_infantry_name(side, cp.id, u), u,
|
||||
position=infantry_position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
@@ -220,18 +222,18 @@ class GroundConflictGenerator:
|
||||
u = random.choice(possible_infantry_units)
|
||||
self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp, u), u,
|
||||
namegen.next_infantry_name(side, cp.id, u), u,
|
||||
position=infantry_position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
move_formation=PointAction.OffRoad)
|
||||
|
||||
for i in range(random.randint(3, 10)):
|
||||
for i in range(INFANTRY_GROUP_SIZE):
|
||||
u = random.choice(possible_infantry_units)
|
||||
position = infantry_position.random_point_within(55, 5)
|
||||
self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp, u), u,
|
||||
namegen.next_infantry_name(side, cp.id, u), u,
|
||||
position=position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
@@ -281,7 +283,7 @@ class GroundConflictGenerator:
|
||||
|
||||
# Hold position
|
||||
dcs_group.points[1].tasks.append(Hold())
|
||||
retreat = self.find_retreat_point(dcs_group, heading_sum(forward_heading, 180), (int)(RETREAT_DISTANCE/3))
|
||||
retreat = self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE/3))
|
||||
dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad)
|
||||
dcs_group.points[2].tasks.append(Hold())
|
||||
dcs_group.add_waypoint(retreat, PointAction.OffRoad)
|
||||
@@ -350,9 +352,14 @@ class GroundConflictGenerator:
|
||||
to_cp.position.random_point_within(500, 0)
|
||||
)
|
||||
else:
|
||||
# We use an offset heading here because DCS doesn't always
|
||||
# force vehicles to move if there's no heading change.
|
||||
offset_heading = forward_heading - 2
|
||||
if offset_heading < 0:
|
||||
offset_heading = 358
|
||||
attack_point = self.find_offensive_point(
|
||||
dcs_group,
|
||||
forward_heading,
|
||||
offset_heading,
|
||||
AGGRESIVE_MOVE_DISTANCE
|
||||
)
|
||||
dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
|
||||
@@ -365,7 +372,12 @@ class GroundConflictGenerator:
|
||||
to_cp.position.random_point_within(500, 0)
|
||||
)
|
||||
else:
|
||||
attack_point = self.find_offensive_point(dcs_group, forward_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE)
|
||||
# We use an offset heading here because DCS doesn't always
|
||||
# force vehicles to move if there's no heading change.
|
||||
offset_heading = forward_heading - 1
|
||||
if offset_heading < 0:
|
||||
offset_heading = 359
|
||||
attack_point = self.find_offensive_point(dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE)
|
||||
dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
|
||||
elif stance == CombatStance.ELIMINATION:
|
||||
# In elimination mode, the units focus on destroying as much enemy groups as possible
|
||||
@@ -675,12 +687,14 @@ class GroundConflictGenerator:
|
||||
else:
|
||||
g.set_skill(self.game.settings.enemy_vehicle_skill)
|
||||
positioned_groups.append((g, group))
|
||||
self.gen_infantry_group_for_group(
|
||||
g,
|
||||
is_player,
|
||||
self.mission.country(country),
|
||||
opposite_heading(spawn_heading)
|
||||
)
|
||||
|
||||
if group.role in [CombatGroupRole.APC, CombatGroupRole.IFV]:
|
||||
self.gen_infantry_group_for_group(
|
||||
g,
|
||||
is_player,
|
||||
self.mission.country(country),
|
||||
opposite_heading(spawn_heading)
|
||||
)
|
||||
else:
|
||||
logging.warning(f"Unable to get valid position for {group}")
|
||||
|
||||
|
||||
21
gen/ato.py
21
gen/ato.py
@@ -17,8 +17,10 @@ from typing import Dict, List, Optional
|
||||
from dcs.mapping import Point
|
||||
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.utils import Speed
|
||||
from .flights.flight import Flight, FlightType
|
||||
from .flights.flightplan import FormationFlightPlan
|
||||
from .flights.traveltime import TotEstimator
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -53,13 +55,18 @@ class Package:
|
||||
|
||||
delay: int = field(default=0)
|
||||
|
||||
#: True if the package ToT should be reset to ASAP whenever the player makes
|
||||
#: a change. This is really a UI property rather than a game property, but
|
||||
#: we want it to persist in the save.
|
||||
auto_asap: bool = field(default=False)
|
||||
|
||||
#: Desired TOT as an offset from mission start.
|
||||
time_over_target: timedelta = field(default=timedelta())
|
||||
|
||||
waypoints: Optional[PackageWaypoints] = field(default=None)
|
||||
|
||||
@property
|
||||
def formation_speed(self) -> Optional[int]:
|
||||
def formation_speed(self) -> Optional[Speed]:
|
||||
"""The speed of the package when in formation.
|
||||
|
||||
If none of the flights in the package will join a formation, this
|
||||
@@ -117,6 +124,18 @@ class Package:
|
||||
return max(times)
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> Optional[timedelta]:
|
||||
times = []
|
||||
for flight in self.flights:
|
||||
times.append(flight.flight_plan.mission_departure_time)
|
||||
if times:
|
||||
return max(times)
|
||||
return None
|
||||
|
||||
def set_tot_asap(self) -> None:
|
||||
self.time_over_target = TotEstimator(self).earliest_tot()
|
||||
|
||||
def add_flight(self, flight: Flight) -> None:
|
||||
"""Adds a flight to the package."""
|
||||
self.flights.append(flight)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import logging
|
||||
import random
|
||||
from typing import Tuple, Optional
|
||||
|
||||
from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
from shapely.geometry import LineString, Point as ShapelyPoint
|
||||
|
||||
from game.theater.conflicttheater import ConflictTheater, FrontLine
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
@@ -56,7 +56,7 @@ class Conflict:
|
||||
"""
|
||||
center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
|
||||
left_heading = heading_sum(heading, -90)
|
||||
right_heading = heading_sum(heading, 90)
|
||||
right_heading = heading_sum(heading, 90)
|
||||
left_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater)
|
||||
right_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater)
|
||||
distance = int(left_position.distance_to_point(right_position))
|
||||
@@ -83,12 +83,25 @@ class Conflict:
|
||||
@classmethod
|
||||
def extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
|
||||
"""Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance"""
|
||||
pos = initial
|
||||
for distance in range(0, int(max_distance), 100):
|
||||
pos = initial.point_from_heading(heading, distance)
|
||||
if not theater.is_on_land(pos):
|
||||
return initial.point_from_heading(heading, distance - 100)
|
||||
return pos
|
||||
extended = initial.point_from_heading(heading, max_distance)
|
||||
if theater.landmap is None:
|
||||
# TODO: Why is this possible?
|
||||
return extended
|
||||
|
||||
p0 = ShapelyPoint(initial.x, initial.y)
|
||||
p1 = ShapelyPoint(extended.x, extended.y)
|
||||
line = LineString([p0, p1])
|
||||
|
||||
intersection = line.intersection(
|
||||
theater.landmap.inclusion_zone_only.boundary)
|
||||
if intersection.is_empty:
|
||||
# Max extent does not intersect with the boundary of the inclusion
|
||||
# zone, so the full front line is usable. This does assume that the
|
||||
# front line was centered on a valid location.
|
||||
return extended
|
||||
|
||||
# Otherwise extend the front line only up to the intersection.
|
||||
return initial.point_from_heading(heading, p0.distance(intersection))
|
||||
|
||||
@classmethod
|
||||
def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater, coerce=True) -> Optional[Point]:
|
||||
|
||||
@@ -21,7 +21,7 @@ class EnvironmentGenerator:
|
||||
def set_fog(self, fog: Optional[Fog]) -> None:
|
||||
if fog is None:
|
||||
return
|
||||
self.mission.weather.fog_visibility = fog.visibility
|
||||
self.mission.weather.fog_visibility = fog.visibility.meters
|
||||
self.mission.weather.fog_thickness = fog.thickness
|
||||
|
||||
def set_wind(self, wind: WindConditions) -> None:
|
||||
|
||||
@@ -8,7 +8,6 @@ from dcs.ships import (
|
||||
Type_052C_Destroyer,
|
||||
Type_052B_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
)
|
||||
|
||||
from game.factions.faction import Faction
|
||||
@@ -27,10 +26,8 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
||||
include_frigate = random.choice([True, True, False])
|
||||
include_dd = random.choice([True, False])
|
||||
|
||||
if include_dd:
|
||||
include_cc = random.choice([True, False])
|
||||
else:
|
||||
include_cc = False
|
||||
if not any([include_frigate, include_dd]):
|
||||
include_frigate = True
|
||||
|
||||
if include_frigate:
|
||||
self.add_unit(Type_054A_Frigate, "FF1", self.position.x + 1200, self.position.y + 900, self.heading)
|
||||
@@ -41,10 +38,6 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
||||
self.add_unit(dd_type, "DD1", self.position.x + 2400, self.position.y + 900, self.heading)
|
||||
self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading)
|
||||
|
||||
if include_cc:
|
||||
cc_type = random.choice([CGN_1144_2_Pyotr_Velikiy])
|
||||
self.add_unit(cc_type, "CC1", self.position.x, self.position.y, self.heading)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ from dcs.ships import (
|
||||
FFG_11540_Neustrashimy,
|
||||
FF_1135M_Rezky,
|
||||
CG_1164_Moskva,
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
SSK_877,
|
||||
SSK_641B
|
||||
)
|
||||
@@ -35,6 +34,9 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||
else:
|
||||
include_cc = False
|
||||
|
||||
if not any([include_frigate, include_dd, include_cc]):
|
||||
include_frigate = True
|
||||
|
||||
if include_frigate:
|
||||
frigate_type = random.choice([FFL_1124_4_Grisha, FSG_1241_1MP_Molniya])
|
||||
self.add_unit(frigate_type, "FF1", self.position.x + 1200, self.position.y + 900, self.heading)
|
||||
@@ -46,8 +48,9 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||
self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading)
|
||||
|
||||
if include_cc:
|
||||
cc_type = random.choice([CG_1164_Moskva, CGN_1144_2_Pyotr_Velikiy])
|
||||
self.add_unit(cc_type, "CC1", self.position.x, self.position.y, self.heading)
|
||||
# Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster.
|
||||
# See https://github.com/Khopa/dcs_liberation/issues/567
|
||||
self.add_unit(CG_1164_Moskva, "CC1", self.position.x, self.position.y, self.heading)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
|
||||
|
||||
@@ -3,9 +3,12 @@ from __future__ import annotations
|
||||
import logging
|
||||
import operator
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from enum import Enum, auto
|
||||
from typing import (
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
@@ -18,41 +21,28 @@ from typing import (
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from game.data.radar_db import UNITS_WITH_RADAR
|
||||
from game.infos.information import Information
|
||||
from game.procurement import AircraftProcurementRequest
|
||||
from game.theater import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
Fob,
|
||||
FrontLine,
|
||||
MissionTarget,
|
||||
OffMapSpawn,
|
||||
SamGroundObject,
|
||||
TheaterGroundObject,
|
||||
)
|
||||
# Avoid importing some types that cause circular imports unless type checking.
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
EwrGroundObject,
|
||||
NavalGroundObject, VehicleGroupGroundObject,
|
||||
NavalGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
)
|
||||
from game.utils import nm_to_meter
|
||||
from game.utils import Distance, nautical_miles
|
||||
from gen import Conflict
|
||||
from gen.ato import Package
|
||||
from gen.flights.ai_flight_planner_db import (
|
||||
ANTISHIP_CAPABLE,
|
||||
ANTISHIP_PREFERRED,
|
||||
CAP_CAPABLE,
|
||||
CAP_PREFERRED,
|
||||
CAS_CAPABLE,
|
||||
CAS_PREFERRED,
|
||||
RUNWAY_ATTACK_CAPABLE,
|
||||
RUNWAY_ATTACK_PREFERRED,
|
||||
SEAD_CAPABLE,
|
||||
SEAD_PREFERRED,
|
||||
STRIKE_CAPABLE,
|
||||
STRIKE_PREFERRED, capable_aircraft_for_task, preferred_aircraft_for_task,
|
||||
)
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.closestairfields import (
|
||||
ClosestAirfields,
|
||||
ObjectiveDistanceCache,
|
||||
@@ -64,11 +54,17 @@ from gen.flights.flight import (
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
|
||||
# Avoid importing some types that cause circular imports unless type checking.
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
|
||||
|
||||
class EscortType(Enum):
|
||||
AirToAir = auto()
|
||||
Sead = auto()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProposedFlight:
|
||||
"""A flight outline proposed by the mission planner.
|
||||
@@ -85,7 +81,13 @@ class ProposedFlight:
|
||||
num_aircraft: int
|
||||
|
||||
#: The maximum distance between the objective and the departure airfield.
|
||||
max_distance: int
|
||||
max_distance: Distance
|
||||
|
||||
#: The type of threat this flight defends against if it is an escort. Escort
|
||||
#: flights will be pruned if the rest of the package is not threatened by
|
||||
#: the threat they defend against. If this flight is not an escort, this
|
||||
#: field is None.
|
||||
escort_type: Optional[EscortType] = field(default=None)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.task} {self.num_aircraft} ship"
|
||||
@@ -123,7 +125,7 @@ class AircraftAllocator:
|
||||
|
||||
def find_aircraft_for_flight(
|
||||
self, flight: ProposedFlight
|
||||
) -> Optional[Tuple[ControlPoint, FlyingType]]:
|
||||
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
|
||||
"""Finds aircraft suitable for the given mission.
|
||||
|
||||
Searches for aircraft capable of performing the given mission within the
|
||||
@@ -142,13 +144,8 @@ class AircraftAllocator:
|
||||
on subsequent calls. If the found aircraft are not used, the caller is
|
||||
responsible for returning them to the inventory.
|
||||
"""
|
||||
result = self.find_aircraft_of_type(
|
||||
flight, preferred_aircraft_for_task(flight.task)
|
||||
)
|
||||
if result is not None:
|
||||
return result
|
||||
return self.find_aircraft_of_type(
|
||||
flight, capable_aircraft_for_task(flight.task)
|
||||
flight, aircraft_for_task(flight.task)
|
||||
)
|
||||
|
||||
def find_aircraft_of_type(
|
||||
@@ -178,9 +175,11 @@ class PackageBuilder:
|
||||
closest_airfields: ClosestAirfields,
|
||||
global_inventory: GlobalAircraftInventory,
|
||||
is_player: bool,
|
||||
package_country: str,
|
||||
start_type: str) -> None:
|
||||
self.closest_airfields = closest_airfields
|
||||
self.is_player = is_player
|
||||
self.package_country = package_country
|
||||
self.package = Package(location)
|
||||
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
|
||||
is_player)
|
||||
@@ -204,15 +203,15 @@ class PackageBuilder:
|
||||
else:
|
||||
start_type = self.start_type
|
||||
|
||||
flight = Flight(self.package, aircraft, plan.num_aircraft, plan.task,
|
||||
flight = Flight(self.package, self.package_country, aircraft, plan.num_aircraft, plan.task,
|
||||
start_type, departure=airfield, arrival=airfield,
|
||||
divert=self.find_divert_field(aircraft, airfield))
|
||||
self.package.add_flight(flight)
|
||||
return True
|
||||
|
||||
def find_divert_field(self, aircraft: FlyingType,
|
||||
def find_divert_field(self, aircraft: Type[FlyingType],
|
||||
arrival: ControlPoint) -> Optional[ControlPoint]:
|
||||
divert_limit = nm_to_meter(150)
|
||||
divert_limit = nautical_miles(150)
|
||||
for airfield in self.closest_airfields.airfields_within(divert_limit):
|
||||
if airfield.captured != self.is_player:
|
||||
continue
|
||||
@@ -241,8 +240,8 @@ class ObjectiveFinder:
|
||||
"""Identifies potential objectives for the mission planner."""
|
||||
|
||||
# TODO: Merge into doctrine.
|
||||
AIRFIELD_THREAT_RANGE = nm_to_meter(150)
|
||||
SAM_THREAT_RANGE = nm_to_meter(100)
|
||||
AIRFIELD_THREAT_RANGE = nautical_miles(150)
|
||||
SAM_THREAT_RANGE = nautical_miles(100)
|
||||
|
||||
def __init__(self, game: Game, is_player: bool) -> None:
|
||||
self.game = game
|
||||
@@ -266,7 +265,7 @@ class ObjectiveFinder:
|
||||
if ground_object.name in found_targets:
|
||||
continue
|
||||
|
||||
if not self.object_has_radar(ground_object):
|
||||
if not ground_object.has_radar:
|
||||
continue
|
||||
|
||||
# TODO: Yield in order of most threatening.
|
||||
@@ -349,12 +348,35 @@ class ObjectiveFinder:
|
||||
found_targets: Set[str] = set()
|
||||
for enemy_cp in self.enemy_control_points():
|
||||
for ground_object in enemy_cp.ground_objects:
|
||||
# TODO: Reuse ground_object.mission_types.
|
||||
# The mission types for ground objects are currently not
|
||||
# accurate because we include things like strike and BAI for all
|
||||
# targets since they have different planning behavior (waypoint
|
||||
# generation is better for players with strike when the targets
|
||||
# are stationary, AI behavior against weaker air defenses is
|
||||
# better with BAI), so that's not a useful filter. Once we have
|
||||
# better control over planning profiles and target dependent
|
||||
# loadouts we can clean this up.
|
||||
if isinstance(ground_object, VehicleGroupGroundObject):
|
||||
# BAI target, not strike target.
|
||||
continue
|
||||
|
||||
if isinstance(ground_object, NavalGroundObject):
|
||||
# Anti-ship target, not strike target.
|
||||
continue
|
||||
|
||||
if isinstance(ground_object, SamGroundObject):
|
||||
# SAMs are targeted by DEAD. No need to double plan.
|
||||
continue
|
||||
|
||||
is_building = isinstance(ground_object, BuildingGroundObject)
|
||||
is_fob = isinstance(enemy_cp, Fob)
|
||||
if is_building and is_fob and ground_object.airbase_group:
|
||||
# This is the FOB structure itself. Can't be repaired or
|
||||
# targeted by the player, so shouldn't be targetable by the
|
||||
# AI.
|
||||
continue
|
||||
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
if ground_object.name in found_targets:
|
||||
@@ -368,15 +390,6 @@ class ObjectiveFinder:
|
||||
for target, _range in targets:
|
||||
yield target
|
||||
|
||||
@staticmethod
|
||||
def object_has_radar(ground_object: TheaterGroundObject) -> bool:
|
||||
"""Returns True if the ground object contains a unit with radar."""
|
||||
for group in ground_object.groups:
|
||||
for unit in group.units:
|
||||
if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR:
|
||||
return True
|
||||
return False
|
||||
|
||||
def front_lines(self) -> Iterator[FrontLine]:
|
||||
"""Iterates over all active front lines in the theater."""
|
||||
for cp in self.friendly_control_points():
|
||||
@@ -467,25 +480,42 @@ class CoalitionMissionPlanner:
|
||||
"""
|
||||
|
||||
# TODO: Merge into doctrine, also limit by aircraft.
|
||||
MAX_CAP_RANGE = nm_to_meter(100)
|
||||
MAX_CAS_RANGE = nm_to_meter(50)
|
||||
MAX_ANTISHIP_RANGE = nm_to_meter(150)
|
||||
MAX_BAI_RANGE = nm_to_meter(150)
|
||||
MAX_OCA_RANGE = nm_to_meter(150)
|
||||
MAX_SEAD_RANGE = nm_to_meter(150)
|
||||
MAX_STRIKE_RANGE = nm_to_meter(150)
|
||||
MAX_CAP_RANGE = nautical_miles(100)
|
||||
MAX_CAS_RANGE = nautical_miles(50)
|
||||
MAX_ANTISHIP_RANGE = nautical_miles(150)
|
||||
MAX_BAI_RANGE = nautical_miles(150)
|
||||
MAX_OCA_RANGE = nautical_miles(150)
|
||||
MAX_SEAD_RANGE = nautical_miles(150)
|
||||
MAX_STRIKE_RANGE = nautical_miles(150)
|
||||
|
||||
def __init__(self, game: Game, is_player: bool) -> None:
|
||||
self.game = game
|
||||
self.is_player = is_player
|
||||
self.objective_finder = ObjectiveFinder(self.game, self.is_player)
|
||||
self.ato = self.game.blue_ato if is_player else self.game.red_ato
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
self.procurement_requests: List[AircraftProcurementRequest] = []
|
||||
|
||||
def propose_missions(self) -> Iterator[ProposedMission]:
|
||||
"""Identifies and iterates over potential mission in priority order."""
|
||||
def critical_missions(self) -> Iterator[ProposedMission]:
|
||||
"""Identifies the most important missions to plan this turn.
|
||||
|
||||
Non-critical missions that cannot be fulfilled will create purchase
|
||||
orders for the next turn. Critical missions will create a purchase order
|
||||
unless the mission can be doubly fulfilled. In other words, the AI will
|
||||
attempt to have *double* the aircraft it needs for these missions to
|
||||
ensure that they can be planned again next turn even if all aircraft are
|
||||
eliminated this turn.
|
||||
"""
|
||||
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||
for cp in self.objective_finder.vulnerable_control_points():
|
||||
# Plan three rounds of CAP to give ~90 minutes coverage. Spacing
|
||||
# these out appropriately is done in stagger_missions.
|
||||
yield ProposedMission(cp, [
|
||||
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||
])
|
||||
yield ProposedMission(cp, [
|
||||
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||
])
|
||||
yield ProposedMission(cp, [
|
||||
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||
])
|
||||
@@ -493,10 +523,15 @@ class CoalitionMissionPlanner:
|
||||
# Find front lines, plan CAS.
|
||||
for front_line in self.objective_finder.front_lines():
|
||||
yield ProposedMission(front_line, [
|
||||
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE),
|
||||
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
|
||||
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE,
|
||||
EscortType.AirToAir),
|
||||
])
|
||||
|
||||
def propose_missions(self) -> Iterator[ProposedMission]:
|
||||
"""Identifies and iterates over potential mission in priority order."""
|
||||
yield from self.critical_missions()
|
||||
|
||||
# Find enemy SAM sites with ranges that cover friendly CPs, front lines,
|
||||
# or objects, plan DEAD.
|
||||
# Find enemy SAM sites with ranges that extend to within 50 nmi of
|
||||
@@ -505,39 +540,55 @@ class CoalitionMissionPlanner:
|
||||
yield ProposedMission(sam, [
|
||||
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE),
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE,
|
||||
EscortType.AirToAir),
|
||||
])
|
||||
|
||||
for group in self.objective_finder.threatening_ships():
|
||||
yield ProposedMission(group, [
|
||||
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_ANTISHIP_RANGE),
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_ANTISHIP_RANGE,
|
||||
EscortType.AirToAir),
|
||||
])
|
||||
|
||||
for group in self.objective_finder.threatening_vehicle_groups():
|
||||
yield ProposedMission(group, [
|
||||
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE),
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_BAI_RANGE),
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_BAI_RANGE,
|
||||
EscortType.AirToAir),
|
||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE,
|
||||
EscortType.Sead),
|
||||
])
|
||||
|
||||
for target in self.objective_finder.oca_targets(min_aircraft=20):
|
||||
yield ProposedMission(target, [
|
||||
ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE),
|
||||
flights = [
|
||||
ProposedFlight(FlightType.OCA_RUNWAY, 2, self.MAX_OCA_RANGE),
|
||||
]
|
||||
if self.game.settings.default_start_type == "Cold":
|
||||
# Only schedule if the default start type is Cold. If the player
|
||||
# has set anything else there are no targets to hit.
|
||||
flights.append(ProposedFlight(FlightType.OCA_AIRCRAFT, 2,
|
||||
self.MAX_OCA_RANGE))
|
||||
flights.extend([
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_OCA_RANGE),
|
||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE),
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_OCA_RANGE,
|
||||
EscortType.AirToAir),
|
||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE,
|
||||
EscortType.Sead),
|
||||
])
|
||||
yield ProposedMission(target, flights)
|
||||
|
||||
# Plan strike missions.
|
||||
for target in self.objective_finder.strike_targets():
|
||||
yield ProposedMission(target, [
|
||||
ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE),
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE),
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE,
|
||||
EscortType.AirToAir),
|
||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE,
|
||||
EscortType.Sead),
|
||||
])
|
||||
|
||||
def plan_missions(self) -> None:
|
||||
@@ -545,6 +596,9 @@ class CoalitionMissionPlanner:
|
||||
for proposed_mission in self.propose_missions():
|
||||
self.plan_mission(proposed_mission)
|
||||
|
||||
for critical_mission in self.critical_missions():
|
||||
self.plan_mission(critical_mission, reserves=True)
|
||||
|
||||
self.stagger_missions()
|
||||
|
||||
for cp in self.objective_finder.friendly_control_points():
|
||||
@@ -553,48 +607,128 @@ class CoalitionMissionPlanner:
|
||||
self.message("Unused aircraft",
|
||||
f"{available} {aircraft.id} from {cp}")
|
||||
|
||||
def plan_mission(self, mission: ProposedMission) -> None:
|
||||
def plan_flight(self, mission: ProposedMission, flight: ProposedFlight,
|
||||
builder: PackageBuilder, missing_types: Set[FlightType],
|
||||
for_reserves: bool) -> None:
|
||||
if not builder.plan_flight(flight):
|
||||
missing_types.add(flight.task)
|
||||
purchase_order = AircraftProcurementRequest(
|
||||
near=mission.location,
|
||||
range=flight.max_distance,
|
||||
task_capability=flight.task,
|
||||
number=flight.num_aircraft
|
||||
)
|
||||
if for_reserves:
|
||||
# Reserves are planned for critical missions, so prioritize
|
||||
# those orders over aircraft needed for non-critical missions.
|
||||
self.procurement_requests.insert(0, purchase_order)
|
||||
else:
|
||||
self.procurement_requests.append(purchase_order)
|
||||
|
||||
def scrub_mission_missing_aircraft(
|
||||
self, mission: ProposedMission, builder: PackageBuilder,
|
||||
missing_types: Set[FlightType],
|
||||
not_attempted: Iterable[ProposedFlight],
|
||||
reserves: bool) -> None:
|
||||
# Try to plan the rest of the mission just so we can count the missing
|
||||
# types to buy.
|
||||
for flight in not_attempted:
|
||||
self.plan_flight(mission, flight, builder, missing_types, reserves)
|
||||
|
||||
missing_types_str = ", ".join(
|
||||
sorted([t.name for t in missing_types]))
|
||||
builder.release_planned_aircraft()
|
||||
desc = "reserve aircraft" if reserves else "aircraft"
|
||||
self.message(
|
||||
"Insufficient aircraft",
|
||||
f"Not enough {desc} in range for {mission.location.name} "
|
||||
f"capable of: {missing_types_str}")
|
||||
|
||||
def check_needed_escorts(
|
||||
self, builder: PackageBuilder) -> Dict[EscortType, bool]:
|
||||
threats = defaultdict(bool)
|
||||
for flight in builder.package.flights:
|
||||
if self.threat_zones.threatened_by_aircraft(flight):
|
||||
threats[EscortType.AirToAir] = True
|
||||
if self.threat_zones.threatened_by_air_defense(flight):
|
||||
threats[EscortType.Sead] = True
|
||||
return threats
|
||||
|
||||
def plan_mission(self, mission: ProposedMission,
|
||||
reserves: bool = False) -> None:
|
||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||
|
||||
if self.game.settings.perf_ai_parking_start:
|
||||
start_type = "Cold"
|
||||
if self.is_player:
|
||||
package_country = self.game.player_country
|
||||
else:
|
||||
start_type = "Warm"
|
||||
package_country = self.game.enemy_country
|
||||
|
||||
builder = PackageBuilder(
|
||||
mission.location,
|
||||
self.objective_finder.closest_airfields_to(mission.location),
|
||||
self.game.aircraft_inventory,
|
||||
self.is_player,
|
||||
start_type
|
||||
package_country,
|
||||
self.game.settings.default_start_type
|
||||
)
|
||||
|
||||
# Attempt to plan all the main elements of the mission first. Escorts
|
||||
# will be planned separately so we can prune escorts for packages that
|
||||
# are not expected to encounter that type of threat.
|
||||
missing_types: Set[FlightType] = set()
|
||||
escorts = []
|
||||
for proposed_flight in mission.flights:
|
||||
if not builder.plan_flight(proposed_flight):
|
||||
missing_types.add(proposed_flight.task)
|
||||
self.procurement_requests.append(AircraftProcurementRequest(
|
||||
near=mission.location,
|
||||
range=proposed_flight.max_distance,
|
||||
task_capability=proposed_flight.task,
|
||||
number=proposed_flight.num_aircraft
|
||||
))
|
||||
if proposed_flight.escort_type is not None:
|
||||
# Escorts are planned after the primary elements of the package.
|
||||
# If the package does not need escorts they may be pruned.
|
||||
escorts.append(proposed_flight)
|
||||
continue
|
||||
self.plan_flight(mission, proposed_flight, builder, missing_types,
|
||||
reserves)
|
||||
|
||||
if missing_types:
|
||||
missing_types_str = ", ".join(
|
||||
sorted([t.name for t in missing_types]))
|
||||
self.scrub_mission_missing_aircraft(mission, builder, missing_types,
|
||||
escorts, reserves)
|
||||
return
|
||||
|
||||
# Create flight plans for the main flights of the package so we can
|
||||
# determine threats. This is done *after* creating all of the flights
|
||||
# rather than as each flight is added because the flight plan for
|
||||
# flights that will rendezvous with their package will be affected by
|
||||
# the other flights in the package. Escorts will not be able to
|
||||
# contribute to this.
|
||||
flight_plan_builder = FlightPlanBuilder(self.game, builder.package,
|
||||
self.is_player)
|
||||
for flight in builder.package.flights:
|
||||
flight_plan_builder.populate_flight_plan(flight)
|
||||
|
||||
needed_escorts = self.check_needed_escorts(builder)
|
||||
for escort in escorts:
|
||||
# This list was generated from the not None set, so this should be
|
||||
# impossible.
|
||||
assert escort.escort_type is not None
|
||||
if needed_escorts[escort.escort_type]:
|
||||
self.plan_flight(mission, escort, builder, missing_types,
|
||||
reserves)
|
||||
|
||||
# Check again for unavailable aircraft. If the escort was required and
|
||||
# none were found, scrub the mission.
|
||||
if missing_types:
|
||||
self.scrub_mission_missing_aircraft(mission, builder, missing_types,
|
||||
escorts, reserves)
|
||||
return
|
||||
|
||||
if reserves:
|
||||
# Mission is planned reserves which will not be used this turn.
|
||||
# Return reserves to the inventory.
|
||||
builder.release_planned_aircraft()
|
||||
self.message(
|
||||
"Insufficient aircraft",
|
||||
f"Not enough aircraft in range for {mission.location.name} "
|
||||
f"capable of: {missing_types_str}")
|
||||
return
|
||||
|
||||
package = builder.build()
|
||||
flight_plan_builder = FlightPlanBuilder(self.game, package,
|
||||
self.is_player)
|
||||
# Add flight plans for escorts.
|
||||
for flight in package.flights:
|
||||
flight_plan_builder.populate_flight_plan(flight)
|
||||
if not flight.flight_plan.waypoints:
|
||||
flight_plan_builder.populate_flight_plan(flight)
|
||||
self.ato.add_package(package)
|
||||
|
||||
def stagger_missions(self) -> None:
|
||||
@@ -607,10 +741,12 @@ class CoalitionMissionPlanner:
|
||||
|
||||
dca_types = {
|
||||
FlightType.BARCAP,
|
||||
FlightType.INTERCEPTION,
|
||||
FlightType.TARCAP,
|
||||
}
|
||||
|
||||
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(
|
||||
timedelta
|
||||
)
|
||||
non_dca_packages = [p for p in self.ato.packages if
|
||||
p.primary_task not in dca_types]
|
||||
|
||||
@@ -623,8 +759,22 @@ class CoalitionMissionPlanner:
|
||||
for package in self.ato.packages:
|
||||
tot = TotEstimator(package).earliest_tot()
|
||||
if package.primary_task in dca_types:
|
||||
# All CAP missions should be on station ASAP.
|
||||
package.time_over_target = tot
|
||||
previous_end_time = previous_cap_end_time[package.target]
|
||||
if tot > previous_end_time:
|
||||
# Can't get there exactly on time, so get there ASAP. This
|
||||
# will typically only happen for the first CAP at each
|
||||
# target.
|
||||
package.time_over_target = tot
|
||||
else:
|
||||
package.time_over_target = previous_end_time
|
||||
|
||||
departure_time = package.mission_departure_time
|
||||
# Should be impossible for CAPs
|
||||
if departure_time is None:
|
||||
logging.error(
|
||||
f"Could not determine mission end time for {package}")
|
||||
continue
|
||||
previous_cap_end_time[package.target] = departure_time
|
||||
else:
|
||||
# But other packages should be spread out a bit. Note that take
|
||||
# times are delayed, but all aircraft will become active at
|
||||
|
||||
@@ -13,6 +13,7 @@ from dcs.helicopters import (
|
||||
SA342L,
|
||||
SA342M,
|
||||
UH_1H,
|
||||
SH_60B
|
||||
)
|
||||
from dcs.planes import (
|
||||
AJS37,
|
||||
@@ -63,6 +64,7 @@ from dcs.planes import (
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
RQ_1A_Predator,
|
||||
S_3B,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
Su_17M4,
|
||||
@@ -92,443 +94,278 @@ from pydcs_extensions.f22a.f22a import F_22A
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
|
||||
# All aircraft lists are in priority order. Aircraft higher in the list will be
|
||||
# preferred over those lower in the list.
|
||||
|
||||
# TODO: These lists really ought to be era (faction) dependent.
|
||||
# Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but
|
||||
# factions that also have F-4s should not.
|
||||
|
||||
# Interceptor are the aircraft prioritized for interception tasks
|
||||
# If none is available, the AI will use regular CAP-capable aircraft instead
|
||||
INTERCEPT_CAPABLE = [
|
||||
MiG_21Bis,
|
||||
MiG_25PD,
|
||||
MiG_31,
|
||||
MiG_29S,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29K,
|
||||
JF_17,
|
||||
J_11A,
|
||||
Su_27,
|
||||
Su_30,
|
||||
Su_33,
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
Rafale_M,
|
||||
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
|
||||
]
|
||||
|
||||
# Used for CAP, Escort, and intercept if there is not a specialised aircraft available
|
||||
CAP_CAPABLE = [
|
||||
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
MiG_25PD,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29S,
|
||||
Su_57,
|
||||
F_22A,
|
||||
MiG_31,
|
||||
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
MiG_25PD,
|
||||
Rafale_M,
|
||||
Su_33,
|
||||
Su_30,
|
||||
Su_27,
|
||||
J_11A,
|
||||
JF_17,
|
||||
Su_30,
|
||||
Su_33,
|
||||
Su_57,
|
||||
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_15E,
|
||||
F_16A,
|
||||
MiG_29S,
|
||||
MiG_29K,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
F_22A,
|
||||
|
||||
F_15E,
|
||||
F_16A,
|
||||
F_4E,
|
||||
JF_17,
|
||||
MiG_23MLD,
|
||||
MiG_21Bis,
|
||||
Mirage_2000_5,
|
||||
M_2000C,
|
||||
F_5E_3,
|
||||
MiG_19P,
|
||||
A_4E_C,
|
||||
F_86F_Sabre,
|
||||
MiG_15bis,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
|
||||
I_16,
|
||||
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_M,
|
||||
]
|
||||
|
||||
CAP_PREFERRED = [
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29S,
|
||||
|
||||
Su_27,
|
||||
J_11A,
|
||||
JF_17,
|
||||
Su_30,
|
||||
Su_33,
|
||||
Su_57,
|
||||
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_16C_50,
|
||||
F_22A,
|
||||
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
I_16,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
|
||||
Rafale_M,
|
||||
]
|
||||
|
||||
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
|
||||
CAS_CAPABLE = [
|
||||
|
||||
MiG_15bis,
|
||||
MiG_29A,
|
||||
MiG_27K,
|
||||
MiG_29S,
|
||||
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_24MR,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_30,
|
||||
Su_34,
|
||||
|
||||
JF_17,
|
||||
|
||||
M_2000C,
|
||||
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_5E_3,
|
||||
|
||||
A_10C,
|
||||
B_1B,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
Su_25TM,
|
||||
Su_25T,
|
||||
Su_25,
|
||||
F_15E,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
F_15E,
|
||||
F_22A,
|
||||
|
||||
Tornado_IDS,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
Tornado_GR4,
|
||||
|
||||
Tornado_IDS,
|
||||
JF_17,
|
||||
A_10A,
|
||||
A_4E_C,
|
||||
AJS37,
|
||||
Su_24MR,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
AV8BNA,
|
||||
S_3B,
|
||||
Su_34,
|
||||
Su_30,
|
||||
MiG_29S,
|
||||
MiG_27K,
|
||||
MiG_29A,
|
||||
AH_64D,
|
||||
AH_64A,
|
||||
AH_1W,
|
||||
OH_58D,
|
||||
SA342M,
|
||||
SA342L,
|
||||
Ka_50,
|
||||
Mi_28N,
|
||||
Mi_24V,
|
||||
Mi_8MT,
|
||||
UH_1H,
|
||||
MiG_15bis,
|
||||
M_2000C,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
MB_339PAN,
|
||||
L_39ZA,
|
||||
AJS37,
|
||||
|
||||
SA342M,
|
||||
SA342L,
|
||||
OH_58D,
|
||||
|
||||
AH_64A,
|
||||
AH_64D,
|
||||
AH_1W,
|
||||
|
||||
UH_1H,
|
||||
|
||||
Mi_8MT,
|
||||
Mi_28N,
|
||||
Mi_24V,
|
||||
Ka_50,
|
||||
|
||||
A_20G,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
A_20G,
|
||||
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
I_16,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
|
||||
WingLoong_I,
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator
|
||||
RQ_1A_Predator,
|
||||
]
|
||||
|
||||
CAS_PREFERRED = [
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_24MR,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_30,
|
||||
Su_34,
|
||||
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
|
||||
Tornado_GR4,
|
||||
|
||||
C_101CC,
|
||||
MB_339PAN,
|
||||
L_39ZA,
|
||||
AJS37,
|
||||
|
||||
SA342M,
|
||||
SA342L,
|
||||
OH_58D,
|
||||
|
||||
AH_64A,
|
||||
AH_64D,
|
||||
AH_1W,
|
||||
|
||||
Mi_28N,
|
||||
Mi_24V,
|
||||
Ka_50,
|
||||
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
A_20G,
|
||||
I_16,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
|
||||
WingLoong_I,
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator
|
||||
]
|
||||
|
||||
# Aircraft used for SEAD / DEAD tasks
|
||||
# Aircraft used for SEAD tasks
|
||||
SEAD_CAPABLE = [
|
||||
F_4E,
|
||||
FA_18C_hornet,
|
||||
|
||||
F_16C_50,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
|
||||
Su_24M,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_17M4,
|
||||
Su_30,
|
||||
Su_34,
|
||||
MiG_27K,
|
||||
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S,
|
||||
Rafale_B
|
||||
]
|
||||
|
||||
SEAD_PREFERRED = [
|
||||
F_4E,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Tornado_IDS,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
Su_30,
|
||||
Su_34,
|
||||
Tornado_IDS,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
F_4E,
|
||||
A_4E_C,
|
||||
AV8BNA,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
Su_34,
|
||||
Su_30,
|
||||
MiG_27K,
|
||||
Tornado_GR4,
|
||||
F_117A,
|
||||
B_17G,
|
||||
A_20G,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
]
|
||||
|
||||
|
||||
# Aircraft used for DEAD tasks
|
||||
DEAD_CAPABLE = [
|
||||
AJS37,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
B_1B,
|
||||
B_52H,
|
||||
Tu_160,
|
||||
Tu_95MS,
|
||||
] + SEAD_CAPABLE
|
||||
|
||||
|
||||
# Aircraft used for Strike mission
|
||||
STRIKE_CAPABLE = [
|
||||
MiG_15bis,
|
||||
MiG_21Bis,
|
||||
MiG_27K,
|
||||
MB_339PAN,
|
||||
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_24MR,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_27,
|
||||
Su_33,
|
||||
Su_30,
|
||||
Su_34,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29K,
|
||||
MiG_29S,
|
||||
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
|
||||
JF_17,
|
||||
|
||||
M_2000C,
|
||||
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_5E_3,
|
||||
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15E,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
|
||||
F_117A,
|
||||
B_1B,
|
||||
B_52H,
|
||||
F_117A,
|
||||
|
||||
Tornado_IDS,
|
||||
Tu_160,
|
||||
Tu_95MS,
|
||||
Tu_22M3,
|
||||
F_15E,
|
||||
AJS37,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
Tornado_GR4,
|
||||
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
F_16A,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
Tornado_IDS,
|
||||
Su_17M4,
|
||||
Su_24MR,
|
||||
Su_24M,
|
||||
Su_25TM,
|
||||
Su_25T,
|
||||
Su_25,
|
||||
Su_34,
|
||||
Su_33,
|
||||
Su_30,
|
||||
Su_27,
|
||||
MiG_29S,
|
||||
MiG_29K,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
JF_17,
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
AV8BNA,
|
||||
S_3B,
|
||||
A_4E_C,
|
||||
M_2000C,
|
||||
MiG_27K,
|
||||
MiG_21Bis,
|
||||
MiG_15bis,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
MB_339PAN,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
AJS37,
|
||||
|
||||
B_17G,
|
||||
A_20G,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
A_20G,
|
||||
B_17G,
|
||||
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S,
|
||||
Rafale_B
|
||||
|
||||
]
|
||||
|
||||
STRIKE_PREFERRED = [
|
||||
AJS37,
|
||||
A_20G,
|
||||
B_17G,
|
||||
B_1B,
|
||||
B_52H,
|
||||
F_117A,
|
||||
F_15E,
|
||||
Su_24M,
|
||||
Su_30,
|
||||
Su_34,
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
]
|
||||
|
||||
ANTISHIP_CAPABLE = [
|
||||
AJS37,
|
||||
C_101CC,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
FA_18C_hornet,
|
||||
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
|
||||
Su_30,
|
||||
Su_34,
|
||||
Tu_22M3,
|
||||
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
|
||||
Ju_88A4,
|
||||
Rafale_A_S,
|
||||
Rafale_B
|
||||
]
|
||||
|
||||
ANTISHIP_PREFERRED = [
|
||||
AJS37,
|
||||
C_101CC,
|
||||
FA_18C_hornet,
|
||||
JF_17,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
Su_24M,
|
||||
Su_30,
|
||||
Su_34,
|
||||
Tu_22M3,
|
||||
Ju_88A4
|
||||
]
|
||||
|
||||
RUNWAY_ATTACK_PREFERRED = [
|
||||
Su_17M4,
|
||||
JF_17,
|
||||
Su_30,
|
||||
Su_34,
|
||||
Su_30,
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
AV8BNA,
|
||||
S_3B,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
C_101CC,
|
||||
SH_60B,
|
||||
]
|
||||
|
||||
RUNWAY_ATTACK_CAPABLE = STRIKE_CAPABLE
|
||||
|
||||
# Duplicates some list entries but that's fine.
|
||||
RUNWAY_ATTACK_CAPABLE = [
|
||||
JF_17,
|
||||
Su_34,
|
||||
Su_30,
|
||||
Tornado_IDS,
|
||||
] + STRIKE_CAPABLE
|
||||
|
||||
# For any aircraft that isn't necessarily directly involved in strike
|
||||
# missions in a direct combat sense, but can transport objects and infantry.
|
||||
TRANSPORT_CAPABLE = [
|
||||
Hercules,
|
||||
Mi_8MT,
|
||||
UH_1H,
|
||||
]
|
||||
|
||||
DRONES = [
|
||||
MQ_9_Reaper,
|
||||
@@ -537,31 +374,7 @@ DRONES = [
|
||||
]
|
||||
|
||||
|
||||
def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
|
||||
if task in cap_missions:
|
||||
return CAP_PREFERRED
|
||||
elif task == FlightType.ANTISHIP:
|
||||
return ANTISHIP_PREFERRED
|
||||
elif task == FlightType.BAI:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.CAS:
|
||||
return CAS_PREFERRED
|
||||
elif task in (FlightType.DEAD, FlightType.SEAD):
|
||||
return SEAD_PREFERRED
|
||||
elif task == FlightType.OCA_AIRCRAFT:
|
||||
return CAS_PREFERRED
|
||||
elif task == FlightType.OCA_RUNWAY:
|
||||
return RUNWAY_ATTACK_PREFERRED
|
||||
elif task == FlightType.STRIKE:
|
||||
return STRIKE_PREFERRED
|
||||
elif task == FlightType.ESCORT:
|
||||
return CAP_PREFERRED
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
|
||||
if task in cap_missions:
|
||||
return CAP_CAPABLE
|
||||
@@ -571,8 +384,10 @@ def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.CAS:
|
||||
return CAS_CAPABLE
|
||||
elif task in (FlightType.DEAD, FlightType.SEAD):
|
||||
elif task == FlightType.SEAD:
|
||||
return SEAD_CAPABLE
|
||||
elif task == FlightType.DEAD:
|
||||
return DEAD_CAPABLE
|
||||
elif task == FlightType.OCA_AIRCRAFT:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.OCA_RUNWAY:
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"""Objective adjacency lists."""
|
||||
from typing import Dict, Iterator, List, Optional
|
||||
from __future__ import annotations
|
||||
|
||||
from game.theater import ConflictTheater, ControlPoint, MissionTarget
|
||||
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING
|
||||
|
||||
from game.utils import Distance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater, ControlPoint, MissionTarget
|
||||
|
||||
|
||||
class ClosestAirfields:
|
||||
@@ -10,18 +15,25 @@ class ClosestAirfields:
|
||||
def __init__(self, target: MissionTarget,
|
||||
all_control_points: List[ControlPoint]) -> None:
|
||||
self.target = target
|
||||
# This cache is configured once on load, so it's important that it is
|
||||
# complete and deterministic to avoid different behaviors across loads.
|
||||
# E.g. https://github.com/Khopa/dcs_liberation/issues/819
|
||||
self.closest_airfields: List[ControlPoint] = sorted(
|
||||
all_control_points, key=lambda c: self.target.distance_to(c)
|
||||
)
|
||||
|
||||
def airfields_within(self, meters: int) -> Iterator[ControlPoint]:
|
||||
@property
|
||||
def operational_airfields(self) -> Iterator[ControlPoint]:
|
||||
return (c for c in self.closest_airfields if c.runway_is_operational())
|
||||
|
||||
def airfields_within(self, distance: Distance) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all airfields within the given range of the target.
|
||||
|
||||
Note that this iterates over *all* airfields, not just friendly
|
||||
airfields.
|
||||
"""
|
||||
for cp in self.closest_airfields:
|
||||
if cp.distance_to(self.target) < meters:
|
||||
if cp.distance_to(self.target) < distance.meters:
|
||||
yield cp
|
||||
else:
|
||||
break
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING, Type
|
||||
@@ -9,7 +10,9 @@ from dcs.point import MovingPoint, PointAction
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from game.data.weapons import Weapon
|
||||
from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||
from game.utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.ato import Package
|
||||
@@ -67,7 +70,7 @@ class FlightWaypointType(Enum):
|
||||
class FlightWaypoint:
|
||||
|
||||
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
|
||||
alt: int = 0) -> None:
|
||||
alt: Distance = meters(0)) -> None:
|
||||
"""Creates a flight waypoint.
|
||||
|
||||
Args:
|
||||
@@ -83,6 +86,9 @@ class FlightWaypoint:
|
||||
self.alt = alt
|
||||
self.alt_type = "BARO"
|
||||
self.name = ""
|
||||
# TODO: Merge with pretty_name.
|
||||
# Only used in the waypoint list in the flight edit page. No sense
|
||||
# having three names. A short and long form is enough.
|
||||
self.description = ""
|
||||
self.targets: List[MissionTarget] = []
|
||||
self.obj_name = ""
|
||||
@@ -105,7 +111,7 @@ class FlightWaypoint:
|
||||
def from_pydcs(cls, point: MovingPoint,
|
||||
from_cp: ControlPoint) -> "FlightWaypoint":
|
||||
waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x,
|
||||
point.position.y, point.alt)
|
||||
point.position.y, meters(point.alt))
|
||||
waypoint.alt_type = point.alt_type
|
||||
# Other actions exist... but none of them *should* be the first
|
||||
# waypoint for a flight.
|
||||
@@ -130,11 +136,13 @@ class FlightWaypoint:
|
||||
|
||||
class Flight:
|
||||
|
||||
def __init__(self, package: Package, unit_type: Type[FlyingType],
|
||||
def __init__(self, package: Package, country: str, unit_type: Type[FlyingType],
|
||||
count: int, flight_type: FlightType, start_type: str,
|
||||
departure: ControlPoint, arrival: ControlPoint,
|
||||
divert: Optional[ControlPoint]) -> None:
|
||||
divert: Optional[ControlPoint],
|
||||
custom_name: Optional[str] = None) -> None:
|
||||
self.package = package
|
||||
self.country = country
|
||||
self.unit_type = unit_type
|
||||
self.count = count
|
||||
self.departure = departure
|
||||
@@ -143,10 +151,11 @@ class Flight:
|
||||
self.flight_type = flight_type
|
||||
# TODO: Replace with FlightPlan.
|
||||
self.targets: List[MissionTarget] = []
|
||||
self.loadout: Dict[str, str] = {}
|
||||
self.loadout: Dict[int, Optional[Weapon]] = {}
|
||||
self.start_type = start_type
|
||||
self.use_custom_loadout = False
|
||||
self.client_count = 0
|
||||
self.custom_name = custom_name
|
||||
|
||||
# Will be replaced with a more appropriate FlightPlan by
|
||||
# FlightPlanBuilder, but an empty flight plan the flight begins with an
|
||||
@@ -168,4 +177,12 @@ class Flight:
|
||||
|
||||
def __repr__(self):
|
||||
name = db.unit_type_name(self.unit_type)
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {name}"
|
||||
return f"[{self.flight_type}] {self.count} x {name}"
|
||||
|
||||
def __str__(self):
|
||||
name = db.unit_get_expanded_info(self.country, self.unit_type, 'name')
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {name}"
|
||||
return f"[{self.flight_type}] {self.count} x {name}"
|
||||
|
||||
@@ -17,6 +17,7 @@ from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from shapely.geometry import Point as ShapelyPoint
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater import (
|
||||
@@ -28,12 +29,12 @@ from game.theater import (
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.theater.theatergroundobject import EwrGroundObject
|
||||
from game.utils import nm_to_meter, meter_to_nm
|
||||
from game.utils import Distance, Speed, meters, nautical_miles
|
||||
from .closestairfields import ObjectiveDistanceCache
|
||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||
from .traveltime import GroundSpeed, TravelTime
|
||||
from .waypointbuilder import StrikeTarget, WaypointBuilder
|
||||
from ..conflictgen import Conflict
|
||||
from ..conflictgen import Conflict, FRONTLINE_LENGTH
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -73,13 +74,20 @@ class FlightPlan:
|
||||
"""Iterates over all waypoints in the flight plan, in order."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def edges(self) -> Iterator[Tuple[FlightWaypoint, FlightWaypoint]]:
|
||||
def edges(
|
||||
self, until: Optional[FlightWaypoint] = None
|
||||
) -> Iterator[Tuple[FlightWaypoint, FlightWaypoint]]:
|
||||
"""A list of all paths between waypoints, in order."""
|
||||
return zip(self.waypoints, self.waypoints[1:])
|
||||
waypoints = self.waypoints
|
||||
if until is None:
|
||||
last_index = len(waypoints)
|
||||
else:
|
||||
last_index = waypoints.index(until) + 1
|
||||
|
||||
return zip(self.waypoints[:last_index], self.waypoints[1:last_index])
|
||||
|
||||
def best_speed_between_waypoints(self, a: FlightWaypoint,
|
||||
b: FlightWaypoint) -> int:
|
||||
b: FlightWaypoint) -> Speed:
|
||||
"""Desired ground speed between points a and b."""
|
||||
factor = 1.0
|
||||
if b.waypoint_type == FlightWaypointType.ASCEND_POINT:
|
||||
@@ -98,11 +106,10 @@ class FlightPlan:
|
||||
# We don't have an exact heightmap, but we should probably be performing
|
||||
# *some* adjustment for NTTR since the minimum altitude of the map is
|
||||
# near 2000 ft MSL.
|
||||
return int(
|
||||
GroundSpeed.for_flight(self.flight, min(a.alt, b.alt)) * factor)
|
||||
return GroundSpeed.for_flight(self.flight, min(a.alt, b.alt)) * factor
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint,
|
||||
b: FlightWaypoint) -> int:
|
||||
b: FlightWaypoint) -> Speed:
|
||||
return self.best_speed_between_waypoints(a, b)
|
||||
|
||||
@property
|
||||
@@ -119,16 +126,17 @@ class FlightPlan:
|
||||
def bingo_fuel(self) -> int:
|
||||
"""Bingo fuel value for the FlightPlan
|
||||
"""
|
||||
distance_to_arrival = meter_to_nm(self.max_distance_from(self.flight.arrival))
|
||||
distance_to_arrival = self.max_distance_from(self.flight.arrival)
|
||||
|
||||
bingo = 1000 # Minimum Emergency Fuel
|
||||
bingo += 500 # Visual Traffic
|
||||
bingo += 15 * distance_to_arrival
|
||||
bingo = 1000.0 # Minimum Emergency Fuel
|
||||
bingo += 500 # Visual Traffic
|
||||
bingo += 15 * distance_to_arrival.nautical_miles
|
||||
|
||||
# TODO: Per aircraft tweaks.
|
||||
|
||||
if self.flight.divert is not None:
|
||||
bingo += 10 * meter_to_nm(self.max_distance_from(self.flight.divert))
|
||||
max_divert_distance = self.max_distance_from(self.flight.divert)
|
||||
bingo += 10 * max_divert_distance.nautical_miles
|
||||
|
||||
return round(bingo / 100) * 100
|
||||
|
||||
@@ -137,15 +145,15 @@ class FlightPlan:
|
||||
"""Joker fuel value for the FlightPlan
|
||||
"""
|
||||
return self.bingo_fuel + 1000
|
||||
|
||||
|
||||
def max_distance_from(self, cp: ControlPoint) -> int:
|
||||
def max_distance_from(self, cp: ControlPoint) -> Distance:
|
||||
"""Returns the farthest waypoint of the flight plan from a ControlPoint.
|
||||
:arg cp The ControlPoint to measure distance from.
|
||||
"""
|
||||
if not self.waypoints:
|
||||
return 0
|
||||
return max([cp.position.distance_to_point(w.position) for w in self.waypoints])
|
||||
return meters(0)
|
||||
return max([meters(cp.position.distance_to_point(w.position)) for w in
|
||||
self.waypoints])
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
@@ -156,26 +164,18 @@ class FlightPlan:
|
||||
"""
|
||||
return timedelta()
|
||||
|
||||
# Not cached because changes to the package might alter the formation speed.
|
||||
@property
|
||||
def travel_time_to_target(self) -> Optional[timedelta]:
|
||||
"""The estimated time between the first waypoint and the target."""
|
||||
if self.tot_waypoint is None:
|
||||
return None
|
||||
return self._travel_time_to_waypoint(self.tot_waypoint)
|
||||
|
||||
def _travel_time_to_waypoint(
|
||||
self, destination: FlightWaypoint) -> timedelta:
|
||||
total = timedelta()
|
||||
for previous_waypoint, waypoint in self.edges:
|
||||
total += self.travel_time_between_waypoints(previous_waypoint,
|
||||
waypoint)
|
||||
if waypoint == destination:
|
||||
break
|
||||
else:
|
||||
|
||||
if destination not in self.waypoints:
|
||||
raise PlanningError(
|
||||
f"Did not find destination waypoint {destination} in "
|
||||
f"waypoints for {self.flight}")
|
||||
|
||||
for previous_waypoint, waypoint in self.edges(until=destination):
|
||||
total += self.travel_time_between_waypoints(previous_waypoint,
|
||||
waypoint)
|
||||
return total
|
||||
|
||||
def travel_time_between_waypoints(self, a: FlightWaypoint,
|
||||
@@ -196,10 +196,64 @@ class FlightPlan:
|
||||
def dismiss_escort_at(self) -> Optional[FlightWaypoint]:
|
||||
return None
|
||||
|
||||
def takeoff_time(self) -> Optional[timedelta]:
|
||||
tot_waypoint = self.tot_waypoint
|
||||
if tot_waypoint is None:
|
||||
return None
|
||||
|
||||
time = self.tot_for_waypoint(tot_waypoint)
|
||||
if time is None:
|
||||
return None
|
||||
time += self.tot_offset
|
||||
return time - self._travel_time_to_waypoint(tot_waypoint)
|
||||
|
||||
def startup_time(self) -> Optional[timedelta]:
|
||||
takeoff_time = self.takeoff_time()
|
||||
if takeoff_time is None:
|
||||
return None
|
||||
|
||||
start_time = (takeoff_time - self.estimate_startup() -
|
||||
self.estimate_ground_ops())
|
||||
|
||||
# In case FP math has given us some barely below zero time, round to
|
||||
# zero.
|
||||
if math.isclose(start_time.total_seconds(), 0):
|
||||
return timedelta()
|
||||
|
||||
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
|
||||
# and they're not interesting from a mission planning perspective so we
|
||||
# don't want them in the UI.
|
||||
#
|
||||
# Round down so *barely* above zero start times are just zero.
|
||||
return timedelta(seconds=math.floor(start_time.total_seconds()))
|
||||
|
||||
def estimate_startup(self) -> timedelta:
|
||||
if self.flight.start_type == "Cold":
|
||||
if self.flight.client_count:
|
||||
return timedelta(minutes=10)
|
||||
else:
|
||||
# The AI doesn't seem to have a real startup procedure.
|
||||
return timedelta(minutes=2)
|
||||
return timedelta()
|
||||
|
||||
def estimate_ground_ops(self) -> timedelta:
|
||||
if self.flight.start_type in ("Runway", "In Flight"):
|
||||
return timedelta()
|
||||
if self.flight.from_cp.is_fleet:
|
||||
return timedelta(minutes=2)
|
||||
else:
|
||||
return timedelta(minutes=5)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
"""The time that the mission is complete and the flight RTBs."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LoiterFlightPlan(FlightPlan):
|
||||
hold: FlightWaypoint
|
||||
hold_duration: timedelta
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
raise NotImplementedError
|
||||
@@ -221,6 +275,17 @@ class LoiterFlightPlan(FlightPlan):
|
||||
return self.push_time
|
||||
return None
|
||||
|
||||
def travel_time_between_waypoints(self, a: FlightWaypoint,
|
||||
b: FlightWaypoint) -> timedelta:
|
||||
travel_time = super().travel_time_between_waypoints(a, b)
|
||||
if a != self.hold:
|
||||
return travel_time
|
||||
try:
|
||||
return travel_time + self.hold_duration
|
||||
except AttributeError:
|
||||
# Save compat for 2.3.
|
||||
return travel_time + timedelta(minutes=5)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FormationFlightPlan(LoiterFlightPlan):
|
||||
@@ -245,7 +310,7 @@ class FormationFlightPlan(LoiterFlightPlan):
|
||||
return self.split
|
||||
|
||||
@cached_property
|
||||
def best_flight_formation_speed(self) -> int:
|
||||
def best_flight_formation_speed(self) -> Speed:
|
||||
"""The best speed this flight is capable at all formation waypoints.
|
||||
|
||||
To ease coordination with other flights, we aim to have a single mission
|
||||
@@ -254,14 +319,14 @@ class FormationFlightPlan(LoiterFlightPlan):
|
||||
all of its formation waypoints.
|
||||
"""
|
||||
speeds = []
|
||||
for previous_waypoint, waypoint in self.edges:
|
||||
for previous_waypoint, waypoint in self.edges():
|
||||
if waypoint in self.package_speed_waypoints:
|
||||
speeds.append(self.best_speed_between_waypoints(
|
||||
previous_waypoint, waypoint))
|
||||
return min(speeds)
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint,
|
||||
b: FlightWaypoint) -> int:
|
||||
b: FlightWaypoint) -> Speed:
|
||||
if b in self.package_speed_waypoints:
|
||||
# Should be impossible, as any package with at least one
|
||||
# FormationFlightPlan flight needs a formation speed.
|
||||
@@ -297,15 +362,27 @@ class FormationFlightPlan(LoiterFlightPlan):
|
||||
GroundSpeed.for_flight(self.flight, self.hold.alt)
|
||||
)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.split_time
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PatrollingFlightPlan(FlightPlan):
|
||||
nav_to: List[FlightWaypoint]
|
||||
nav_from: List[FlightWaypoint]
|
||||
patrol_start: FlightWaypoint
|
||||
patrol_end: FlightWaypoint
|
||||
|
||||
#: Maximum time to remain on station.
|
||||
patrol_duration: timedelta
|
||||
|
||||
#: The engagement range of any Search Then Engage task, or the radius of a
|
||||
#: Search Then Engage in Zone task. Any enemies of the appropriate type for
|
||||
#: this mission within this range of the flight's current position (or the
|
||||
#: center of the zone) will be engaged by the flight.
|
||||
engagement_distance: Distance
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
@@ -339,6 +416,10 @@ class PatrollingFlightPlan(FlightPlan):
|
||||
def tot_waypoint(self) -> Optional[FlightWaypoint]:
|
||||
return self.patrol_start
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.patrol_end_time
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BarCapFlightPlan(PatrollingFlightPlan):
|
||||
@@ -347,12 +428,14 @@ class BarCapFlightPlan(PatrollingFlightPlan):
|
||||
divert: Optional[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.takeoff
|
||||
yield from self.nav_to
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.patrol_start,
|
||||
self.patrol_end,
|
||||
self.land,
|
||||
]
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
@@ -365,13 +448,15 @@ class CasFlightPlan(PatrollingFlightPlan):
|
||||
divert: Optional[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.takeoff
|
||||
yield from self.nav_to
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.patrol_start,
|
||||
self.target,
|
||||
self.patrol_end,
|
||||
self.land,
|
||||
]
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
@@ -390,12 +475,14 @@ class TarCapFlightPlan(PatrollingFlightPlan):
|
||||
lead_time: timedelta
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.takeoff
|
||||
yield from self.nav_to
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.patrol_start,
|
||||
self.patrol_end,
|
||||
self.land,
|
||||
]
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
@@ -428,27 +515,27 @@ class TarCapFlightPlan(PatrollingFlightPlan):
|
||||
class StrikeFlightPlan(FormationFlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
hold: FlightWaypoint
|
||||
nav_to: List[FlightWaypoint]
|
||||
join: FlightWaypoint
|
||||
ingress: FlightWaypoint
|
||||
targets: List[FlightWaypoint]
|
||||
egress: FlightWaypoint
|
||||
split: FlightWaypoint
|
||||
nav_from: List[FlightWaypoint]
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.hold,
|
||||
self.join,
|
||||
self.ingress
|
||||
]
|
||||
yield self.takeoff
|
||||
yield self.hold
|
||||
yield from self.nav_to
|
||||
yield self.join
|
||||
yield self.ingress
|
||||
yield from self.targets
|
||||
yield from [
|
||||
self.egress,
|
||||
self.split,
|
||||
self.land,
|
||||
]
|
||||
yield self.egress
|
||||
yield self.split
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
@@ -461,7 +548,7 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
} | set(self.targets)
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint,
|
||||
b: FlightWaypoint) -> int:
|
||||
b: FlightWaypoint) -> Speed:
|
||||
# FlightWaypoint is only comparable by identity, so adding
|
||||
# target_area_waypoint to package_speed_waypoints is useless.
|
||||
if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC:
|
||||
@@ -479,14 +566,15 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
def target_area_waypoint(self) -> FlightWaypoint:
|
||||
return FlightWaypoint(FlightWaypointType.TARGET_GROUP_LOC,
|
||||
self.package.target.position.x,
|
||||
self.package.target.position.y, 0)
|
||||
self.package.target.position.y,
|
||||
meters(0))
|
||||
|
||||
@property
|
||||
def travel_time_to_target(self) -> timedelta:
|
||||
"""The estimated time between the first waypoint and the target."""
|
||||
destination = self.tot_waypoint
|
||||
total = timedelta()
|
||||
for previous_waypoint, waypoint in self.edges:
|
||||
for previous_waypoint, waypoint in self.edges():
|
||||
if waypoint == self.tot_waypoint:
|
||||
# For anything strike-like the TOT waypoint is the *flight's*
|
||||
# mission target, but to synchronize with the rest of the
|
||||
@@ -504,7 +592,7 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
return total
|
||||
|
||||
@property
|
||||
def mission_speed(self) -> int:
|
||||
def mission_speed(self) -> Speed:
|
||||
return GroundSpeed.for_flight(self.flight, self.ingress.alt)
|
||||
|
||||
@property
|
||||
@@ -546,20 +634,22 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
@dataclass(frozen=True)
|
||||
class SweepFlightPlan(LoiterFlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
nav_to: List[FlightWaypoint]
|
||||
sweep_start: FlightWaypoint
|
||||
sweep_end: FlightWaypoint
|
||||
nav_from: List[FlightWaypoint]
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
lead_time: timedelta
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.hold,
|
||||
self.sweep_start,
|
||||
self.sweep_end,
|
||||
self.land,
|
||||
]
|
||||
yield self.takeoff
|
||||
yield self.hold
|
||||
yield from self.nav_to
|
||||
yield self.sweep_start
|
||||
yield self.sweep_end
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
@@ -602,6 +692,9 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
GroundSpeed.for_flight(self.flight, self.hold.alt)
|
||||
)
|
||||
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.sweep_end_time
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CustomFlightPlan(FlightPlan):
|
||||
@@ -632,6 +725,10 @@ class CustomFlightPlan(FlightPlan):
|
||||
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
|
||||
|
||||
class FlightPlanBuilder:
|
||||
"""Generates flight plans for flights."""
|
||||
@@ -652,6 +749,7 @@ class FlightPlanBuilder:
|
||||
else:
|
||||
faction = self.game.enemy_faction
|
||||
self.doctrine: Doctrine = faction.doctrine
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
def populate_flight_plan(
|
||||
self, flight: Flight,
|
||||
@@ -697,12 +795,79 @@ class FlightPlanBuilder:
|
||||
f"{task} flight plan generation not implemented")
|
||||
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
ingress_point = self._ingress_point()
|
||||
egress_point = self._egress_point()
|
||||
# The simple case is where the target is greater than the ingress
|
||||
# distance into the threat zone and the target is not near the departure
|
||||
# airfield. In this case, we can plan the shortest route from the
|
||||
# departure airfield to the target, use the last non-threatened point as
|
||||
# the join point, and plan the IP inside the threatened area.
|
||||
#
|
||||
# When the target is near the edge of the threat zone the IP may need to
|
||||
# be placed outside the zone.
|
||||
#
|
||||
# +--------------+ +---------------+
|
||||
# | | | |
|
||||
# | | IP---+-T |
|
||||
# | | | |
|
||||
# | | | |
|
||||
# +--------------+ +---------------+
|
||||
#
|
||||
# Here we want to place the IP first and route the flight to the IP
|
||||
# rather than routing to the target and placing the IP based on the join
|
||||
# point.
|
||||
#
|
||||
# The other case that we need to handle is when the target is close to
|
||||
# the origin airfield. In this case we also need to set up the IP first,
|
||||
# but depending on the placement of the IP we may need to place the join
|
||||
# point in a retreating position.
|
||||
#
|
||||
# A messy (and very unlikely) case that we can't do much about:
|
||||
#
|
||||
# +--------------+ +---------------+
|
||||
# | | | |
|
||||
# | IP-+---+-T |
|
||||
# | | | |
|
||||
# | | | |
|
||||
# +--------------+ +---------------+
|
||||
from gen.ato import PackageWaypoints
|
||||
target = self.package.target.position
|
||||
|
||||
join_point = self.preferred_join_point()
|
||||
if join_point is None:
|
||||
# The whole path from the origin airfield to the target is
|
||||
# threatened. Need to retreat out of the threat area.
|
||||
join_point = self.retreat_point(self.package_airfield().position)
|
||||
|
||||
attack_heading = join_point.heading_between_point(target)
|
||||
ingress_point = self._ingress_point(attack_heading)
|
||||
join_distance = meters(join_point.distance_to_point(target))
|
||||
ingress_distance = meters(ingress_point.distance_to_point(target))
|
||||
if join_distance < ingress_distance:
|
||||
# The second case described above. The ingress point is farther from
|
||||
# the target than the join point. Use the fallback behavior for now.
|
||||
self.legacy_package_waypoints_impl()
|
||||
return
|
||||
|
||||
# The first case described above. The ingress and join points are placed
|
||||
# reasonably relative to each other.
|
||||
egress_point = self._egress_point(attack_heading)
|
||||
self.package.waypoints = PackageWaypoints(
|
||||
WaypointBuilder.perturb(join_point),
|
||||
ingress_point,
|
||||
egress_point,
|
||||
WaypointBuilder.perturb(join_point),
|
||||
)
|
||||
|
||||
def retreat_point(self, origin: Point) -> Point:
|
||||
return self.threat_zones.closest_boundary(origin)
|
||||
|
||||
def legacy_package_waypoints_impl(self) -> None:
|
||||
from gen.ato import PackageWaypoints
|
||||
ingress_point = self._ingress_point(
|
||||
self._target_heading_to_package_airfield())
|
||||
egress_point = self._egress_point(
|
||||
self._target_heading_to_package_airfield())
|
||||
join_point = self._rendezvous_point(ingress_point)
|
||||
split_point = self._rendezvous_point(egress_point)
|
||||
|
||||
from gen.ato import PackageWaypoints
|
||||
self.package.waypoints = PackageWaypoints(
|
||||
join_point,
|
||||
ingress_point,
|
||||
@@ -710,6 +875,14 @@ class FlightPlanBuilder:
|
||||
split_point,
|
||||
)
|
||||
|
||||
def preferred_join_point(self) -> Optional[Point]:
|
||||
path = self.game.navmesh_for(self.is_player).shortest_path(
|
||||
self.package_airfield().position, self.package.target.position)
|
||||
for point in reversed(path):
|
||||
if not self.threat_zones.threatened(point):
|
||||
return point
|
||||
return None
|
||||
|
||||
def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
|
||||
"""Generates a strike flight plan.
|
||||
|
||||
@@ -804,20 +977,25 @@ class FlightPlanBuilder:
|
||||
if isinstance(location, FrontLine):
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
|
||||
start, end = self.racetrack_for_objective(location)
|
||||
patrol_alt = random.randint(
|
||||
self.doctrine.min_patrol_altitude,
|
||||
self.doctrine.max_patrol_altitude
|
||||
)
|
||||
start, end = self.racetrack_for_objective(location, barcap=True)
|
||||
patrol_alt = meters(random.randint(
|
||||
int(self.doctrine.min_patrol_altitude.meters),
|
||||
int(self.doctrine.max_patrol_altitude.meters)
|
||||
))
|
||||
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
start, end = builder.race_track(start, end, patrol_alt)
|
||||
|
||||
return BarCapFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
patrol_duration=self.doctrine.cap_duration,
|
||||
engagement_distance=self.doctrine.cap_engagement_range,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(flight.departure.position, start.position,
|
||||
patrol_alt),
|
||||
nav_from=builder.nav_path(end.position, flight.arrival.position,
|
||||
patrol_alt),
|
||||
patrol_start=start,
|
||||
patrol_end=end,
|
||||
land=builder.land(flight.arrival),
|
||||
@@ -830,32 +1008,40 @@ class FlightPlanBuilder:
|
||||
Args:
|
||||
flight: The flight to generate the flight plan for.
|
||||
"""
|
||||
assert self.package.waypoints is not None
|
||||
target = self.package.target.position
|
||||
|
||||
heading = self._heading_to_package_airfield(target)
|
||||
heading = self.package.waypoints.join.heading_between_point(target)
|
||||
start = target.point_from_heading(heading,
|
||||
-self.doctrine.sweep_distance)
|
||||
-self.doctrine.sweep_distance.meters)
|
||||
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
start, end = builder.sweep(start, target,
|
||||
self.doctrine.ingress_altitude)
|
||||
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
|
||||
return SweepFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
lead_time=timedelta(minutes=5),
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
hold=builder.hold(self._hold_point(flight)),
|
||||
hold=hold,
|
||||
hold_duration=timedelta(minutes=5),
|
||||
nav_to=builder.nav_path(hold.position, start.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
nav_from=builder.nav_path(end.position, flight.arrival.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
sweep_start=start,
|
||||
sweep_end=end,
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert)
|
||||
)
|
||||
|
||||
def racetrack_for_objective(self,
|
||||
location: MissionTarget) -> Tuple[Point, Point]:
|
||||
def racetrack_for_objective(self, location: MissionTarget,
|
||||
barcap: bool) -> Tuple[Point, Point]:
|
||||
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
|
||||
for airfield in closest_cache.closest_airfields:
|
||||
for airfield in closest_cache.operational_airfields:
|
||||
# If the mission is a BARCAP of an enemy airfield, find the *next*
|
||||
# closest enemy airfield.
|
||||
if airfield == self.package.target:
|
||||
@@ -870,11 +1056,28 @@ class FlightPlanBuilder:
|
||||
closest_airfield.position
|
||||
)
|
||||
|
||||
min_distance_from_enemy = nm_to_meter(20)
|
||||
distance_to_airfield = int(closest_airfield.position.distance_to_point(
|
||||
self.package.target.position
|
||||
))
|
||||
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
|
||||
position = ShapelyPoint(self.package.target.position.x,
|
||||
self.package.target.position.y)
|
||||
|
||||
if barcap:
|
||||
# BARCAPs should remain far enough back from the enemy that their
|
||||
# commit range does not enter the enemy's threat zone. Include a 5nm
|
||||
# buffer.
|
||||
distance_to_no_fly = meters(
|
||||
position.distance(self.threat_zones.all)
|
||||
) - self.doctrine.cap_engagement_range - nautical_miles(5)
|
||||
else:
|
||||
# Other race tracks (TARCAPs, currently) just try to keep some
|
||||
# distance from the nearest enemy airbase, but since they are by
|
||||
# definition in enemy territory they can't avoid the threat zone
|
||||
# without being useless.
|
||||
min_distance_from_enemy = nautical_miles(20)
|
||||
distance_to_airfield = meters(
|
||||
closest_airfield.position.distance_to_point(
|
||||
self.package.target.position
|
||||
))
|
||||
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
|
||||
|
||||
min_cap_distance = min(self.doctrine.cap_min_distance_from_cp,
|
||||
distance_to_no_fly)
|
||||
max_cap_distance = min(self.doctrine.cap_max_distance_from_cp,
|
||||
@@ -882,16 +1085,17 @@ class FlightPlanBuilder:
|
||||
|
||||
end = location.position.point_from_heading(
|
||||
heading,
|
||||
random.randint(min_cap_distance, max_cap_distance)
|
||||
random.randint(int(min_cap_distance.meters),
|
||||
int(max_cap_distance.meters))
|
||||
)
|
||||
diameter = random.randint(
|
||||
self.doctrine.cap_min_track_length,
|
||||
self.doctrine.cap_max_track_length
|
||||
int(self.doctrine.cap_min_track_length.meters),
|
||||
int(self.doctrine.cap_max_track_length.meters)
|
||||
)
|
||||
start = end.point_from_heading(heading - 180, diameter)
|
||||
return start, end
|
||||
|
||||
def racetrack_for_frontline(self,
|
||||
def racetrack_for_frontline(self, origin: Point,
|
||||
front_line: FrontLine) -> Tuple[Point, Point]:
|
||||
ally_cp, enemy_cp = front_line.control_points
|
||||
|
||||
@@ -901,7 +1105,8 @@ class FlightPlanBuilder:
|
||||
)
|
||||
center = ingress.point_from_heading(heading, distance / 2)
|
||||
orbit_center = center.point_from_heading(
|
||||
heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15))
|
||||
heading - 90, random.randint(int(nautical_miles(6).meters),
|
||||
int(nautical_miles(15).meters))
|
||||
)
|
||||
|
||||
combat_width = distance / 2
|
||||
@@ -911,10 +1116,12 @@ class FlightPlanBuilder:
|
||||
combat_width = 35000
|
||||
|
||||
radius = combat_width * 1.25
|
||||
orbit0p = orbit_center.point_from_heading(heading, radius)
|
||||
orbit1p = orbit_center.point_from_heading(heading + 180, radius)
|
||||
start = orbit_center.point_from_heading(heading, radius)
|
||||
end = orbit_center.point_from_heading(heading + 180, radius)
|
||||
|
||||
return orbit0p, orbit1p
|
||||
if end.distance_to_point(origin) < start.distance_to_point(origin):
|
||||
start, end = end, start
|
||||
return start, end
|
||||
|
||||
def generate_tarcap(self, flight: Flight) -> TarCapFlightPlan:
|
||||
"""Generate a CAP flight plan for the given front line.
|
||||
@@ -924,16 +1131,19 @@ class FlightPlanBuilder:
|
||||
"""
|
||||
location = self.package.target
|
||||
|
||||
patrol_alt = random.randint(self.doctrine.min_patrol_altitude,
|
||||
self.doctrine.max_patrol_altitude)
|
||||
patrol_alt = meters(
|
||||
random.randint(int(self.doctrine.min_patrol_altitude.meters),
|
||||
int(self.doctrine.max_patrol_altitude.meters)))
|
||||
|
||||
# Create points
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
|
||||
if isinstance(location, FrontLine):
|
||||
orbit0p, orbit1p = self.racetrack_for_frontline(location)
|
||||
orbit0p, orbit1p = self.racetrack_for_frontline(
|
||||
flight.departure.position, location)
|
||||
else:
|
||||
orbit0p, orbit1p = self.racetrack_for_objective(location)
|
||||
orbit0p, orbit1p = self.racetrack_for_objective(location,
|
||||
barcap=False)
|
||||
|
||||
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
|
||||
return TarCapFlightPlan(
|
||||
@@ -945,7 +1155,12 @@ class FlightPlanBuilder:
|
||||
# requests an escort the CAP flight will remain on station for the
|
||||
# duration of the escorted mission, or until it is winchester/bingo.
|
||||
patrol_duration=self.doctrine.cap_duration,
|
||||
engagement_distance=self.doctrine.cap_engagement_range,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(flight.departure.position, orbit0p,
|
||||
patrol_alt),
|
||||
nav_from=builder.nav_path(orbit1p, flight.arrival.position,
|
||||
patrol_alt),
|
||||
patrol_start=start,
|
||||
patrol_end=end,
|
||||
land=builder.land(flight.arrival),
|
||||
@@ -1040,21 +1255,29 @@ class FlightPlanBuilder:
|
||||
def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
ingress, target, egress = builder.escort(
|
||||
self.package.waypoints.ingress, self.package.target,
|
||||
self.package.waypoints.egress)
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
|
||||
return StrikeFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
hold=builder.hold(self._hold_point(flight)),
|
||||
join=builder.join(self.package.waypoints.join),
|
||||
hold=hold,
|
||||
hold_duration=timedelta(minutes=5),
|
||||
nav_to=builder.nav_path(hold.position, join.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
join=join,
|
||||
ingress=ingress,
|
||||
targets=[target],
|
||||
egress=egress,
|
||||
split=builder.split(self.package.waypoints.split),
|
||||
split=split,
|
||||
nav_from=builder.nav_path(split.position, flight.arrival.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert)
|
||||
)
|
||||
@@ -1077,15 +1300,25 @@ class FlightPlanBuilder:
|
||||
center = ingress.point_from_heading(heading, distance / 2)
|
||||
egress = ingress.point_from_heading(heading, distance)
|
||||
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
ingress_distance = ingress.distance_to_point(flight.departure.position)
|
||||
egress_distance = egress.distance_to_point(flight.departure.position)
|
||||
if egress_distance < ingress_distance:
|
||||
ingress, egress = egress, ingress
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
|
||||
return CasFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
patrol_duration=self.doctrine.cas_duration,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(flight.departure.position, ingress,
|
||||
self.doctrine.ingress_altitude),
|
||||
nav_from=builder.nav_path(egress, flight.arrival.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
patrol_start=builder.ingress(FlightWaypointType.INGRESS_CAS,
|
||||
ingress, location),
|
||||
engagement_distance=meters(FRONTLINE_LENGTH) / 2,
|
||||
target=builder.cas(center),
|
||||
patrol_end=builder.egress(egress, location),
|
||||
land=builder.land(flight.arrival),
|
||||
@@ -1128,12 +1361,13 @@ class FlightPlanBuilder:
|
||||
# point, plan the hold point such that it retreats from the origin
|
||||
# airfield.
|
||||
return join.point_from_heading(target.heading_between_point(origin),
|
||||
self.doctrine.push_distance)
|
||||
self.doctrine.push_distance.meters)
|
||||
|
||||
heading_to_join = origin.heading_between_point(join)
|
||||
hold_point = origin.point_from_heading(heading_to_join,
|
||||
self.doctrine.push_distance)
|
||||
if hold_point.distance_to_point(join) >= self.doctrine.push_distance:
|
||||
hold_point = origin.point_from_heading(
|
||||
heading_to_join, self.doctrine.push_distance.meters)
|
||||
hold_distance = meters(hold_point.distance_to_point(join))
|
||||
if hold_distance >= self.doctrine.push_distance:
|
||||
# Hold point is between the origin airfield and the join point and
|
||||
# spaced sufficiently.
|
||||
return hold_point
|
||||
@@ -1145,10 +1379,10 @@ class FlightPlanBuilder:
|
||||
# properly.
|
||||
origin_to_join = origin.distance_to_point(join)
|
||||
cos_theta = (
|
||||
(self.doctrine.hold_distance ** 2 +
|
||||
(self.doctrine.hold_distance.meters ** 2 +
|
||||
origin_to_join ** 2 -
|
||||
self.doctrine.join_distance ** 2) /
|
||||
(2 * self.doctrine.hold_distance * origin_to_join)
|
||||
self.doctrine.join_distance.meters ** 2) /
|
||||
(2 * self.doctrine.hold_distance.meters * origin_to_join)
|
||||
)
|
||||
try:
|
||||
theta = math.acos(cos_theta)
|
||||
@@ -1157,10 +1391,10 @@ class FlightPlanBuilder:
|
||||
# hold point away from the target.
|
||||
return origin.point_from_heading(
|
||||
target.heading_between_point(origin),
|
||||
self.doctrine.hold_distance)
|
||||
self.doctrine.hold_distance.meters)
|
||||
|
||||
return origin.point_from_heading(heading_to_join - theta,
|
||||
self.doctrine.hold_distance)
|
||||
self.doctrine.hold_distance.meters)
|
||||
|
||||
# TODO: Make a model for the waypoint builder and use that in the UI.
|
||||
def generate_rtb_waypoint(self, flight: Flight,
|
||||
@@ -1171,7 +1405,7 @@ class FlightPlanBuilder:
|
||||
flight: The flight to generate the landing waypoint for.
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
return builder.land(arrival)
|
||||
|
||||
def strike_flightplan(
|
||||
@@ -1179,8 +1413,7 @@ class FlightPlanBuilder:
|
||||
ingress_type: FlightWaypointType,
|
||||
targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan:
|
||||
assert self.package.waypoints is not None
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine,
|
||||
targets)
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player, targets)
|
||||
|
||||
target_waypoints: List[FlightWaypoint] = []
|
||||
if targets is not None:
|
||||
@@ -1191,17 +1424,26 @@ class FlightPlanBuilder:
|
||||
target_waypoints.append(
|
||||
self.target_area_waypoint(flight, location, builder))
|
||||
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
|
||||
return StrikeFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
hold=builder.hold(self._hold_point(flight)),
|
||||
join=builder.join(self.package.waypoints.join),
|
||||
hold=hold,
|
||||
hold_duration=timedelta(minutes=5),
|
||||
nav_to=builder.nav_path(hold.position, join.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
join=join,
|
||||
ingress=builder.ingress(ingress_type,
|
||||
self.package.waypoints.ingress, location),
|
||||
targets=target_waypoints,
|
||||
egress=builder.egress(self.package.waypoints.egress, location),
|
||||
split=builder.split(self.package.waypoints.split),
|
||||
split=split,
|
||||
nav_from=builder.nav_path(split.position, flight.arrival.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert)
|
||||
)
|
||||
@@ -1211,13 +1453,13 @@ class FlightPlanBuilder:
|
||||
return attack_transition.point_from_heading(
|
||||
self.package.target.position.heading_between_point(
|
||||
self.package_airfield().position),
|
||||
self.doctrine.join_distance)
|
||||
self.doctrine.join_distance.meters)
|
||||
|
||||
def _advancing_rendezvous_point(self, attack_transition: Point) -> Point:
|
||||
"""Creates a rendezvous point that advances toward the target."""
|
||||
heading = self._heading_to_package_airfield(attack_transition)
|
||||
return attack_transition.point_from_heading(
|
||||
heading, -self.doctrine.join_distance)
|
||||
heading, -self.doctrine.join_distance.meters)
|
||||
|
||||
def _rendezvous_should_retreat(self, attack_transition: Point) -> bool:
|
||||
transition_target_distance = attack_transition.distance_to_point(
|
||||
@@ -1242,16 +1484,14 @@ class FlightPlanBuilder:
|
||||
return self._retreating_rendezvous_point(attack_transition)
|
||||
return self._advancing_rendezvous_point(attack_transition)
|
||||
|
||||
def _ingress_point(self) -> Point:
|
||||
heading = self._target_heading_to_package_airfield()
|
||||
def _ingress_point(self, heading: int) -> Point:
|
||||
return self.package.target.position.point_from_heading(
|
||||
heading - 180 + 25, self.doctrine.ingress_egress_distance
|
||||
heading - 180 + 15, self.doctrine.ingress_egress_distance.meters
|
||||
)
|
||||
|
||||
def _egress_point(self) -> Point:
|
||||
heading = self._target_heading_to_package_airfield()
|
||||
def _egress_point(self, heading: int) -> Point:
|
||||
return self.package.target.position.point_from_heading(
|
||||
heading - 180 - 25, self.doctrine.ingress_egress_distance
|
||||
heading - 180 - 15, self.doctrine.ingress_egress_distance.meters
|
||||
)
|
||||
|
||||
def _target_heading_to_package_airfield(self) -> int:
|
||||
@@ -1277,7 +1517,7 @@ class FlightPlanBuilder:
|
||||
cache = ObjectiveDistanceCache.get_closest_airfields(
|
||||
self.package.target
|
||||
)
|
||||
for airfield in cache.closest_airfields:
|
||||
for airfield in cache.operational_airfields:
|
||||
for flight in self.package.flights:
|
||||
if flight.departure == airfield:
|
||||
return airfield
|
||||
|
||||
@@ -3,12 +3,19 @@ from __future__ import annotations
|
||||
import logging
|
||||
import math
|
||||
from datetime import timedelta
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.utils import meter_to_nm
|
||||
from game.utils import (
|
||||
Distance,
|
||||
SPEED_OF_SOUND_AT_SEA_LEVEL,
|
||||
Speed,
|
||||
kph,
|
||||
mach,
|
||||
meters,
|
||||
)
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -18,7 +25,7 @@ if TYPE_CHECKING:
|
||||
class GroundSpeed:
|
||||
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, altitude: int) -> int:
|
||||
def for_flight(cls, flight: Flight, altitude: Distance) -> Speed:
|
||||
if not issubclass(flight.unit_type, FlyingType):
|
||||
raise TypeError("Flight has non-flying unit")
|
||||
|
||||
@@ -27,130 +34,50 @@ class GroundSpeed:
|
||||
# on fuel, but mission speed will be fast enough to keep the flight
|
||||
# safer.
|
||||
|
||||
c_sound_sea_level = 661.5
|
||||
|
||||
# DCS's max speed is in kph at 0 MSL. Convert to knots.
|
||||
max_speed = flight.unit_type.max_speed * 0.539957
|
||||
if max_speed > c_sound_sea_level:
|
||||
# DCS's max speed is in kph at 0 MSL.
|
||||
max_speed = kph(flight.unit_type.max_speed)
|
||||
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
|
||||
# Aircraft is supersonic. Limit to mach 0.8 to conserve fuel and
|
||||
# account for heavily loaded jets.
|
||||
return int(cls.from_mach(0.8, altitude))
|
||||
return mach(0.8, altitude)
|
||||
|
||||
# For subsonic aircraft, assume the aircraft can reasonably perform at
|
||||
# 80% of its maximum, and that it can maintain the same mach at altitude
|
||||
# as it can at sea level. This probably isn't great assumption, but
|
||||
# might. be sufficient given the wiggle room. We can come up with
|
||||
# another heuristic if needed.
|
||||
mach = max_speed * 0.8 / c_sound_sea_level
|
||||
return int(cls.from_mach(mach, altitude)) # knots
|
||||
|
||||
@staticmethod
|
||||
def from_mach(mach: float, altitude_m: int) -> float:
|
||||
"""Returns the ground speed in knots for the given mach and altitude.
|
||||
|
||||
Args:
|
||||
mach: The mach number to convert to ground speed.
|
||||
altitude_m: The altitude in meters.
|
||||
|
||||
Returns:
|
||||
The ground speed corresponding to the given altitude and mach number
|
||||
in knots.
|
||||
"""
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
|
||||
altitude_ft = altitude_m * 3.28084
|
||||
if altitude_ft <= 36152:
|
||||
temperature_f = 59 - 0.00356 * altitude_ft
|
||||
else:
|
||||
# There's another formula for altitudes over 82k feet, but we better
|
||||
# not be planning waypoints that high...
|
||||
temperature_f = -70
|
||||
|
||||
temperature_k = (temperature_f + 459.67) * (5 / 9)
|
||||
|
||||
# https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html
|
||||
# Dependent on temperature, but varies very little (+/-0.001)
|
||||
# between -40F and 180F.
|
||||
heat_capacity_ratio = 1.4
|
||||
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html
|
||||
gas_constant = 286 # m^2/s^2/K
|
||||
c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k)
|
||||
# c_sound is in m/s, convert to knots.
|
||||
return (c_sound * 1.944) * mach
|
||||
cruise_mach = max_speed.mach() * 0.8
|
||||
return mach(cruise_mach, altitude)
|
||||
|
||||
|
||||
class TravelTime:
|
||||
@staticmethod
|
||||
def between_points(a: Point, b: Point, speed: float) -> timedelta:
|
||||
def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
|
||||
error_factor = 1.1
|
||||
distance = meter_to_nm(a.distance_to_point(b))
|
||||
return timedelta(hours=distance / speed * error_factor)
|
||||
distance = meters(a.distance_to_point(b))
|
||||
return timedelta(
|
||||
hours=distance.nautical_miles / speed.knots * error_factor)
|
||||
|
||||
|
||||
# TODO: Most if not all of this should move into FlightPlan.
|
||||
class TotEstimator:
|
||||
# An extra five minutes given as wiggle room. Expected to be spent at the
|
||||
# hold point performing any last minute configuration.
|
||||
HOLD_TIME = timedelta(minutes=5)
|
||||
|
||||
def __init__(self, package: Package) -> None:
|
||||
self.package = package
|
||||
|
||||
def mission_start_time(self, flight: Flight) -> timedelta:
|
||||
takeoff_time = self.takeoff_time_for_flight(flight)
|
||||
if takeoff_time is None:
|
||||
@staticmethod
|
||||
def mission_start_time(flight: Flight) -> timedelta:
|
||||
startup_time = flight.flight_plan.startup_time()
|
||||
if startup_time is None:
|
||||
# Could not determine takeoff time, probably due to a custom flight
|
||||
# plan. Start immediately.
|
||||
return timedelta()
|
||||
|
||||
startup_time = self.estimate_startup(flight)
|
||||
ground_ops_time = self.estimate_ground_ops(flight)
|
||||
start_time = takeoff_time - startup_time - ground_ops_time
|
||||
# In case FP math has given us some barely below zero time, round to
|
||||
# zero.
|
||||
if math.isclose(start_time.total_seconds(), 0):
|
||||
return timedelta()
|
||||
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
|
||||
# and they're not interesting from a mission planning perspective so we
|
||||
# don't want them in the UI.
|
||||
#
|
||||
# Round down so *barely* above zero start times are just zero.
|
||||
return timedelta(seconds=math.floor(start_time.total_seconds()))
|
||||
|
||||
def takeoff_time_for_flight(self, flight: Flight) -> Optional[timedelta]:
|
||||
travel_time = self.travel_time_to_rendezvous_or_target(flight)
|
||||
if travel_time is None:
|
||||
from gen.flights.flightplan import CustomFlightPlan
|
||||
if not isinstance(flight.flight_plan, CustomFlightPlan):
|
||||
logging.warning(
|
||||
"Found no rendezvous or target point. Cannot estimate "
|
||||
f"takeoff time takeoff time for {flight}.")
|
||||
return None
|
||||
|
||||
from gen.flights.flightplan import FormationFlightPlan
|
||||
if isinstance(flight.flight_plan, FormationFlightPlan):
|
||||
tot = flight.flight_plan.tot_for_waypoint(
|
||||
flight.flight_plan.join)
|
||||
if tot is None:
|
||||
logging.warning(
|
||||
"Could not determine the TOT of the join point. Takeoff "
|
||||
f"time for {flight} will be immediate.")
|
||||
return None
|
||||
else:
|
||||
tot_waypoint = flight.flight_plan.tot_waypoint
|
||||
if tot_waypoint is None:
|
||||
tot = self.package.time_over_target
|
||||
else:
|
||||
tot = flight.flight_plan.tot_for_waypoint(tot_waypoint)
|
||||
if tot is None:
|
||||
logging.error(f"TOT waypoint for {flight} has no TOT")
|
||||
tot = self.package.time_over_target
|
||||
return tot - travel_time - self.HOLD_TIME
|
||||
return startup_time
|
||||
|
||||
def earliest_tot(self) -> timedelta:
|
||||
earliest_tot = max((
|
||||
self.earliest_tot_for_flight(f) for f in self.package.flights
|
||||
)) + self.HOLD_TIME
|
||||
))
|
||||
|
||||
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
|
||||
# and they're not interesting from a mission planning perspective so we
|
||||
@@ -159,7 +86,8 @@ class TotEstimator:
|
||||
# Round up so we don't get negative start times.
|
||||
return timedelta(seconds=math.ceil(earliest_tot.total_seconds()))
|
||||
|
||||
def earliest_tot_for_flight(self, flight: Flight) -> timedelta:
|
||||
@staticmethod
|
||||
def earliest_tot_for_flight(flight: Flight) -> timedelta:
|
||||
"""Estimate fastest time from mission start to the target position.
|
||||
|
||||
For BARCAP flights, this is time to race track start. This ensures that
|
||||
@@ -175,51 +103,18 @@ class TotEstimator:
|
||||
The earliest possible TOT for the given flight in seconds. Returns 0
|
||||
if an ingress point cannot be found.
|
||||
"""
|
||||
time_to_target = self.travel_time_to_target(flight)
|
||||
if time_to_target is None:
|
||||
# Clear the TOT, calculate the startup time. Negating the result gives
|
||||
# the earliest possible start time.
|
||||
orig_tot = flight.package.time_over_target
|
||||
try:
|
||||
flight.package.time_over_target = timedelta()
|
||||
time = flight.flight_plan.startup_time()
|
||||
finally:
|
||||
flight.package.time_over_target = orig_tot
|
||||
|
||||
if time is None:
|
||||
logging.warning(f"Cannot estimate TOT for {flight}")
|
||||
# Return 0 so this flight's travel time does not affect the rest
|
||||
# of the package.
|
||||
return timedelta()
|
||||
# Account for TOT offsets for the flight plan. An offset of -2 minutes
|
||||
# means the flight's TOT is 2 minutes ahead of the package's so it needs
|
||||
# an extra two minutes.
|
||||
offset = -flight.flight_plan.tot_offset
|
||||
startup = self.estimate_startup(flight)
|
||||
ground_ops = self.estimate_ground_ops(flight)
|
||||
return startup + ground_ops + time_to_target + offset
|
||||
|
||||
@staticmethod
|
||||
def estimate_startup(flight: Flight) -> timedelta:
|
||||
if flight.start_type == "Cold":
|
||||
if flight.client_count:
|
||||
return timedelta(minutes=10)
|
||||
else:
|
||||
# The AI doesn't seem to have a real startup procedure.
|
||||
return timedelta(minutes=2)
|
||||
return timedelta()
|
||||
|
||||
@staticmethod
|
||||
def estimate_ground_ops(flight: Flight) -> timedelta:
|
||||
if flight.start_type in ("Runway", "In Flight"):
|
||||
return timedelta()
|
||||
if flight.from_cp.is_fleet:
|
||||
return timedelta(minutes=2)
|
||||
else:
|
||||
return timedelta(minutes=5)
|
||||
|
||||
@staticmethod
|
||||
def travel_time_to_target(flight: Flight) -> Optional[timedelta]:
|
||||
if flight.flight_plan is None:
|
||||
return None
|
||||
return flight.flight_plan.travel_time_to_target
|
||||
|
||||
@staticmethod
|
||||
def travel_time_to_rendezvous_or_target(
|
||||
flight: Flight) -> Optional[timedelta]:
|
||||
if flight.flight_plan is None:
|
||||
return None
|
||||
from gen.flights.flightplan import FormationFlightPlan
|
||||
if isinstance(flight.flight_plan, FormationFlightPlan):
|
||||
return flight.flight_plan.travel_time_to_rendezvous
|
||||
return flight.flight_plan.travel_time_to_target
|
||||
return -time
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import (
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
MissionTarget,
|
||||
OffMapSpawn,
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.weather import Conditions
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from .flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
|
||||
|
||||
@@ -25,12 +36,13 @@ class StrikeTarget:
|
||||
|
||||
|
||||
class WaypointBuilder:
|
||||
def __init__(self, conditions: Conditions, flight: Flight,
|
||||
doctrine: Doctrine,
|
||||
def __init__(self, flight: Flight, game: Game, player: bool,
|
||||
targets: Optional[List[StrikeTarget]] = None) -> None:
|
||||
self.conditions = conditions
|
||||
self.flight = flight
|
||||
self.doctrine = doctrine
|
||||
self.conditions = game.conditions
|
||||
self.doctrine = game.faction_for(player).doctrine
|
||||
self.threat_zones = game.threat_zone_for(not player)
|
||||
self.navmesh = game.navmesh_for(player)
|
||||
self.targets = targets
|
||||
|
||||
@property
|
||||
@@ -53,7 +65,9 @@ class WaypointBuilder:
|
||||
FlightWaypointType.NAV,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
)
|
||||
waypoint.name = "NAV"
|
||||
waypoint.alt_type = "BARO"
|
||||
@@ -64,7 +78,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.TAKEOFF,
|
||||
position.x,
|
||||
position.y,
|
||||
0
|
||||
meters(0)
|
||||
)
|
||||
waypoint.name = "TAKEOFF"
|
||||
waypoint.alt_type = "RADIO"
|
||||
@@ -84,7 +98,9 @@ class WaypointBuilder:
|
||||
FlightWaypointType.NAV,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
)
|
||||
waypoint.name = "NAV"
|
||||
waypoint.alt_type = "BARO"
|
||||
@@ -95,7 +111,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.LANDING_POINT,
|
||||
position.x,
|
||||
position.y,
|
||||
0
|
||||
meters(0)
|
||||
)
|
||||
waypoint.name = "LANDING"
|
||||
waypoint.alt_type = "RADIO"
|
||||
@@ -116,12 +132,12 @@ class WaypointBuilder:
|
||||
position = divert.position
|
||||
if isinstance(divert, OffMapSpawn):
|
||||
if self.is_helo:
|
||||
altitude = 500
|
||||
altitude = meters(500)
|
||||
else:
|
||||
altitude = self.doctrine.rendezvous_altitude
|
||||
altitude_type = "BARO"
|
||||
else:
|
||||
altitude = 0
|
||||
altitude = meters(0)
|
||||
altitude_type = "RADIO"
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
@@ -142,7 +158,9 @@ class WaypointBuilder:
|
||||
FlightWaypointType.LOITER,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
)
|
||||
waypoint.pretty_name = "Hold"
|
||||
waypoint.description = "Wait until push time"
|
||||
@@ -154,7 +172,9 @@ class WaypointBuilder:
|
||||
FlightWaypointType.JOIN,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "Join"
|
||||
waypoint.description = "Rendezvous with package"
|
||||
@@ -166,7 +186,9 @@ class WaypointBuilder:
|
||||
FlightWaypointType.SPLIT,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "Split"
|
||||
waypoint.description = "Depart from package"
|
||||
@@ -179,7 +201,9 @@ class WaypointBuilder:
|
||||
ingress_type,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "INGRESS on " + objective.name
|
||||
waypoint.description = "INGRESS on " + objective.name
|
||||
@@ -193,7 +217,9 @@ class WaypointBuilder:
|
||||
FlightWaypointType.EGRESS,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "EGRESS from " + target.name
|
||||
waypoint.description = "EGRESS from " + target.name
|
||||
@@ -218,7 +244,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
target.target.position.x,
|
||||
target.target.position.y,
|
||||
0
|
||||
meters(0)
|
||||
)
|
||||
waypoint.description = description
|
||||
waypoint.pretty_name = description
|
||||
@@ -249,7 +275,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
location.position.x,
|
||||
location.position.y,
|
||||
0
|
||||
meters(0)
|
||||
)
|
||||
waypoint.description = name
|
||||
waypoint.pretty_name = name
|
||||
@@ -274,7 +300,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.CAS,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else 1000
|
||||
meters(500) if self.is_helo else meters(1000)
|
||||
)
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Provide CAS"
|
||||
@@ -283,12 +309,12 @@ class WaypointBuilder:
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def race_track_start(position: Point, altitude: int) -> FlightWaypoint:
|
||||
def race_track_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a racetrack start waypoint.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack in meters.
|
||||
altitude: Altitude of the racetrack.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
@@ -302,12 +328,12 @@ class WaypointBuilder:
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def race_track_end(position: Point, altitude: int) -> FlightWaypoint:
|
||||
def race_track_end(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a racetrack end waypoint.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack in meters.
|
||||
altitude: Altitude of the racetrack.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.PATROL,
|
||||
@@ -321,7 +347,7 @@ class WaypointBuilder:
|
||||
return waypoint
|
||||
|
||||
def race_track(self, start: Point, end: Point,
|
||||
altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates two waypoint for a racetrack orbit.
|
||||
|
||||
Args:
|
||||
@@ -333,7 +359,7 @@ class WaypointBuilder:
|
||||
self.race_track_end(end, altitude))
|
||||
|
||||
@staticmethod
|
||||
def sweep_start(position: Point, altitude: int) -> FlightWaypoint:
|
||||
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a sweep start waypoint.
|
||||
|
||||
Args:
|
||||
@@ -352,7 +378,7 @@ class WaypointBuilder:
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def sweep_end(position: Point, altitude: int) -> FlightWaypoint:
|
||||
def sweep_end(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a sweep end waypoint.
|
||||
|
||||
Args:
|
||||
@@ -371,7 +397,7 @@ class WaypointBuilder:
|
||||
return waypoint
|
||||
|
||||
def sweep(self, start: Point, end: Point,
|
||||
altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates two waypoint for a racetrack orbit.
|
||||
|
||||
Args:
|
||||
@@ -404,7 +430,9 @@ class WaypointBuilder:
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
target.position.x,
|
||||
target.position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.name = "TARGET"
|
||||
waypoint.description = "Escort the package"
|
||||
@@ -412,3 +440,80 @@ class WaypointBuilder:
|
||||
|
||||
egress = self.egress(egress, target)
|
||||
return ingress, waypoint, egress
|
||||
|
||||
@staticmethod
|
||||
def nav(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a navigation point.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the waypoint.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.NAV,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
)
|
||||
waypoint.name = "NAV"
|
||||
waypoint.description = "NAV"
|
||||
waypoint.pretty_name = "Nav"
|
||||
return waypoint
|
||||
|
||||
def nav_path(self, a: Point, b: Point,
|
||||
altitude: Distance) -> List[FlightWaypoint]:
|
||||
path = self.clean_nav_points(self.navmesh.shortest_path(a, b))
|
||||
return [self.nav(self.perturb(p), altitude) for p in path]
|
||||
|
||||
def clean_nav_points(self, points: Iterable[Point]) -> Iterator[Point]:
|
||||
# Examine a sliding window of three waypoints. `current` is the waypoint
|
||||
# being checked for prunability. `previous` is the last emitted waypoint
|
||||
# before `current`. `nxt` is the waypoint after `current`.
|
||||
previous: Optional[Point] = None
|
||||
current: Optional[Point] = None
|
||||
for nxt in points:
|
||||
if current is None:
|
||||
current = nxt
|
||||
continue
|
||||
if previous is None:
|
||||
previous = current
|
||||
current = nxt
|
||||
continue
|
||||
|
||||
if self.nav_point_prunable(previous, current, nxt):
|
||||
current = nxt
|
||||
continue
|
||||
|
||||
yield current
|
||||
previous = current
|
||||
current = nxt
|
||||
|
||||
def nav_point_prunable(self, previous: Point, current: Point,
|
||||
nxt: Point) -> bool:
|
||||
previous_threatened = self.threat_zones.path_threatened(previous,
|
||||
current)
|
||||
next_threatened = self.threat_zones.path_threatened(current, nxt)
|
||||
pruned_threatened = self.threat_zones.path_threatened(previous, nxt)
|
||||
previous_distance = meters(previous.distance_to_point(current))
|
||||
distance = meters(current.distance_to_point(nxt))
|
||||
distance_without = previous_distance + distance
|
||||
if distance > distance_without:
|
||||
# Don't prune paths to make them longer.
|
||||
return False
|
||||
|
||||
# We could shorten the path by removing the intermediate
|
||||
# waypoint. Do so if the new path isn't higher threat.
|
||||
if not pruned_threatened:
|
||||
# The new path is not threatened, so safe to prune.
|
||||
return True
|
||||
|
||||
# The new path is threatened. Only allow if both paths were
|
||||
# threatened anyway.
|
||||
return previous_threatened and next_threatened
|
||||
|
||||
@staticmethod
|
||||
def perturb(point: Point) -> Point:
|
||||
deviation = nautical_miles(1)
|
||||
x_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||
y_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||
return Point(point.x + x_adj, point.y + y_adj)
|
||||
|
||||
@@ -3,178 +3,14 @@ from enum import Enum
|
||||
from typing import Dict, List
|
||||
|
||||
from dcs.unittype import VehicleType
|
||||
from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
|
||||
|
||||
import pydcs_extensions.frenchpack.frenchpack as frenchpack
|
||||
from game.theater import ControlPoint
|
||||
from gen.ground_forces.ai_ground_planner_db import *
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
TYPE_TANKS = [
|
||||
Armor.MBT_T_55,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.MBT_T_80U,
|
||||
Armor.MBT_T_90,
|
||||
Armor.MBT_Leopard_2,
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.MBT_Leclerc,
|
||||
Armor.MBT_Challenger_II,
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.MBT_Merkava_Mk__4,
|
||||
Armor.ZTZ_96B,
|
||||
|
||||
# WW2
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||
Armor.MT_Pz_Kpfw_IV_Ausf_H,
|
||||
Armor.HT_Pz_Kpfw_VI_Tiger_I,
|
||||
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
|
||||
Armor.MT_M4_Sherman,
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
Armor.StuG_IV,
|
||||
Armor.CT_Centaur_IV,
|
||||
Armor.CT_Cromwell_IV,
|
||||
Armor.HIT_Churchill_VII,
|
||||
Armor.LT_Mk_VII_Tetrarch,
|
||||
|
||||
# Mods
|
||||
frenchpack.DIM__TOYOTA_BLUE,
|
||||
frenchpack.DIM__TOYOTA_GREEN,
|
||||
frenchpack.DIM__TOYOTA_DESERT,
|
||||
frenchpack.DIM__KAMIKAZE,
|
||||
|
||||
frenchpack.AMX_10RCR,
|
||||
frenchpack.AMX_10RCR_SEPAR,
|
||||
frenchpack.AMX_30B2,
|
||||
frenchpack.Leclerc_Serie_XXI,
|
||||
|
||||
]
|
||||
|
||||
TYPE_ATGM = [
|
||||
Armor.ATGM_M1045_HMMWV_TOW,
|
||||
Armor.ATGM_M1134_Stryker,
|
||||
Armor.IFV_BMP_2,
|
||||
|
||||
# WW2 (Tank Destroyers)
|
||||
Armor.M30_Cargo_Carrier,
|
||||
Armor.TD_Jagdpanzer_IV,
|
||||
Armor.TD_Jagdpanther_G1,
|
||||
Armor.TD_M10_GMC,
|
||||
|
||||
# Mods
|
||||
frenchpack.VBAE_CRAB_MMP,
|
||||
frenchpack.VAB_MEPHISTO,
|
||||
frenchpack.TRM_2000_PAMELA,
|
||||
|
||||
]
|
||||
|
||||
TYPE_IFV = [
|
||||
Armor.IFV_BMP_3,
|
||||
Armor.IFV_BMP_2,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.IFV_Marder,
|
||||
Armor.IFV_MCV_80,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.ZBD_04A,
|
||||
|
||||
# WW2
|
||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
||||
Armor.LAC_M8_Greyhound,
|
||||
Armor.Daimler_Armoured_Car,
|
||||
|
||||
# Mods
|
||||
frenchpack.ERC_90,
|
||||
frenchpack.VBAE_CRAB,
|
||||
frenchpack.VAB_T20_13
|
||||
|
||||
]
|
||||
|
||||
TYPE_APC = [
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
Armor.APC_M1126_Stryker_ICV,
|
||||
Armor.APC_M113,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.APC_MTLB,
|
||||
Armor.APC_M2A1,
|
||||
Armor.APC_Cobra,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
Armor.APC_AAV_7,
|
||||
Armor.TPz_Fuchs,
|
||||
Armor.ARV_BRDM_2,
|
||||
Armor.ARV_BTR_RD,
|
||||
Armor.FDDM_Grad,
|
||||
|
||||
# WW2
|
||||
Armor.APC_M2A1,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
|
||||
# Mods
|
||||
frenchpack.VAB__50,
|
||||
frenchpack.VBL__50,
|
||||
frenchpack.VBL_AANF1,
|
||||
|
||||
]
|
||||
|
||||
TYPE_ARTILLERY = [
|
||||
Artillery.MLRS_9A52_Smerch,
|
||||
Artillery.SPH_2S1_Gvozdika,
|
||||
Artillery.SPH_2S3_Akatsia,
|
||||
Artillery.MLRS_BM_21_Grad,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27,
|
||||
Artillery.SPH_M109_Paladin,
|
||||
Artillery.MLRS_M270,
|
||||
Artillery.SPH_2S9_Nona,
|
||||
Artillery.SpGH_Dana,
|
||||
Artillery.SPH_2S19_Msta,
|
||||
Artillery.MLRS_FDDM,
|
||||
|
||||
# WW2
|
||||
Artillery.Sturmpanzer_IV_Brummbär,
|
||||
Artillery.M12_GMC
|
||||
]
|
||||
|
||||
TYPE_LOGI = [
|
||||
Unarmed.Transport_M818,
|
||||
Unarmed.Transport_KAMAZ_43101,
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_GAZ_66,
|
||||
Unarmed.Transport_GAZ_3307,
|
||||
Unarmed.Transport_GAZ_3308,
|
||||
Unarmed.Transport_Ural_4320_31_Armored,
|
||||
Unarmed.Transport_Ural_4320T,
|
||||
Unarmed.Blitz_3_6_6700A,
|
||||
Unarmed.Kübelwagen_82,
|
||||
Unarmed.Sd_Kfz_7,
|
||||
Unarmed.Sd_Kfz_2,
|
||||
Unarmed.Willys_MB,
|
||||
Unarmed.Land_Rover_109_S3,
|
||||
Unarmed.Land_Rover_101_FC,
|
||||
|
||||
# Mods
|
||||
frenchpack.VBL,
|
||||
frenchpack.VAB,
|
||||
|
||||
]
|
||||
|
||||
TYPE_INFANTRY = [
|
||||
Infantry.Infantry_Soldier_Insurgents,
|
||||
Infantry.Soldier_AK,
|
||||
Infantry.Infantry_M1_Garand,
|
||||
Infantry.Infantry_Mauser_98,
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
Infantry.Georgian_soldier_with_M4,
|
||||
Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Soldier_M249,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_RPG,
|
||||
]
|
||||
|
||||
MAX_COMBAT_GROUP_PER_CP = 10
|
||||
|
||||
|
||||
class CombatGroupRole(Enum):
|
||||
TANK = 1
|
||||
APC = 2
|
||||
@@ -222,6 +58,7 @@ class CombatGroup:
|
||||
s += "UNITS " + self.units[0].name + " * " + str(len(self.units))
|
||||
return s
|
||||
|
||||
|
||||
class GroundPlanner:
|
||||
|
||||
def __init__(self, cp:ControlPoint, game):
|
||||
@@ -241,7 +78,6 @@ class GroundPlanner:
|
||||
self.units_per_cp[cp.id] = []
|
||||
self.reserve: List[CombatGroup] = []
|
||||
|
||||
|
||||
def plan_groundwar(self):
|
||||
|
||||
if hasattr(self.cp, 'stance'):
|
||||
@@ -273,6 +109,9 @@ class GroundPlanner:
|
||||
elif key in TYPE_ATGM:
|
||||
collection = self.atgm_group
|
||||
role = CombatGroupRole.ATGM
|
||||
elif key in TYPE_SHORAD:
|
||||
collection = self.shorad_groups
|
||||
role = CombatGroupRole.SHORAD
|
||||
else:
|
||||
print("Warning unit type not handled by ground generator")
|
||||
print(key)
|
||||
@@ -280,12 +119,16 @@ class GroundPlanner:
|
||||
|
||||
available = self.cp.base.armor[key]
|
||||
while available > 0:
|
||||
n = random.choice(group_size_choice)
|
||||
if n > available:
|
||||
if available >= 2:
|
||||
n = 2
|
||||
else:
|
||||
n = 1
|
||||
|
||||
if role == CombatGroupRole.SHORAD:
|
||||
n = 1
|
||||
else:
|
||||
n = random.choice(group_size_choice)
|
||||
if n > available:
|
||||
if available >= 2:
|
||||
n = 2
|
||||
else:
|
||||
n = 1
|
||||
available -= n
|
||||
|
||||
group = CombatGroup(role)
|
||||
|
||||
199
gen/ground_forces/ai_ground_planner_db.py
Normal file
199
gen/ground_forces/ai_ground_planner_db.py
Normal file
@@ -0,0 +1,199 @@
|
||||
from dcs.vehicles import AirDefence, Infantry, Unarmed, Artillery, Armor
|
||||
|
||||
from pydcs_extensions.frenchpack import frenchpack
|
||||
|
||||
TYPE_TANKS = [
|
||||
Armor.MBT_T_55,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.MBT_T_72B3,
|
||||
Armor.MBT_T_80U,
|
||||
Armor.MBT_T_90,
|
||||
Armor.MBT_Leopard_2,
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.MBT_Leclerc,
|
||||
Armor.MBT_Challenger_II,
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.MBT_Merkava_Mk__4,
|
||||
Armor.ZTZ_96B,
|
||||
|
||||
# WW2
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||
Armor.MT_Pz_Kpfw_IV_Ausf_H,
|
||||
Armor.HT_Pz_Kpfw_VI_Tiger_I,
|
||||
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
|
||||
Armor.MT_M4_Sherman,
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
Armor.StuG_IV,
|
||||
Armor.CT_Centaur_IV,
|
||||
Armor.CT_Cromwell_IV,
|
||||
Armor.HIT_Churchill_VII,
|
||||
Armor.LT_Mk_VII_Tetrarch,
|
||||
|
||||
# Mods
|
||||
frenchpack.DIM__TOYOTA_BLUE,
|
||||
frenchpack.DIM__TOYOTA_GREEN,
|
||||
frenchpack.DIM__TOYOTA_DESERT,
|
||||
frenchpack.DIM__KAMIKAZE,
|
||||
|
||||
frenchpack.AMX_10RCR,
|
||||
frenchpack.AMX_10RCR_SEPAR,
|
||||
frenchpack.AMX_30B2,
|
||||
frenchpack.Leclerc_Serie_XXI,
|
||||
|
||||
]
|
||||
|
||||
TYPE_ATGM = [
|
||||
Armor.ATGM_M1045_HMMWV_TOW,
|
||||
Armor.ATGM_M1134_Stryker,
|
||||
Armor.IFV_BMP_2,
|
||||
|
||||
# WW2 (Tank Destroyers)
|
||||
Armor.M30_Cargo_Carrier,
|
||||
Armor.TD_Jagdpanzer_IV,
|
||||
Armor.TD_Jagdpanther_G1,
|
||||
Armor.TD_M10_GMC,
|
||||
|
||||
# Mods
|
||||
frenchpack.VBAE_CRAB_MMP,
|
||||
frenchpack.VAB_MEPHISTO,
|
||||
frenchpack.TRM_2000_PAMELA,
|
||||
|
||||
]
|
||||
|
||||
TYPE_IFV = [
|
||||
Armor.IFV_BMP_3,
|
||||
Armor.IFV_BMP_2,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.IFV_Marder,
|
||||
Armor.IFV_MCV_80,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.SPG_M1128_Stryker_MGS,
|
||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.ZBD_04A,
|
||||
|
||||
# WW2
|
||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
||||
Armor.LAC_M8_Greyhound,
|
||||
Armor.Daimler_Armoured_Car,
|
||||
|
||||
# Mods
|
||||
frenchpack.ERC_90,
|
||||
frenchpack.VBAE_CRAB,
|
||||
frenchpack.VAB_T20_13
|
||||
|
||||
]
|
||||
|
||||
TYPE_APC = [
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
Armor.APC_M1126_Stryker_ICV,
|
||||
Armor.APC_M113,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.APC_BTR_82A,
|
||||
Armor.APC_MTLB,
|
||||
Armor.APC_M2A1,
|
||||
Armor.APC_Cobra,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
Armor.APC_AAV_7,
|
||||
Armor.TPz_Fuchs,
|
||||
Armor.ARV_BRDM_2,
|
||||
Armor.ARV_BTR_RD,
|
||||
Armor.FDDM_Grad,
|
||||
|
||||
# WW2
|
||||
Armor.APC_M2A1,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
|
||||
# Mods
|
||||
frenchpack.VAB__50,
|
||||
frenchpack.VBL__50,
|
||||
frenchpack.VBL_AANF1,
|
||||
|
||||
]
|
||||
|
||||
TYPE_ARTILLERY = [
|
||||
Artillery.MLRS_9A52_Smerch,
|
||||
Artillery.SPH_2S1_Gvozdika,
|
||||
Artillery.SPH_2S3_Akatsia,
|
||||
Artillery.MLRS_BM_21_Grad,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27,
|
||||
Artillery.SPH_M109_Paladin,
|
||||
Artillery.MLRS_M270,
|
||||
Artillery.SPH_2S9_Nona,
|
||||
Artillery.SpGH_Dana,
|
||||
Artillery.SPH_2S19_Msta,
|
||||
Artillery.MLRS_FDDM,
|
||||
|
||||
# WW2
|
||||
Artillery.Sturmpanzer_IV_Brummbär,
|
||||
Artillery.M12_GMC
|
||||
]
|
||||
|
||||
TYPE_LOGI = [
|
||||
Unarmed.Transport_M818,
|
||||
Unarmed.Transport_KAMAZ_43101,
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_GAZ_66,
|
||||
Unarmed.Transport_GAZ_3307,
|
||||
Unarmed.Transport_GAZ_3308,
|
||||
Unarmed.Transport_Ural_4320_31_Armored,
|
||||
Unarmed.Transport_Ural_4320T,
|
||||
Unarmed.Blitz_3_6_6700A,
|
||||
Unarmed.Kübelwagen_82,
|
||||
Unarmed.Sd_Kfz_7,
|
||||
Unarmed.Sd_Kfz_2,
|
||||
Unarmed.Willys_MB,
|
||||
Unarmed.Land_Rover_109_S3,
|
||||
Unarmed.Land_Rover_101_FC,
|
||||
|
||||
# Mods
|
||||
frenchpack.VBL,
|
||||
frenchpack.VAB,
|
||||
|
||||
]
|
||||
|
||||
TYPE_INFANTRY = [
|
||||
Infantry.Infantry_Soldier_Insurgents,
|
||||
Infantry.Soldier_AK,
|
||||
Infantry.Infantry_M1_Garand,
|
||||
Infantry.Infantry_Mauser_98,
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
Infantry.Georgian_soldier_with_M4,
|
||||
Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Soldier_M249,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_RPG,
|
||||
]
|
||||
|
||||
TYPE_SHORAD = [
|
||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
|
||||
AirDefence.AAA_ZSU_57_2,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
AirDefence.SAM_SA_8_Osa_9A33,
|
||||
AirDefence.SAM_SA_9_Strela_1_9P31,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
|
||||
AirDefence.SAM_SA_15_Tor_9A331,
|
||||
AirDefence.SAM_SA_19_Tunguska_2S6,
|
||||
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
AirDefence.SAM_Linebacker_M6,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
AirDefence.AAA_8_8cm_Flak_37,
|
||||
AirDefence.AAA_8_8cm_Flak_41,
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
AirDefence.AAA_M1_37mm,
|
||||
AirDefence.AA_gun_QF_3_7,
|
||||
|
||||
]
|
||||
@@ -9,20 +9,21 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type
|
||||
from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List
|
||||
|
||||
from dcs import Mission
|
||||
from dcs import Mission, Point
|
||||
from dcs.country import Country
|
||||
from dcs.statics import fortification_map, warehouse_map
|
||||
from dcs.task import (
|
||||
ActivateBeaconCommand,
|
||||
ActivateICLSCommand,
|
||||
EPLRS,
|
||||
OptAlarmState,
|
||||
OptAlarmState, FireAtPoint,
|
||||
)
|
||||
from dcs.unit import Ship, Unit, Vehicle
|
||||
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
|
||||
from dcs.unittype import StaticType, UnitType
|
||||
from dcs.vehicles import vehicle_map
|
||||
|
||||
from game import db
|
||||
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
|
||||
@@ -31,10 +32,10 @@ from game.theater import ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject, CarrierGroundObject,
|
||||
GenericCarrierGroundObject,
|
||||
LhaGroundObject, ShipGroundObject,
|
||||
LhaGroundObject, ShipGroundObject, MissileSiteGroundObject,
|
||||
)
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import knots_to_kph, kph_to_mps, mps_to_kph
|
||||
from game.utils import knots, mps
|
||||
from .radios import RadioFrequency, RadioRegistry
|
||||
from .runways import RunwayData
|
||||
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||
@@ -50,7 +51,7 @@ AA_CP_MIN_DISTANCE = 40000
|
||||
class GenericGroundObjectGenerator:
|
||||
"""An unspecialized ground object generator.
|
||||
|
||||
Currently used only for SAM and missile (V1/V2) sites.
|
||||
Currently used only for SAM
|
||||
"""
|
||||
def __init__(self, ground_object: TheaterGroundObject, country: Country,
|
||||
game: Game, mission: Mission, unit_map: UnitMap) -> None:
|
||||
@@ -111,6 +112,58 @@ class GenericGroundObjectGenerator:
|
||||
persistence_group, miz_group)
|
||||
|
||||
|
||||
class MissileSiteGenerator(GenericGroundObjectGenerator):
|
||||
|
||||
def generate(self) -> None:
|
||||
super(MissileSiteGenerator, self).generate()
|
||||
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
|
||||
# TODO : Should be pre-planned ?
|
||||
# TODO : Add delay to task to spread fire task over mission duration ?
|
||||
for group in self.ground_object.groups:
|
||||
vg = self.m.find_group(group.name)
|
||||
if vg is not None:
|
||||
targets = self.possible_missile_targets(vg)
|
||||
if targets:
|
||||
target = random.choice(targets)
|
||||
real_target = target.point_from_heading(random.randint(0, 360), random.randint(0, 2500))
|
||||
vg.points[0].add_task(FireAtPoint(real_target))
|
||||
logging.info("Set up fire task for missile group.")
|
||||
else:
|
||||
logging.info("Couldn't setup missile site to fire, no valid target in range.")
|
||||
else:
|
||||
logging.info("Couldn't setup missile site to fire, group was not generated.")
|
||||
|
||||
def possible_missile_targets(self, vg: Group) -> List[Point]:
|
||||
"""
|
||||
Find enemy control points in range
|
||||
:param vg: Vehicle group we are searching a target for (There is always only oe group right now)
|
||||
:return: List of possible missile targets
|
||||
"""
|
||||
targets: List[Point] = []
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.captured != self.ground_object.control_point.captured:
|
||||
distance = cp.position.distance_to_point(vg.position)
|
||||
if distance < self.missile_site_range:
|
||||
targets.append(cp.position)
|
||||
return targets
|
||||
|
||||
@property
|
||||
def missile_site_range(self) -> int:
|
||||
"""
|
||||
Get the missile site range
|
||||
:return: Missile site range
|
||||
"""
|
||||
site_range = 0
|
||||
for group in self.ground_object.groups:
|
||||
vg = self.m.find_group(group.name)
|
||||
if vg is not None:
|
||||
for u in vg.units:
|
||||
if u.type in vehicle_map:
|
||||
if vehicle_map[u.type].threat_range > site_range:
|
||||
site_range = vehicle_map[u.type].threat_range
|
||||
return site_range
|
||||
|
||||
|
||||
class BuildingSiteGenerator(GenericGroundObjectGenerator):
|
||||
"""Generator for building sites.
|
||||
|
||||
@@ -247,13 +300,13 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
wind = self.game.conditions.weather.wind.at_0m
|
||||
brc = wind.direction + 180
|
||||
# Aim for 25kts over the deck.
|
||||
carrier_speed = knots_to_kph(25) - mps_to_kph(wind.speed)
|
||||
carrier_speed = knots(25) - mps(wind.speed)
|
||||
for attempt in range(5):
|
||||
point = group.points[0].position.point_from_heading(
|
||||
brc, 100000 - attempt * 20000)
|
||||
if self.game.theater.is_in_sea(point):
|
||||
group.points[0].speed = kph_to_mps(carrier_speed)
|
||||
group.add_waypoint(point, carrier_speed)
|
||||
group.points[0].speed = carrier_speed.meters_per_second
|
||||
group.add_waypoint(point, carrier_speed.kph)
|
||||
return brc
|
||||
return None
|
||||
|
||||
@@ -421,8 +474,11 @@ class GroundObjectsGenerator:
|
||||
generator = ShipObjectGenerator(
|
||||
ground_object, country, self.game, self.m,
|
||||
self.unit_map)
|
||||
elif isinstance(ground_object, MissileSiteGroundObject):
|
||||
generator = MissileSiteGenerator(
|
||||
ground_object, country, self.game, self.m,
|
||||
self.unit_map)
|
||||
else:
|
||||
|
||||
generator = GenericGroundObjectGenerator(
|
||||
ground_object, country, self.game, self.m,
|
||||
self.unit_map)
|
||||
|
||||
@@ -33,8 +33,7 @@ from dcs.mission import Mission
|
||||
from dcs.unittype import FlyingType
|
||||
from tabulate import tabulate
|
||||
|
||||
from game.utils import meter_to_nm
|
||||
from . import units
|
||||
from game.utils import meters
|
||||
from .aircraft import AIRCRAFT_DATA, FlightData
|
||||
from .airsupportgen import AwacsInfo, TankerInfo
|
||||
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
|
||||
@@ -95,6 +94,23 @@ class KneeboardPageWriter:
|
||||
def write(self, path: Path) -> None:
|
||||
self.image.save(path)
|
||||
|
||||
@staticmethod
|
||||
def wrap_line(inputstr: str, max_length: int) -> str:
|
||||
if len(inputstr) <= max_length:
|
||||
return inputstr
|
||||
tokens = inputstr.split(" ")
|
||||
output = ""
|
||||
segments = []
|
||||
for token in tokens:
|
||||
combo = output + " " + token
|
||||
if len(combo) > max_length:
|
||||
combo = output + "\n" + token
|
||||
segments.append(combo)
|
||||
output = ""
|
||||
else:
|
||||
output = combo
|
||||
return "".join(segments + [output]).strip()
|
||||
|
||||
|
||||
class KneeboardPage:
|
||||
"""Base class for all kneeboard pages."""
|
||||
@@ -111,6 +127,9 @@ class NumberedWaypoint:
|
||||
|
||||
|
||||
class FlightPlanBuilder:
|
||||
|
||||
WAYPOINT_DESC_MAX_LEN = 25
|
||||
|
||||
def __init__(self, start_time: datetime.datetime) -> None:
|
||||
self.start_time = start_time
|
||||
self.rows: List[List[str]] = []
|
||||
@@ -152,8 +171,10 @@ class FlightPlanBuilder:
|
||||
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
|
||||
self.rows.append([
|
||||
str(waypoint.number),
|
||||
waypoint.waypoint.pretty_name,
|
||||
str(int(units.meters_to_feet(waypoint.waypoint.alt))),
|
||||
KneeboardPageWriter.wrap_line(
|
||||
waypoint.waypoint.pretty_name,
|
||||
FlightPlanBuilder.WAYPOINT_DESC_MAX_LEN),
|
||||
str(int(waypoint.waypoint.alt.feet)),
|
||||
self._waypoint_distance(waypoint.waypoint),
|
||||
self._ground_speed(waypoint.waypoint),
|
||||
self._format_time(waypoint.waypoint.tot),
|
||||
@@ -170,10 +191,10 @@ class FlightPlanBuilder:
|
||||
if self.last_waypoint is None:
|
||||
return "-"
|
||||
|
||||
distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
|
||||
distance = meters(self.last_waypoint.position.distance_to_point(
|
||||
waypoint.position
|
||||
))
|
||||
return f"{distance} NM"
|
||||
return f"{distance.nautical_miles:.1f} NM"
|
||||
|
||||
def _ground_speed(self, waypoint: FlightWaypoint) -> str:
|
||||
if self.last_waypoint is None:
|
||||
@@ -189,19 +210,11 @@ class FlightPlanBuilder:
|
||||
else:
|
||||
return "-"
|
||||
|
||||
distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
|
||||
distance = meters(self.last_waypoint.position.distance_to_point(
|
||||
waypoint.position
|
||||
))
|
||||
duration = (waypoint.tot - last_time).total_seconds() / 3600
|
||||
try:
|
||||
return f"{int(distance / duration)} kt"
|
||||
except ZeroDivisionError:
|
||||
# TODO: Improve resolution of unit conversions.
|
||||
# When waypoints are very close to each other they can end up with
|
||||
# identical TOTs because our unit conversion functions truncate to
|
||||
# int. When waypoints have the same TOT the duration will be zero.
|
||||
# https://github.com/Khopa/dcs_liberation/issues/557
|
||||
return "-"
|
||||
return f"{int(distance.nautical_miles / duration)} kt"
|
||||
|
||||
def build(self) -> List[List[str]]:
|
||||
return self.rows
|
||||
@@ -267,11 +280,9 @@ class BriefingPage(KneeboardPage):
|
||||
str(tanker.tacan),
|
||||
self.format_frequency(tanker.freq),
|
||||
])
|
||||
|
||||
|
||||
writer.table(comm_ladder, headers=["Callsign","Task", "Type", "TACAN", "FREQ"])
|
||||
|
||||
|
||||
writer.heading("JTAC")
|
||||
jtacs = []
|
||||
for jtac in self.jtacs:
|
||||
|
||||
130
gen/naming.py
130
gen/naming.py
@@ -1,16 +1,21 @@
|
||||
from game import db
|
||||
import random
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
ALPHA_MILITARY = ["Alpha","Bravo","Charlie","Delta","Echo","Foxtrot",
|
||||
"Golf","Hotel","India","Juliet","Kilo","Lima","Mike",
|
||||
"November","Oscar","Papa","Quebec","Romeo","Sierra",
|
||||
"Tango","Uniform","Victor","Whisky","XRay","Yankee",
|
||||
"Zulu","Zero"]
|
||||
from dcs.country import Country
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
class NameGenerator:
|
||||
number = 0
|
||||
from game import db
|
||||
|
||||
ANIMALS = [
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
ALPHA_MILITARY = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot",
|
||||
"Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike",
|
||||
"November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra",
|
||||
"Tango", "Uniform", "Victor", "Whisky", "XRay", "Yankee",
|
||||
"Zulu", "Zero"]
|
||||
|
||||
ANIMALS = [
|
||||
"SHARK", "TORTOISE", "BAT", "PANGOLIN", "AARDWOLF",
|
||||
"MONKEY", "BUFFALO", "DOG", "BOBCAT", "LYNX", "PANTHER", "TIGER",
|
||||
"LION", "OWL", "BUTTERFLY", "BISON", "DUCK", "COBRA", "MAMBA",
|
||||
@@ -38,47 +43,92 @@ class NameGenerator:
|
||||
"ANACONDA"
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.number = 0
|
||||
self.ANIMALS = NameGenerator.ANIMALS.copy()
|
||||
class NameGenerator:
|
||||
number = 0
|
||||
infantry_number = 0
|
||||
aircraft_number = 0
|
||||
|
||||
def reset(self):
|
||||
self.number = 0
|
||||
self.ANIMALS = NameGenerator.ANIMALS.copy()
|
||||
ANIMALS = ANIMALS
|
||||
existing_alphas: List[str] = []
|
||||
|
||||
def next_unit_name(self, country, parent_base_id, unit_type):
|
||||
self.number += 1
|
||||
return "unit|{}|{}|{}|{}|".format(country.id, self.number, parent_base_id, db.unit_type_name(unit_type))
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
cls.number = 0
|
||||
cls.infantry_number = 0
|
||||
cls.ANIMALS = ANIMALS
|
||||
cls.existing_alphas = []
|
||||
|
||||
def next_infantry_name(self, country, parent_base_id, unit_type):
|
||||
self.number += 1
|
||||
return "infantry|{}|{}|{}|{}|".format(country.id, self.number, parent_base_id, db.unit_type_name(unit_type))
|
||||
@classmethod
|
||||
def reset_numbers(cls):
|
||||
cls.number = 0
|
||||
cls.infantry_number = 0
|
||||
cls.aircraft_number = 0
|
||||
|
||||
def next_basedefense_name(self):
|
||||
@classmethod
|
||||
def next_aircraft_name(cls, country: Country, parent_base_id: int, flight: Flight):
|
||||
cls.aircraft_number += 1
|
||||
try:
|
||||
if flight.custom_name:
|
||||
name_str = flight.custom_name
|
||||
else:
|
||||
name_str = "{} {}".format(
|
||||
flight.package.target.name, flight.flight_type)
|
||||
except AttributeError: # Here to maintain save compatibility with 2.3
|
||||
name_str = "{} {}".format(
|
||||
flight.package.target.name, flight.flight_type)
|
||||
return "{}|{}|{}|{}|{}|".format(name_str, country.id, cls.aircraft_number, parent_base_id, db.unit_type_name(flight.unit_type))
|
||||
|
||||
@classmethod
|
||||
def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
|
||||
cls.number += 1
|
||||
return "unit|{}|{}|{}|{}|".format(country.id, cls.number, parent_base_id, db.unit_type_name(unit_type))
|
||||
|
||||
@classmethod
|
||||
def next_infantry_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
|
||||
cls.infantry_number += 1
|
||||
return "infantry|{}|{}|{}|{}|".format(country.id, cls.infantry_number, parent_base_id, db.unit_type_name(unit_type))
|
||||
|
||||
@staticmethod
|
||||
def next_basedefense_name():
|
||||
return "basedefense_aa|0|0|"
|
||||
|
||||
def next_awacs_name(self, country):
|
||||
self.number += 1
|
||||
return "awacs|{}|{}|0|".format(country.id, self.number)
|
||||
@classmethod
|
||||
def next_awacs_name(cls, country: Country):
|
||||
cls.number += 1
|
||||
return "awacs|{}|{}|0|".format(country.id, cls.number)
|
||||
|
||||
def next_tanker_name(self, country, unit_type):
|
||||
self.number += 1
|
||||
return "tanker|{}|{}|0|{}".format(country.id, self.number, db.unit_type_name(unit_type))
|
||||
@classmethod
|
||||
def next_tanker_name(cls, country: Country, unit_type: UnitType):
|
||||
cls.number += 1
|
||||
return "tanker|{}|{}|0|{}".format(country.id, cls.number, db.unit_type_name(unit_type))
|
||||
|
||||
def next_carrier_name(self, country):
|
||||
self.number += 1
|
||||
return "carrier|{}|{}|0|".format(country.id, self.number)
|
||||
@classmethod
|
||||
def next_carrier_name(cls, country: Country):
|
||||
cls.number += 1
|
||||
return "carrier|{}|{}|0|".format(country.id, cls.number)
|
||||
|
||||
def random_objective_name(self):
|
||||
if len(self.ANIMALS) == 0:
|
||||
return random.choice(ALPHA_MILITARY).upper() + "#" + str(random.randint(0, 100))
|
||||
@classmethod
|
||||
def random_objective_name(cls):
|
||||
if len(cls.ANIMALS) == 0:
|
||||
for i in range(10):
|
||||
new_name_generated = True
|
||||
alpha_mil_name = random.choice(ALPHA_MILITARY).upper() + "#" + str(random.randint(0, 100))
|
||||
for existing_name in cls.existing_alphas:
|
||||
if existing_name == alpha_mil_name:
|
||||
new_name_generated = False
|
||||
if new_name_generated:
|
||||
cls.existing_alphas.append(alpha_mil_name)
|
||||
return alpha_mil_name
|
||||
|
||||
# At this point, give up trying - something has gone wrong and we haven't been able to make a new name in 10 tries.
|
||||
# We'll just make a longer name using the current unix epoch in nanoseconds. That should be unique... right?
|
||||
last_chance_name = alpha_mil_name + str(time.time_ns())
|
||||
cls.existing_alphas.append(last_chance_name)
|
||||
return last_chance_name
|
||||
else:
|
||||
animal = random.choice(self.ANIMALS)
|
||||
self.ANIMALS.remove(animal)
|
||||
animal = random.choice(cls.ANIMALS)
|
||||
cls.ANIMALS.remove(animal)
|
||||
return animal
|
||||
|
||||
|
||||
namegen = NameGenerator()
|
||||
|
||||
|
||||
|
||||
namegen = NameGenerator
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Radio frequency types and allocators."""
|
||||
import itertools
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Iterator, List, Set
|
||||
|
||||
@@ -71,12 +72,9 @@ class Radio:
|
||||
self.minimum.hertz, self.maximum.hertz, self.step.hertz
|
||||
))
|
||||
|
||||
|
||||
class OutOfChannelsError(RuntimeError):
|
||||
"""Raised when all channels usable by this radio have been allocated."""
|
||||
|
||||
def __init__(self, radio: Radio) -> None:
|
||||
super().__init__(f"No available channels for {radio}")
|
||||
@property
|
||||
def last_channel(self) -> RadioFrequency:
|
||||
return RadioFrequency(self.maximum.hertz - self.step.hertz)
|
||||
|
||||
|
||||
class ChannelInUseError(RuntimeError):
|
||||
@@ -215,7 +213,13 @@ class RadioRegistry:
|
||||
self.reserve(channel)
|
||||
return channel
|
||||
except StopIteration:
|
||||
raise OutOfChannelsError(radio)
|
||||
# In the event of too many channel users, fail gracefully by reusing
|
||||
# the last channel.
|
||||
# https://github.com/Khopa/dcs_liberation/issues/598
|
||||
channel = radio.last_channel
|
||||
logging.warning(
|
||||
f"No more free channels for {radio.name}. Reusing {channel}.")
|
||||
return channel
|
||||
|
||||
def alloc_uhf(self) -> RadioFrequency:
|
||||
"""Allocates a UHF radio channel suitable for inter-flight comms.
|
||||
|
||||
34
gen/sam/aaa_ks19.py
Normal file
34
gen/sam/aaa_ks19.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import random
|
||||
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
AirDefenseRange,
|
||||
AirDefenseGroupGenerator,
|
||||
)
|
||||
from pydcs_extensions.highdigitsams import highdigitsams
|
||||
|
||||
|
||||
class KS19Generator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
This generate a KS 19 flak artillery group (KS-19 from the High Digit SAM mod)
|
||||
"""
|
||||
|
||||
name = "KS-19 AAA Site"
|
||||
price = 98
|
||||
|
||||
def generate(self):
|
||||
|
||||
spacing = random.randint(10, 40)
|
||||
|
||||
self.add_unit(highdigitsams.AAA_SON_9_Fire_Can, "TR", self.position.x - 20, self.position.y - 20, self.heading)
|
||||
|
||||
index = 0
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
index = index + 1
|
||||
self.add_unit(highdigitsams.AAA_100mm_KS_19, "AAA#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y + spacing * j, self.heading)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
25
gen/sam/aaa_zsu57.py
Normal file
25
gen/sam/aaa_zsu57.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
AirDefenseRange,
|
||||
AirDefenseGroupGenerator,
|
||||
)
|
||||
|
||||
|
||||
class ZSU57Generator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
This generate a Zsu 57 group
|
||||
"""
|
||||
|
||||
name = "ZSU-57-2 Group"
|
||||
price = 60
|
||||
|
||||
def generate(self):
|
||||
num_launchers = 5
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AAA_ZSU_57_2, "SPAA#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
@@ -1,5 +1,9 @@
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Iterator, List
|
||||
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game import Game
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
@@ -21,6 +25,25 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
|
||||
ground_object.skynet_capable = True
|
||||
super().__init__(game, ground_object)
|
||||
|
||||
self.auxiliary_groups: List[VehicleGroup] = []
|
||||
|
||||
def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup:
|
||||
group = VehicleGroup(self.game.next_group_id(),
|
||||
"|".join([self.go.group_name, name_suffix]))
|
||||
self.auxiliary_groups.append(group)
|
||||
return group
|
||||
|
||||
def get_generated_group(self) -> VehicleGroup:
|
||||
raise RuntimeError(
|
||||
"Deprecated call to AirDefenseGroupGenerator.get_generated_group "
|
||||
"misses auxiliary groups. Use AirDefenseGroupGenerator.groups "
|
||||
"instead.")
|
||||
|
||||
@property
|
||||
def groups(self) -> Iterator[VehicleGroup]:
|
||||
yield self.vg
|
||||
yield from self.auxiliary_groups
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from dcs import unitgroup
|
||||
from dcs.mapping import Point
|
||||
from dcs.point import PointAction
|
||||
from dcs.unit import Vehicle, Ship
|
||||
from dcs.unit import Ship, Vehicle
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game.factions.faction import Faction
|
||||
@@ -40,12 +42,17 @@ class GroupGenerator:
|
||||
|
||||
def add_unit(self, unit_type: Type[VehicleType], name: str, pos_x: float,
|
||||
pos_y: float, heading: int) -> Vehicle:
|
||||
return self.add_unit_to_group(self.vg, unit_type, name,
|
||||
Point(pos_x, pos_y), heading)
|
||||
|
||||
def add_unit_to_group(self, group: unitgroup.VehicleGroup,
|
||||
unit_type: Type[VehicleType], name: str,
|
||||
position: Point, heading: int) -> Vehicle:
|
||||
unit = Vehicle(self.game.next_unit_id(),
|
||||
f"{self.go.group_name}|{name}", unit_type.id)
|
||||
unit.position.x = pos_x
|
||||
unit.position.y = pos_y
|
||||
f"{group.name}|{name}", unit_type.id)
|
||||
unit.position = position
|
||||
unit.heading = heading
|
||||
self.vg.add_unit(unit)
|
||||
group.add_unit(unit)
|
||||
return unit
|
||||
|
||||
def get_circular_position(self, num_units, launcher_distance, coverage=90):
|
||||
|
||||
@@ -11,7 +11,9 @@ from game.theater.theatergroundobject import SamGroundObject
|
||||
from gen.sam.aaa_bofors import BoforsGenerator
|
||||
from gen.sam.aaa_flak import FlakGenerator
|
||||
from gen.sam.aaa_flak18 import Flak18Generator
|
||||
from gen.sam.aaa_ks19 import KS19Generator
|
||||
from gen.sam.aaa_ww2_ally_flak import AllyWW2FlakGenerator
|
||||
from gen.sam.aaa_zsu57 import ZSU57Generator
|
||||
from gen.sam.aaa_zu23_insurgent import ZU23InsurgentGenerator
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
AirDefenseGroupGenerator,
|
||||
@@ -47,11 +49,12 @@ from gen.sam.sam_roland import RolandGenerator
|
||||
from gen.sam.sam_sa10 import (
|
||||
SA10Generator,
|
||||
Tier2SA10Generator,
|
||||
Tier3SA10Generator,
|
||||
Tier3SA10Generator, SA10BGenerator, SA12Generator, SA20Generator, SA20BGenerator, SA23Generator,
|
||||
)
|
||||
from gen.sam.sam_sa11 import SA11Generator
|
||||
from gen.sam.sam_sa13 import SA13Generator
|
||||
from gen.sam.sam_sa15 import SA15Generator
|
||||
from gen.sam.sam_sa17 import SA17Generator
|
||||
from gen.sam.sam_sa19 import SA19Generator
|
||||
from gen.sam.sam_sa2 import SA2Generator
|
||||
from gen.sam.sam_sa3 import SA3Generator
|
||||
@@ -98,7 +101,16 @@ SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = {
|
||||
"ColdWarFlakGenerator": ColdWarFlakGenerator,
|
||||
"EarlyColdWarFlakGenerator": EarlyColdWarFlakGenerator,
|
||||
"FreyaGenerator": FreyaGenerator,
|
||||
"AllyWW2FlakGenerator": AllyWW2FlakGenerator
|
||||
"AllyWW2FlakGenerator": AllyWW2FlakGenerator,
|
||||
"ZSU57Generator": ZSU57Generator,
|
||||
|
||||
"KS19Generator": KS19Generator,
|
||||
"SA10BGenerator": SA10BGenerator,
|
||||
"SA12Generator": SA12Generator,
|
||||
"SA17Generator": SA17Generator,
|
||||
"SA20Generator": SA20Generator,
|
||||
"SA20BGenerator": SA20BGenerator,
|
||||
"SA23Generator": SA23Generator,
|
||||
}
|
||||
|
||||
|
||||
@@ -169,19 +181,19 @@ def get_faction_possible_ewrs_generator(faction: Faction) -> List[Type[GroupGene
|
||||
|
||||
def _generate_anti_air_from(
|
||||
generators: Sequence[Type[AirDefenseGroupGenerator]], game: Game,
|
||||
ground_object: SamGroundObject) -> Optional[VehicleGroup]:
|
||||
ground_object: SamGroundObject) -> List[VehicleGroup]:
|
||||
if not generators:
|
||||
return None
|
||||
return []
|
||||
sam_generator_class = random.choice(generators)
|
||||
generator = sam_generator_class(game, ground_object)
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
return list(generator.groups)
|
||||
|
||||
|
||||
def generate_anti_air_group(
|
||||
game: Game, ground_object: SamGroundObject, faction: Faction,
|
||||
ranges: Optional[Iterable[Set[AirDefenseRange]]] = None
|
||||
) -> Optional[VehicleGroup]:
|
||||
) -> List[VehicleGroup]:
|
||||
"""
|
||||
This generate a SAM group
|
||||
:param game: The Game.
|
||||
@@ -210,11 +222,11 @@ def generate_anti_air_group(
|
||||
for range_options in ranges:
|
||||
generators_for_range = [g for g in generators if
|
||||
g.range() in range_options]
|
||||
group = _generate_anti_air_from(generators_for_range, game,
|
||||
ground_object)
|
||||
if group is not None:
|
||||
return group
|
||||
return None
|
||||
groups = _generate_anti_air_from(generators_for_range, game,
|
||||
ground_object)
|
||||
if groups:
|
||||
return groups
|
||||
return []
|
||||
|
||||
|
||||
def generate_ewr_group(game: Game, ground_object: TheaterGroundObject,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import random
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
@@ -22,7 +23,9 @@ class HawkGenerator(AirDefenseGroupGenerator):
|
||||
self.add_unit(AirDefence.SAM_Hawk_TR_AN_MPQ_46, "TR", self.position.x + 40, self.position.y, self.heading)
|
||||
|
||||
# Triple A for close range defense
|
||||
self.add_unit(AirDefence.AAA_Vulcan_M163, "AAA", self.position.x + 20, self.position.y+30, self.heading)
|
||||
aa_group = self.add_auxiliary_group("AA")
|
||||
self.add_unit_to_group(aa_group, AirDefence.AAA_Vulcan_M163, "AAA",
|
||||
self.position + Point(20, 30), self.heading)
|
||||
|
||||
num_launchers = random.randint(3, 6)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import random
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
@@ -21,8 +22,13 @@ class HQ7Generator(AirDefenseGroupGenerator):
|
||||
self.add_unit(AirDefence.HQ_7_Self_Propelled_LN, "LN", self.position.x + 20, self.position.y, self.heading)
|
||||
|
||||
# Triple A for close range defense
|
||||
self.add_unit(AirDefence.AAA_ZU_23_on_Ural_375, "AAA1", self.position.x + 20, self.position.y+30, self.heading)
|
||||
self.add_unit(AirDefence.AAA_ZU_23_on_Ural_375, "AAA2", self.position.x - 20, self.position.y-30, self.heading)
|
||||
aa_group = self.add_auxiliary_group("AA")
|
||||
self.add_unit_to_group(aa_group, AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
"AAA1", self.position + Point(20, 30),
|
||||
self.heading)
|
||||
self.add_unit_to_group(aa_group, AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
"AAA2", self.position - Point(20, 30),
|
||||
self.heading)
|
||||
|
||||
num_launchers = random.randint(0, 3)
|
||||
if num_launchers > 0:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import random
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
@@ -30,10 +31,12 @@ class PatriotGenerator(AirDefenseGroupGenerator):
|
||||
self.add_unit(AirDefence.SAM_Patriot_LN_M901, "LN#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
# Short range protection for high value site
|
||||
aa_group = self.add_auxiliary_group("AA")
|
||||
num_launchers = random.randint(3, 4)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=200, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2])
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(aa_group, AirDefence.AAA_Vulcan_M163,
|
||||
f"SPAAA#{i}", Point(x, y), heading)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import random
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unittype import VehicleType
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from game import Game
|
||||
from game.theater import SamGroundObject
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
AirDefenseRange,
|
||||
AirDefenseGroupGenerator,
|
||||
)
|
||||
from pydcs_extensions.highdigitsams import highdigitsams
|
||||
|
||||
|
||||
class SA10Generator(AirDefenseGroupGenerator):
|
||||
@@ -16,20 +21,30 @@ class SA10Generator(AirDefenseGroupGenerator):
|
||||
name = "SA-10/S-300PS Battery"
|
||||
price = 550
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject):
|
||||
super().__init__(game, ground_object)
|
||||
self.sr1 = AirDefence.SAM_SA_10_S_300PS_SR_5N66M
|
||||
self.sr2 = AirDefence.SAM_SA_10_S_300PS_SR_64H6E
|
||||
self.cp = AirDefence.SAM_SA_10_S_300PS_CP_54K6
|
||||
self.tr1 = AirDefence.SAM_SA_10_S_300PS_TR_30N6
|
||||
self.tr2 = AirDefence.SAM_SA_10_S_300PS_TR_30N6
|
||||
self.ln1 = AirDefence.SAM_SA_10_S_300PS_LN_5P85C
|
||||
self.ln2 = AirDefence.SAM_SA_10_S_300PS_LN_5P85D
|
||||
|
||||
def generate(self):
|
||||
# Search Radar
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_SR_5N66M, "SR1", self.position.x, self.position.y + 40, self.heading)
|
||||
self.add_unit(self.sr1, "SR1", self.position.x, self.position.y + 40, self.heading)
|
||||
|
||||
# Search radar for missiles (optionnal)
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_SR_64H6E, "SR2", self.position.x - 40, self.position.y, self.heading)
|
||||
self.add_unit(self.sr2, "SR2", self.position.x - 40, self.position.y, self.heading)
|
||||
|
||||
# Command Post
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_CP_54K6, "CP", self.position.x, self.position.y, self.heading)
|
||||
self.add_unit(self.cp, "CP", self.position.x, self.position.y, self.heading)
|
||||
|
||||
# 2 Tracking radars
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_TR_30N6, "TR1", self.position.x - 40, self.position.y - 40, self.heading)
|
||||
self.add_unit(self.tr1, "TR1", self.position.x - 40, self.position.y - 40, self.heading)
|
||||
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_TR_30N6, "TR2", self.position.x + 40, self.position.y - 40,
|
||||
self.add_unit(self.tr2, "TR2", self.position.x + 40, self.position.y - 40,
|
||||
self.heading)
|
||||
|
||||
# 2 different launcher type (C & D)
|
||||
@@ -37,9 +52,9 @@ class SA10Generator(AirDefenseGroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=100, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
if i%2 == 0:
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_LN_5P85C, "LN#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(self.ln1, "LN#" + str(i), position[0], position[1], position[2])
|
||||
else:
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_LN_5P85D, "LN#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(self.ln2, "LN#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
self.generate_defensive_groups()
|
||||
|
||||
@@ -49,47 +64,126 @@ class SA10Generator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate_defensive_groups(self) -> None:
|
||||
# AAA for defending against close targets.
|
||||
aa_group = self.add_auxiliary_group("AA")
|
||||
num_launchers = random.randint(6, 8)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=210, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "AA#" + str(i),
|
||||
position[0], position[1], position[2])
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(aa_group, AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
f"AA#{i}", Point(x, y), heading)
|
||||
|
||||
|
||||
class Tier2SA10Generator(SA10Generator):
|
||||
def generate_defensive_groups(self) -> None:
|
||||
# Create AAA the way the main group does.
|
||||
super().generate_defensive_groups()
|
||||
|
||||
# SA-15 for both shorter range targets and point defense.
|
||||
pd_group = self.add_auxiliary_group("PD")
|
||||
num_launchers = random.randint(2, 4)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=140, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "PD#" + str(i),
|
||||
position[0], position[1], position[2])
|
||||
|
||||
# AAA for defending against close targets.
|
||||
num_launchers = random.randint(6, 8)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=210, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "AA#" + str(i),
|
||||
position[0], position[1], position[2])
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(pd_group, AirDefence.SAM_SA_15_Tor_9A331,
|
||||
f"PD#{i}", Point(x, y), heading)
|
||||
|
||||
|
||||
class Tier3SA10Generator(SA10Generator):
|
||||
def generate_defensive_groups(self) -> None:
|
||||
# SA-15 for both shorter range targets and point defense.
|
||||
num_launchers = random.randint(2, 4)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=140, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "PD#" + str(i),
|
||||
position[0], position[1], position[2])
|
||||
|
||||
# AAA for defending against close targets.
|
||||
aa_group = self.add_auxiliary_group("AA")
|
||||
num_launchers = random.randint(6, 8)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=210, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_SA_19_Tunguska_2S6, "AA#" + str(i),
|
||||
position[0], position[1], position[2])
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(aa_group, AirDefence.SAM_SA_19_Tunguska_2S6,
|
||||
f"AA#{i}", Point(x, y), heading)
|
||||
|
||||
# SA-15 for both shorter range targets and point defense.
|
||||
pd_group = self.add_auxiliary_group("PD")
|
||||
num_launchers = random.randint(2, 4)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=140, coverage=360)
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(pd_group, AirDefence.SAM_SA_15_Tor_9A331,
|
||||
f"PD#{i}", Point(x, y), heading)
|
||||
|
||||
|
||||
class SA10BGenerator(Tier3SA10Generator):
|
||||
|
||||
price = 700
|
||||
name = "SA-10B/S-300PS Battery"
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject):
|
||||
super().__init__(game, ground_object)
|
||||
self.sr1 = highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR
|
||||
self.sr2 = highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR
|
||||
self.cp = highdigitsams.SAM_SA_10B_S_300PS_54K6_CP
|
||||
self.tr1 = highdigitsams.SAM_SA_10B_S_300PS_30N6_TR
|
||||
self.tr2 = highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR
|
||||
self.ln1 = highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN
|
||||
self.ln2 = highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN
|
||||
|
||||
|
||||
class SA12Generator(Tier3SA10Generator):
|
||||
|
||||
price = 750
|
||||
name = "SA-12/S-300V Battery"
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject):
|
||||
super().__init__(game, ground_object)
|
||||
self.sr1 = highdigitsams.SAM_SA_12_S_300V_9S15_SR
|
||||
self.sr2 = highdigitsams.SAM_SA_12_S_300V_9S19_SR
|
||||
self.cp = highdigitsams.SAM_SA_12_S_300V_9S457_CP
|
||||
self.tr1 = highdigitsams.SAM_SA_12_S_300V_9S32_TR
|
||||
self.tr2 = highdigitsams.SAM_SA_12_S_300V_9S32_TR
|
||||
self.ln1 = highdigitsams.SAM_SA_12_S_300V_9A82_LN
|
||||
self.ln2 = highdigitsams.SAM_SA_12_S_300V_9A83_LN
|
||||
|
||||
|
||||
class SA20Generator(Tier3SA10Generator):
|
||||
|
||||
price = 800
|
||||
name = "SA-20/S-300PMU-1 Battery"
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject):
|
||||
super().__init__(game, ground_object)
|
||||
self.sr1 = highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E
|
||||
self.sr2 = highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E
|
||||
self.cp = highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6
|
||||
self.tr1 = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E
|
||||
self.tr2 = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck
|
||||
self.ln1 = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE
|
||||
self.ln2 = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE
|
||||
|
||||
|
||||
class SA20BGenerator(Tier3SA10Generator):
|
||||
|
||||
price = 850
|
||||
name = "SA-20B/S-300PMU-2 Battery"
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject):
|
||||
super().__init__(game, ground_object)
|
||||
self.sr1 = highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E
|
||||
self.sr2 = highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E
|
||||
self.cp = highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2
|
||||
self.tr1 = highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck
|
||||
self.tr2 = highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck
|
||||
self.ln1 = highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2
|
||||
self.ln2 = highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2
|
||||
|
||||
|
||||
class SA23Generator(Tier3SA10Generator):
|
||||
|
||||
price = 950
|
||||
name = "SA-23/S-300VM Battery"
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject):
|
||||
super().__init__(game, ground_object)
|
||||
self.sr1 = highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR
|
||||
self.sr2 = highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR
|
||||
self.cp = highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP
|
||||
self.tr1 = highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR
|
||||
self.tr2 = highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR
|
||||
self.ln1 = highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN
|
||||
self.ln2 = highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN
|
||||
30
gen/sam/sam_sa17.py
Normal file
30
gen/sam/sam_sa17.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
AirDefenseRange,
|
||||
AirDefenseGroupGenerator,
|
||||
)
|
||||
from pydcs_extensions.highdigitsams import highdigitsams
|
||||
|
||||
|
||||
class SA17Generator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
This generate a SA-17 group
|
||||
"""
|
||||
|
||||
name = "SA-17 Grizzly Battery"
|
||||
price = 180
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(AirDefence.SAM_SA_11_Buk_SR_9S18M1, "SR", self.position.x + 20, self.position.y, self.heading)
|
||||
self.add_unit(AirDefence.SAM_SA_11_Buk_CC_9S470M1, "CC", self.position.x, self.position.y, self.heading)
|
||||
|
||||
positions = self.get_circular_position(3, launcher_distance=140, coverage=180)
|
||||
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2, "LN#" + str(i), position[0], position[1],
|
||||
position[2])
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Medium
|
||||
2
pydcs
2
pydcs
Submodule pydcs updated: c9751f54e0...84f116c358
@@ -132,31 +132,31 @@ class Hercules(PlaneType):
|
||||
charge_total = 1680
|
||||
chaff_charge_size = 1
|
||||
flare_charge_size = 1
|
||||
radio_frequency = 305
|
||||
radio_frequency = 118
|
||||
|
||||
panel_radio = {
|
||||
1: {
|
||||
"channels": {
|
||||
1: 305,
|
||||
2: 264,
|
||||
4: 256,
|
||||
8: 257,
|
||||
16: 261,
|
||||
17: 261,
|
||||
9: 255,
|
||||
18: 251,
|
||||
5: 254,
|
||||
10: 262,
|
||||
20: 266,
|
||||
11: 259,
|
||||
3: 265,
|
||||
6: 250,
|
||||
12: 268,
|
||||
13: 269,
|
||||
7: 270,
|
||||
14: 260,
|
||||
19: 253,
|
||||
15: 263
|
||||
1: 118,
|
||||
2: 119,
|
||||
4: 121,
|
||||
8: 125,
|
||||
16: 133,
|
||||
17: 134,
|
||||
9: 126,
|
||||
18: 135,
|
||||
5: 122,
|
||||
10: 127,
|
||||
20: 143,
|
||||
11: 128,
|
||||
3: 120,
|
||||
6: 123,
|
||||
12: 129,
|
||||
13: 130,
|
||||
7: 124,
|
||||
14: 131,
|
||||
19: 136,
|
||||
15: 132
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,102 @@
|
||||
from dcs import unittype
|
||||
|
||||
|
||||
class AAA_SON_9_Fire_Can(unittype.VehicleType):
|
||||
id = "Fire Can radar"
|
||||
name = "AAA SON-9 Fire Can"
|
||||
detection_range = 35000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class AAA_100mm_KS_19(unittype.VehicleType):
|
||||
id = "KS19"
|
||||
name = "AAA 100mm KS-19"
|
||||
detection_range = 0
|
||||
threat_range = 15000
|
||||
air_weapon_dist = 15000
|
||||
|
||||
|
||||
class SAM_SA_10B_S_300PS_54K6_CP(unittype.VehicleType):
|
||||
id = "S-300PS SA-10B 54K6 cp"
|
||||
name = "SAM SA-10B S-300PS 54K6 CP"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_10B_S_300PS_5P85SE_LN(unittype.VehicleType):
|
||||
id = "S-300PS 5P85SE_mod ln"
|
||||
name = "SAM SA-10B S-300PS 5P85SE LN "
|
||||
detection_range = 0
|
||||
threat_range = 75000
|
||||
air_weapon_dist = 75000
|
||||
|
||||
|
||||
class SAM_SA_10B_S_300PS_5P85SU_LN(unittype.VehicleType):
|
||||
id = "S-300PS 5P85SU_mod ln"
|
||||
name = "SAM SA-10B S-300PS 5P85SU LN "
|
||||
detection_range = 0
|
||||
threat_range = 75000
|
||||
air_weapon_dist = 75000
|
||||
|
||||
|
||||
class SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE(unittype.VehicleType):
|
||||
id = "S-300PS 5P85CE ln"
|
||||
name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85CE"
|
||||
detection_range = 0
|
||||
threat_range = 90000
|
||||
air_weapon_dist = 90000
|
||||
|
||||
|
||||
class SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE(unittype.VehicleType):
|
||||
id = "S-300PS 5P85DE ln"
|
||||
name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85DE"
|
||||
detection_range = 0
|
||||
threat_range = 90000
|
||||
air_weapon_dist = 90000
|
||||
|
||||
|
||||
class SAM_SA_10B_S_300PS_30N6_TR(unittype.VehicleType):
|
||||
id = "S-300PS 30N6 TRAILER tr"
|
||||
name = "SAM SA-10B S-300PS 30N6 TR"
|
||||
detection_range = 160000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_10B_S_300PS_40B6M_TR(unittype.VehicleType):
|
||||
id = "S-300PS SA-10B 40B6M MAST tr"
|
||||
name = "SAM SA-10B S-300PS 40B6M TR"
|
||||
detection_range = 160000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_10B_S_300PS_40B6MD_SR(unittype.VehicleType):
|
||||
id = "S-300PS SA-10B 40B6MD MAST sr"
|
||||
name = "SAM SA-10B S-300PS 40B6MD SR"
|
||||
detection_range = 60000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_10B_S_300PS_64H6E_SR(unittype.VehicleType):
|
||||
id = "S-300PS 64H6E TRAILER sr"
|
||||
name = "SAM SA-10B S-300PS 64H6E SR"
|
||||
detection_range = 160000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_CP_54K6(unittype.VehicleType):
|
||||
id = "S-300PMU1 54K6 cp"
|
||||
name = "SAM SA-20 S-300PMU1 CP 54K6"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_TR_30N6E(unittype.VehicleType):
|
||||
id = "S-300PMU1 40B6M tr"
|
||||
name = "SAM SA-20 S-300PMU1 TR 30N6E"
|
||||
@@ -33,6 +129,110 @@ class SAM_SA_20_S_300PMU1_SR_64N6E(unittype.VehicleType):
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_LN_5P85CE(unittype.VehicleType):
|
||||
id = "S-300PMU1 5P85CE ln"
|
||||
name = "SAM SA-20 S-300PMU1 LN 5P85CE"
|
||||
detection_range = 0
|
||||
threat_range = 150000
|
||||
air_weapon_dist = 150000
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_LN_5P85DE(unittype.VehicleType):
|
||||
id = "S-300PMU1 5P85DE ln"
|
||||
name = "SAM SA-20 S-300PMU1 LN 5P85DE"
|
||||
detection_range = 0
|
||||
threat_range = 150000
|
||||
air_weapon_dist = 150000
|
||||
|
||||
|
||||
class SAM_SA_20B_S_300PMU2_CP_54K6E2(unittype.VehicleType):
|
||||
id = "S-300PMU2 54K6E2 cp"
|
||||
name = "SAM SA-20B S-300PMU2 CP 54K6E2"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20B_S_300PMU2_TR_92H6E_truck(unittype.VehicleType):
|
||||
id = "S-300PMU2 92H6E tr"
|
||||
name = "SAM SA-20B S-300PMU2 TR 92H6E(truck)"
|
||||
detection_range = 270000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20B_S_300PMU2_SR_64N6E2(unittype.VehicleType):
|
||||
id = "S-300PMU2 64H6E2 sr"
|
||||
name = "SAM SA-20B S-300PMU2 SR 64N6E2"
|
||||
detection_range = 330000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20B_S_300PMU2_LN_5P85SE2(unittype.VehicleType):
|
||||
id = "S-300PMU2 5P85SE2 ln"
|
||||
name = "SAM SA-20B S-300PMU2 LN 5P85SE2"
|
||||
detection_range = 0
|
||||
threat_range = 200000
|
||||
air_weapon_dist = 200000
|
||||
|
||||
|
||||
class SAM_SA_12_S_300V_9S457_CP(unittype.VehicleType):
|
||||
id = "S-300V 9S457 cp"
|
||||
name = "SAM SA-12 S-300V 9S457 CP"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_12_S_300V_9A82_LN(unittype.VehicleType):
|
||||
id = "S-300V 9A82 ln"
|
||||
name = "SAM SA-12 S-300V 9A82 LN"
|
||||
detection_range = 0
|
||||
threat_range = 100000
|
||||
air_weapon_dist = 100000
|
||||
|
||||
|
||||
class SAM_SA_12_S_300V_9A83_LN(unittype.VehicleType):
|
||||
id = "S-300V 9A83 ln"
|
||||
name = "SAM SA-12 S-300V 9A83 LN"
|
||||
detection_range = 0
|
||||
threat_range = 75000
|
||||
air_weapon_dist = 75000
|
||||
|
||||
|
||||
class SAM_SA_12_S_300V_9S15_SR(unittype.VehicleType):
|
||||
id = "S-300V 9S15 sr"
|
||||
name = "SAM SA-12 S-300V 9S15 SR"
|
||||
detection_range = 240000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_12_S_300V_9S19_SR(unittype.VehicleType):
|
||||
id = "S-300V 9S19 sr"
|
||||
name = "SAM SA-12 S-300V 9S19 SR"
|
||||
detection_range = 175000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_12_S_300V_9S32_TR(unittype.VehicleType):
|
||||
id = "S-300V 9S32 tr"
|
||||
name = "SAM SA-12 S-300V 9S32 TR"
|
||||
detection_range = 150000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_23_S_300VM_9S457ME_CP(unittype.VehicleType):
|
||||
id = "S-300VM 9S457ME cp"
|
||||
name = "SAM SA-23 S-300VM 9S457ME CP"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_23_S_300VM_9S15M2_SR(unittype.VehicleType):
|
||||
id = "S-300VM 9S15M2 sr"
|
||||
name = "SAM SA-23 S-300VM 9S15M2 SR"
|
||||
@@ -57,38 +257,6 @@ class SAM_SA_23_S_300VM_9S32ME_TR(unittype.VehicleType):
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_LN_5P85CE(unittype.VehicleType):
|
||||
id = "S-300PMU1 5P85CE ln"
|
||||
name = "SAM SA-20 S-300PMU1 LN 5P85CE"
|
||||
detection_range = 0
|
||||
threat_range = 150000
|
||||
air_weapon_dist = 150000
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_LN_5P85DE(unittype.VehicleType):
|
||||
id = "S-300PMU1 5P85DE ln"
|
||||
name = "SAM SA-20 S-300PMU1 LN 5P85DE"
|
||||
detection_range = 0
|
||||
threat_range = 150000
|
||||
air_weapon_dist = 150000
|
||||
|
||||
|
||||
class SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE(unittype.VehicleType):
|
||||
id = "S-300PS 5P85CE ln"
|
||||
name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85CE"
|
||||
detection_range = 0
|
||||
threat_range = 90000
|
||||
air_weapon_dist = 90000
|
||||
|
||||
|
||||
class SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE(unittype.VehicleType):
|
||||
id = "S-300PS 5P85DE ln"
|
||||
name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85DE"
|
||||
detection_range = 0
|
||||
threat_range = 90000
|
||||
air_weapon_dist = 90000
|
||||
|
||||
|
||||
class SAM_SA_23_S_300VM_9A83ME_LN(unittype.VehicleType):
|
||||
id = "S-300VM 9A83ME ln"
|
||||
name = "SAM SA-23 S-300VM 9A83ME LN"
|
||||
@@ -137,22 +305,6 @@ class SAM_SA_3__V_601P__LN_5P73(unittype.VehicleType):
|
||||
air_weapon_dist = 18000
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_CP_54K6(unittype.VehicleType):
|
||||
id = "S-300PMU1 54K6 cp"
|
||||
name = "SAM SA-20 S-300PMU1 CP 54K6"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_23_S_300VM_9S457ME_CP(unittype.VehicleType):
|
||||
id = "S-300VM 9S457ME cp"
|
||||
name = "SAM SA-23 S-300VM 9S457ME CP"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_24_Igla_S_manpad(unittype.VehicleType):
|
||||
id = "SA-24 Igla-S manpad"
|
||||
name = "SAM SA-24 Igla-S manpad"
|
||||
@@ -167,3 +319,20 @@ class SAM_SA_14_Strela_3_manpad(unittype.VehicleType):
|
||||
detection_range = 5000
|
||||
threat_range = 4500
|
||||
air_weapon_dist = 4500
|
||||
|
||||
|
||||
class Polyana_D4M1_C2_node(unittype.VehicleType):
|
||||
id = "polyana-d4m1 cp"
|
||||
name = "Polyana-D4M1 C2 node"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class _34Ya6E_Gazetchik_E_decoy(unittype.VehicleType):
|
||||
id = "34Ya6E Gazetchik E decoy"
|
||||
name = "34Ya6E Gazetchik E decoy"
|
||||
detection_range = 20000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
@@ -43,25 +43,46 @@ MODDED_VEHICLES = [
|
||||
frenchpack.DIM__TOYOTA_GREEN,
|
||||
frenchpack.DIM__TOYOTA_DESERT,
|
||||
frenchpack.DIM__KAMIKAZE,
|
||||
highdigitsams.AAA_SON_9_Fire_Can,
|
||||
highdigitsams.AAA_100mm_KS_19,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_54K6_CP,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN,
|
||||
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE,
|
||||
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_30N6_TR,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE,
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2,
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck,
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2,
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2,
|
||||
highdigitsams.SAM_SA_12_S_300V_9S457_CP,
|
||||
highdigitsams.SAM_SA_12_S_300V_9A82_LN,
|
||||
highdigitsams.SAM_SA_12_S_300V_9A83_LN,
|
||||
highdigitsams.SAM_SA_12_S_300V_9S15_SR,
|
||||
highdigitsams.SAM_SA_12_S_300V_9S19_SR,
|
||||
highdigitsams.SAM_SA_12_S_300V_9S32_TR,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE,
|
||||
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE,
|
||||
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN,
|
||||
highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2,
|
||||
highdigitsams.SAM_SA_2__V759__LN_SM_90,
|
||||
highdigitsams.SAM_HQ_2_LN_SM_90,
|
||||
highdigitsams.SAM_SA_3__V_601P__LN_5P73,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP,
|
||||
highdigitsams.SAM_SA_24_Igla_S_manpad,
|
||||
highdigitsams.SAM_SA_14_Strela_3_manpad
|
||||
highdigitsams.SAM_SA_14_Strela_3_manpad,
|
||||
highdigitsams.Polyana_D4M1_C2_node,
|
||||
highdigitsams._34Ya6E_Gazetchik_E_decoy
|
||||
]
|
||||
@@ -60,7 +60,7 @@ class Dialog:
|
||||
"""Opens the dialog to edit the given flight."""
|
||||
cls.edit_flight_dialog = QEditFlightDialog(
|
||||
cls.game_model,
|
||||
package_model.package,
|
||||
package_model,
|
||||
flight,
|
||||
parent=parent
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Visibility options for the game map."""
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Iterator, Optional, Union
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Iterator, Optional, Union
|
||||
class DisplayRule:
|
||||
name: str
|
||||
_value: bool
|
||||
debug_only: bool = field(default=False)
|
||||
|
||||
@property
|
||||
def menu_text(self) -> str:
|
||||
@@ -29,8 +30,9 @@ class DisplayRule:
|
||||
|
||||
|
||||
class DisplayGroup:
|
||||
def __init__(self, name: Optional[str]) -> None:
|
||||
def __init__(self, name: Optional[str], debug_only: bool = False) -> None:
|
||||
self.name = name
|
||||
self.debug_only = debug_only
|
||||
|
||||
def __iter__(self) -> Iterator[DisplayRule]:
|
||||
# Python 3.6 enforces that __dict__ is order preserving by default.
|
||||
@@ -47,6 +49,46 @@ class FlightPathOptions(DisplayGroup):
|
||||
self.all = DisplayRule("Show All Flight Paths", True)
|
||||
|
||||
|
||||
class ThreatZoneOptions(DisplayGroup):
|
||||
def __init__(self, coalition_name: str) -> None:
|
||||
super().__init__(f"{coalition_name} Threat Zones")
|
||||
self.none = DisplayRule(
|
||||
f"Hide {coalition_name.lower()} threat zones", True)
|
||||
self.all = DisplayRule(
|
||||
f"Show full {coalition_name.lower()} threat zones", False)
|
||||
self.aircraft = DisplayRule(
|
||||
f"Show {coalition_name.lower()} aircraft threat tones", False)
|
||||
self.air_defenses = DisplayRule(
|
||||
f"Show {coalition_name.lower()} air defenses threat zones", False)
|
||||
|
||||
|
||||
class NavMeshOptions(DisplayGroup):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("Navmeshes", debug_only=True)
|
||||
self.hide = DisplayRule("DEBUG Hide Navmeshes", True)
|
||||
self.blue_navmesh = DisplayRule("DEBUG Show blue navmesh", False)
|
||||
self.red_navmesh = DisplayRule("DEBUG Show red navmesh", False)
|
||||
|
||||
|
||||
class PathDebugFactionOptions(DisplayGroup):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("Faction for path debugging", debug_only=True)
|
||||
self.blue = DisplayRule("Debug blue paths", True)
|
||||
self.red = DisplayRule("Debug red paths", False)
|
||||
|
||||
|
||||
class PathDebugOptions(DisplayGroup):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("Shortest paths", debug_only=True)
|
||||
self.hide = DisplayRule("DEBUG Hide paths", True)
|
||||
self.shortest_path = DisplayRule("DEBUG Show shortest path", False)
|
||||
self.barcap = DisplayRule("DEBUG Show BARCAP plan", False)
|
||||
self.cas = DisplayRule("DEBUG Show CAS plan", False)
|
||||
self.sweep = DisplayRule("DEBUG Show fighter sweep plan", False)
|
||||
self.strike = DisplayRule("DEBUG Show strike plan", False)
|
||||
self.tarcap = DisplayRule("DEBUG Show TARCAP plan", False)
|
||||
|
||||
|
||||
class DisplayOptions:
|
||||
ground_objects = DisplayRule("Ground Objects", True)
|
||||
control_points = DisplayRule("Control Points", True)
|
||||
@@ -57,14 +99,27 @@ class DisplayOptions:
|
||||
map_poly = DisplayRule("Map Polygon Debug Mode", False)
|
||||
waypoint_info = DisplayRule("Waypoint Information", True)
|
||||
culling = DisplayRule("Display Culling Zones", False)
|
||||
actual_frontline_pos = DisplayRule("Display Actual Frontline Location",
|
||||
False)
|
||||
barcap_commit_range = DisplayRule("Display selected BARCAP commit range",
|
||||
False)
|
||||
flight_paths = FlightPathOptions()
|
||||
actual_frontline_pos = DisplayRule("Display Actual Frontline Location", False)
|
||||
blue_threat_zones = ThreatZoneOptions("Blue")
|
||||
red_threat_zones = ThreatZoneOptions("Red")
|
||||
navmeshes = NavMeshOptions()
|
||||
path_debug_faction = PathDebugFactionOptions()
|
||||
path_debug = PathDebugOptions()
|
||||
|
||||
@classmethod
|
||||
def menu_items(cls) -> Iterator[Union[DisplayGroup, DisplayRule]]:
|
||||
debug = False # Set to True to enable debug options.
|
||||
# Python 3.6 enforces that __dict__ is order preserving by default.
|
||||
for value in cls.__dict__.values():
|
||||
if isinstance(value, DisplayRule):
|
||||
if value.debug_only and not debug:
|
||||
continue
|
||||
yield value
|
||||
elif isinstance(value, DisplayGroup):
|
||||
if value.debug_only and not debug:
|
||||
continue
|
||||
yield value
|
||||
|
||||
@@ -7,11 +7,18 @@ from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import dcs
|
||||
from dcs.weapons_data import weapon_ids
|
||||
|
||||
from PySide2 import QtWidgets
|
||||
from PySide2.QtGui import QPixmap
|
||||
from PySide2.QtWidgets import QApplication, QSplashScreen
|
||||
|
||||
from game import Game, db, persistency, VERSION
|
||||
from game.data.weapons import (
|
||||
WEAPON_FALLBACK_MAP,
|
||||
WEAPON_INTRODUCTION_YEARS,
|
||||
Weapon,
|
||||
)
|
||||
from game.settings import Settings
|
||||
from game.theater.start_generator import GameGenerator, GeneratorSettings
|
||||
from qt_ui import (
|
||||
@@ -67,6 +74,8 @@ def run_ui(game: Optional[Game] = None) -> None:
|
||||
uiconstants.load_event_icons()
|
||||
uiconstants.load_aircraft_icons()
|
||||
uiconstants.load_vehicle_icons()
|
||||
uiconstants.load_aircraft_banners()
|
||||
uiconstants.load_vehicle_banners()
|
||||
|
||||
# Replace DCS Mission scripting file to allow DCS Liberation to work
|
||||
try:
|
||||
@@ -103,6 +112,11 @@ def parse_args() -> argparse.Namespace:
|
||||
raise argparse.ArgumentTypeError("path does not exist")
|
||||
return path
|
||||
|
||||
parser.add_argument(
|
||||
"--warn-missing-weapon-data", action="store_true",
|
||||
help="Emits a warning for weapons without date or fallback information."
|
||||
)
|
||||
|
||||
new_game = subparsers.add_parser("new-game")
|
||||
|
||||
new_game.add_argument(
|
||||
@@ -163,6 +177,15 @@ def create_game(campaign_path: Path, blue: str, red: str,
|
||||
return generator.generate()
|
||||
|
||||
|
||||
def lint_weapon_data() -> None:
|
||||
for clsid in weapon_ids:
|
||||
weapon = Weapon.from_clsid(clsid)
|
||||
if weapon not in WEAPON_INTRODUCTION_YEARS:
|
||||
logging.warning(f"{weapon} has no introduction date")
|
||||
if weapon not in WEAPON_FALLBACK_MAP:
|
||||
logging.warning(f"{weapon} has no fallback")
|
||||
|
||||
|
||||
def main():
|
||||
logging_config.init_logging(VERSION)
|
||||
|
||||
@@ -172,6 +195,11 @@ def main():
|
||||
game: Optional[Game] = None
|
||||
|
||||
args = parse_args()
|
||||
|
||||
# TODO: Flesh out data and then make unconditional.
|
||||
if args.warn_missing_weapon_data:
|
||||
lint_weapon_data()
|
||||
|
||||
if args.subcommand == "new-game":
|
||||
game = create_game(args.campaign, args.blue, args.red,
|
||||
args.supercarrier, args.auto_procurement,
|
||||
|
||||
@@ -100,6 +100,8 @@ class PackageModel(QAbstractListModel):
|
||||
#: Emitted when this package is being deleted from the ATO.
|
||||
deleted = Signal()
|
||||
|
||||
tot_changed = Signal()
|
||||
|
||||
def __init__(self, package: Package) -> None:
|
||||
super().__init__()
|
||||
self.package = package
|
||||
@@ -139,6 +141,8 @@ class PackageModel(QAbstractListModel):
|
||||
"""Adds the given flight to the package."""
|
||||
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
|
||||
self.package.add_flight(flight)
|
||||
# update_tot is not called here because the new flight does not have a
|
||||
# flight plan yet. Will be called manually by the caller.
|
||||
self.endInsertRows()
|
||||
|
||||
def delete_flight_at_index(self, index: QModelIndex) -> None:
|
||||
@@ -155,15 +159,27 @@ class PackageModel(QAbstractListModel):
|
||||
self.beginRemoveRows(QModelIndex(), index, index)
|
||||
self.package.remove_flight(flight)
|
||||
self.endRemoveRows()
|
||||
self.update_tot()
|
||||
|
||||
def flight_at_index(self, index: QModelIndex) -> Flight:
|
||||
"""Returns the flight located at the given index."""
|
||||
return self.package.flights[index.row()]
|
||||
|
||||
def update_tot(self, tot: datetime.timedelta) -> None:
|
||||
def set_tot(self, tot: datetime.timedelta) -> None:
|
||||
self.package.time_over_target = tot
|
||||
self.update_tot()
|
||||
# For some reason this is needed to make the UI update quickly.
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def set_asap(self, asap: bool) -> None:
|
||||
self.package.auto_asap = asap
|
||||
self.update_tot()
|
||||
|
||||
def update_tot(self) -> None:
|
||||
if self.package.auto_asap:
|
||||
self.package.set_tot_asap()
|
||||
self.tot_changed.emit()
|
||||
|
||||
@property
|
||||
def mission_target(self) -> MissionTarget:
|
||||
"""Returns the mission target of the package."""
|
||||
|
||||
@@ -72,7 +72,9 @@ COLORS: Dict[str, QColor] = {
|
||||
|
||||
CP_SIZE = 12
|
||||
|
||||
AIRCRAFT_BANNERS: Dict[str, QPixmap] = {}
|
||||
AIRCRAFT_ICONS: Dict[str, QPixmap] = {}
|
||||
VEHICLE_BANNERS: Dict[str, QPixmap] = {}
|
||||
VEHICLES_ICONS: Dict[str, QPixmap] = {}
|
||||
ICONS: Dict[str, QPixmap] = {}
|
||||
|
||||
@@ -130,6 +132,8 @@ def load_icons():
|
||||
ICONS["ship_blue"] = QPixmap("./resources/ui/ground_assets/ship_blue.png")
|
||||
ICONS["missile"] = QPixmap("./resources/ui/ground_assets/missile.png")
|
||||
ICONS["missile_blue"] = QPixmap("./resources/ui/ground_assets/missile_blue.png")
|
||||
ICONS["nothreat"] = QPixmap("./resources/ui/ground_assets/nothreat.png")
|
||||
ICONS["nothreat_blue"] = QPixmap("./resources/ui/ground_assets/nothreat_blue.png")
|
||||
|
||||
ICONS["Generator"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/generator.png")
|
||||
ICONS["Missile"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/missile.png")
|
||||
@@ -171,15 +175,25 @@ def load_event_icons():
|
||||
EVENT_ICONS[image[:-4]] = QPixmap(os.path.join("./resources/ui/events/", image))
|
||||
|
||||
def load_aircraft_icons():
|
||||
for aircraft in os.listdir("./resources/ui/units/aircrafts/"):
|
||||
for aircraft in os.listdir("./resources/ui/units/aircrafts/icons/"):
|
||||
if aircraft.endswith(".jpg"):
|
||||
AIRCRAFT_ICONS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/aircrafts/", aircraft))
|
||||
AIRCRAFT_ICONS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/aircrafts/icons/", aircraft))
|
||||
AIRCRAFT_ICONS["F-16C_50"] = AIRCRAFT_ICONS["F-16C"]
|
||||
AIRCRAFT_ICONS["FA-18C_hornet"] = AIRCRAFT_ICONS["FA-18C"]
|
||||
AIRCRAFT_ICONS["A-10C_2"] = AIRCRAFT_ICONS["A-10C"]
|
||||
|
||||
|
||||
def load_vehicle_icons():
|
||||
for vehicle in os.listdir("./resources/ui/units/vehicles/"):
|
||||
for vehicle in os.listdir("./resources/ui/units/vehicles/icons/"):
|
||||
if vehicle.endswith(".jpg"):
|
||||
VEHICLES_ICONS[vehicle[:-7]] = QPixmap(os.path.join("./resources/ui/units/vehicles/", vehicle))
|
||||
VEHICLES_ICONS[vehicle[:-7]] = QPixmap(os.path.join("./resources/ui/units/vehicles/icons/", vehicle))
|
||||
|
||||
def load_aircraft_banners():
|
||||
for aircraft in os.listdir("./resources/ui/units/aircrafts/banners/"):
|
||||
if aircraft.endswith(".jpg"):
|
||||
AIRCRAFT_BANNERS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/aircrafts/banners/", aircraft))
|
||||
|
||||
def load_vehicle_banners():
|
||||
for aircraft in os.listdir("./resources/ui/units/vehicles/banners/"):
|
||||
if aircraft.endswith(".jpg"):
|
||||
VEHICLE_BANNERS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/vehicles/banners/", aircraft))
|
||||
@@ -1,6 +1,7 @@
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QPushButton
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game.income import Income
|
||||
from qt_ui.windows.finances.QFinancesMenu import QFinancesMenu
|
||||
|
||||
|
||||
@@ -34,14 +35,14 @@ class QBudgetBox(QGroupBox):
|
||||
:param budget: Current money available
|
||||
:param reward: Planned reward for next turn
|
||||
"""
|
||||
self.money_amount.setText(str(budget) + "M (+" + str(reward) + "M)")
|
||||
self.money_amount.setText(str(budget) + "M (+" + str(round(reward,2)) + "M)")
|
||||
|
||||
def setGame(self, game):
|
||||
if game is None:
|
||||
return
|
||||
|
||||
self.game = game
|
||||
self.setBudget(self.game.budget, self.game.budget_reward_amount)
|
||||
self.setBudget(self.game.budget, Income(self.game, player=True).total)
|
||||
self.finances.setEnabled(True)
|
||||
|
||||
def openFinances(self):
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout, QFrame, QGridLayout
|
||||
from PySide2.QtGui import QPixmap
|
||||
|
||||
from game.weather import Conditions, TimeOfDay, Weather
|
||||
from game.utils import meter_to_nm, mps_to_knots
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QVBoxLayout,
|
||||
)
|
||||
from dcs.weather import Weather as PydcsWeather
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game.utils import mps
|
||||
from game.weather import Conditions, TimeOfDay
|
||||
|
||||
|
||||
class QTimeTurnWidget(QGroupBox):
|
||||
"""
|
||||
@@ -163,20 +169,20 @@ class QWeatherWidget(QGroupBox):
|
||||
def updateWinds(self):
|
||||
"""Updates the UI with the current conditions wind info.
|
||||
"""
|
||||
windGlSpeed = mps_to_knots(self.conditions.weather.wind.at_0m.speed or 0)
|
||||
windGlSpeed = mps(self.conditions.weather.wind.at_0m.speed or 0)
|
||||
windGlDir = str(self.conditions.weather.wind.at_0m.direction or 0).rjust(3, '0')
|
||||
self.windGLSpeedLabel.setText('{}kts'.format(windGlSpeed))
|
||||
self.windGLDirLabel.setText('{}º'.format(windGlDir))
|
||||
self.windGLSpeedLabel.setText(f'{int(windGlSpeed.knots)}kts')
|
||||
self.windGLDirLabel.setText(f'{windGlDir}º')
|
||||
|
||||
windFL08Speed = mps_to_knots(self.conditions.weather.wind.at_2000m.speed or 0)
|
||||
windFL08Speed = mps(self.conditions.weather.wind.at_2000m.speed or 0)
|
||||
windFL08Dir = str(self.conditions.weather.wind.at_2000m.direction or 0).rjust(3, '0')
|
||||
self.windFL08SpeedLabel.setText('{}kts'.format(windFL08Speed))
|
||||
self.windFL08DirLabel.setText('{}º'.format(windFL08Dir))
|
||||
self.windFL08SpeedLabel.setText(f'{int(windFL08Speed.knots)}kts')
|
||||
self.windFL08DirLabel.setText(f'{windFL08Dir}º')
|
||||
|
||||
windFL26Speed = mps_to_knots(self.conditions.weather.wind.at_8000m.speed or 0)
|
||||
windFL26Speed = mps(self.conditions.weather.wind.at_8000m.speed or 0)
|
||||
windFL26Dir = str(self.conditions.weather.wind.at_8000m.direction or 0).rjust(3, '0')
|
||||
self.windFL26SpeedLabel.setText('{}kts'.format(windFL26Speed))
|
||||
self.windFL26DirLabel.setText('{}º'.format(windFL26Dir))
|
||||
self.windFL26SpeedLabel.setText(f'{int(windFL26Speed.knots)}kts')
|
||||
self.windFL26DirLabel.setText(f'{windFL26Dir}º')
|
||||
|
||||
def updateForecast(self):
|
||||
"""Updates the Forecast Text and icon with the current conditions wind info.
|
||||
@@ -223,11 +229,10 @@ class QWeatherWidget(QGroupBox):
|
||||
if not fog:
|
||||
self.forecastFog.setText('No fog')
|
||||
else:
|
||||
visvibilityNm = round(meter_to_nm(fog.visibility), 1)
|
||||
self.forecastFog.setText('Fog vis: {}nm'.format(visvibilityNm))
|
||||
visibility = round(fog.visibility.nautical_miles, 1)
|
||||
self.forecastFog.setText(f'Fog vis: {visibility}nm')
|
||||
icon = [time, ('cloudy' if cloudDensity > 1 else None), 'fog']
|
||||
|
||||
|
||||
icon_key = "Weather_{}".format('-'.join(filter(None.__ne__, icon)))
|
||||
icon = CONST.ICONS.get(icon_key) or CONST.ICONS['Weather_night-partly-cloudy']
|
||||
self.weather_icon.setPixmap(icon)
|
||||
|
||||
107
qt_ui/widgets/QIntelBox.py
Normal file
107
qt_ui/widgets/QIntelBox.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtWidgets import (
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
)
|
||||
|
||||
from game import Game
|
||||
from game.income import Income
|
||||
from qt_ui.windows.intel import IntelWindow
|
||||
|
||||
|
||||
class QIntelBox(QGroupBox):
|
||||
def __init__(self, game: Game) -> None:
|
||||
super().__init__("Intel")
|
||||
self.setProperty("style", "IntelSummary")
|
||||
|
||||
self.game = game
|
||||
|
||||
columns = QHBoxLayout()
|
||||
self.setLayout(columns)
|
||||
|
||||
summary = QGridLayout()
|
||||
columns.addLayout(summary)
|
||||
|
||||
summary.addWidget(QLabel("Air superiority:"), 0, 0)
|
||||
self.air_strength = QLabel()
|
||||
summary.addWidget(self.air_strength, 0, 1)
|
||||
|
||||
summary.addWidget(QLabel("Front line:"), 1, 0)
|
||||
self.ground_strength = QLabel()
|
||||
summary.addWidget(self.ground_strength, 1, 1)
|
||||
|
||||
summary.addWidget(QLabel("Economic strength:"), 2, 0)
|
||||
self.economic_strength = QLabel()
|
||||
summary.addWidget(self.economic_strength, 2, 1)
|
||||
|
||||
details = QPushButton("Details")
|
||||
columns.addWidget(details)
|
||||
details.clicked.connect(self.open_details_window)
|
||||
|
||||
self.update_summary()
|
||||
|
||||
self.details_window: Optional[IntelWindow] = None
|
||||
|
||||
def set_game(self, game: Optional[Game]) -> None:
|
||||
self.game = game
|
||||
self.update_summary()
|
||||
|
||||
@staticmethod
|
||||
def forces_strength_text(own: int, enemy: int) -> str:
|
||||
if not enemy:
|
||||
return "enemy eliminated"
|
||||
|
||||
ratio = own / enemy
|
||||
if ratio < 0.6:
|
||||
return "outnumbered"
|
||||
if ratio < 0.8:
|
||||
return "slightly outnumbered"
|
||||
if ratio < 1.2:
|
||||
return "evenly matched"
|
||||
if ratio < 1.4:
|
||||
return "slight advantage"
|
||||
return "strong advantage"
|
||||
|
||||
def economic_strength_text(self) -> str:
|
||||
assert self.game is not None
|
||||
own = Income(self.game, player=True).total
|
||||
enemy = Income(self.game, player=False).total
|
||||
|
||||
if not enemy:
|
||||
return "enemy economy ruined"
|
||||
|
||||
ratio = own / enemy
|
||||
if ratio < 0.6:
|
||||
return "strong disadvantage"
|
||||
if ratio < 0.8:
|
||||
return "slight disadvantage"
|
||||
if ratio < 1.2:
|
||||
return "evenly matched"
|
||||
if ratio < 1.4:
|
||||
return "slight advantage"
|
||||
return "strong advantage"
|
||||
|
||||
def update_summary(self) -> None:
|
||||
if self.game is None:
|
||||
self.air_strength.setText("no data")
|
||||
self.ground_strength.setText("no data")
|
||||
self.economic_strength.setText("no data")
|
||||
return
|
||||
|
||||
data = self.game.game_stats.data_per_turn[-1]
|
||||
|
||||
self.air_strength.setText(self.forces_strength_text(
|
||||
data.allied_units.aircraft_count,
|
||||
data.enemy_units.aircraft_count))
|
||||
self.ground_strength.setText(self.forces_strength_text(
|
||||
data.allied_units.vehicles_count,
|
||||
data.enemy_units.vehicles_count))
|
||||
self.economic_strength.setText(self.economic_strength_text())
|
||||
|
||||
def open_details_window(self) -> None:
|
||||
self.details_window = IntelWindow(self.game)
|
||||
self.details_window.show()
|
||||
@@ -1,3 +1,6 @@
|
||||
import logging
|
||||
import timeit
|
||||
from datetime import timedelta
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide2.QtWidgets import (
|
||||
@@ -16,6 +19,7 @@ from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.widgets.QBudgetBox import QBudgetBox
|
||||
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
|
||||
from qt_ui.widgets.QIntelBox import QIntelBox
|
||||
from qt_ui.widgets.clientslots import MaxPlayerCount
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.QWaitingForMissionResultWindow import \
|
||||
@@ -71,6 +75,8 @@ class QTopPanel(QFrame):
|
||||
self.statistics.setProperty("style", "btn-primary")
|
||||
self.statistics.clicked.connect(self.openStatisticsWindow)
|
||||
|
||||
self.intel_box = QIntelBox(self.game)
|
||||
|
||||
self.buttonBox = QGroupBox("Misc")
|
||||
self.buttonBoxLayout = QHBoxLayout()
|
||||
self.buttonBoxLayout.addWidget(self.settings)
|
||||
@@ -90,6 +96,7 @@ class QTopPanel(QFrame):
|
||||
self.layout.addWidget(self.factionsInfos)
|
||||
self.layout.addWidget(self.conditionsWidget)
|
||||
self.layout.addWidget(self.budgetBox)
|
||||
self.layout.addWidget(self.intel_box)
|
||||
self.layout.addWidget(self.buttonBox)
|
||||
self.layout.addStretch(1)
|
||||
self.layout.addWidget(self.proceedBox)
|
||||
@@ -106,6 +113,7 @@ class QTopPanel(QFrame):
|
||||
self.statistics.setEnabled(True)
|
||||
|
||||
self.conditionsWidget.setCurrentTurn(game.turn, game.conditions)
|
||||
self.intel_box.set_game(game)
|
||||
self.budgetBox.setGame(game)
|
||||
self.factionsInfos.setGame(game)
|
||||
|
||||
@@ -125,9 +133,12 @@ class QTopPanel(QFrame):
|
||||
self.subwindow.show()
|
||||
|
||||
def passTurn(self):
|
||||
start = timeit.default_timer()
|
||||
self.game.pass_turn(no_action=True)
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
self.proceedButton.setEnabled(True)
|
||||
end = timeit.default_timer()
|
||||
logging.info("Skipping turn took %s", timedelta(seconds=end - start))
|
||||
|
||||
def negative_start_packages(self) -> List[Package]:
|
||||
packages = []
|
||||
|
||||
@@ -60,16 +60,15 @@ class FlightDelegate(QStyledItemDelegate):
|
||||
|
||||
def first_row_text(self, index: QModelIndex) -> str:
|
||||
flight = self.flight(index)
|
||||
task = flight.flight_type
|
||||
count = flight.count
|
||||
name = db.unit_type_name(flight.unit_type)
|
||||
estimator = TotEstimator(self.package)
|
||||
delay = estimator.mission_start_time(flight)
|
||||
return f"[{task}] {count} x {name} in {delay}"
|
||||
return f"{flight} in {delay}"
|
||||
|
||||
def second_row_text(self, index: QModelIndex) -> str:
|
||||
flight = self.flight(index)
|
||||
origin = flight.from_cp.name
|
||||
if flight.arrival != flight.departure:
|
||||
return f"From {origin} to {flight.arrival.name}"
|
||||
return f"From {origin}"
|
||||
|
||||
def paint(self, painter: QPainter, option: QStyleOptionViewItem,
|
||||
|
||||
@@ -5,12 +5,50 @@ from PySide2.QtWidgets import QComboBox
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
import gen.flights.ai_flight_planner_db
|
||||
|
||||
from game import Game, db
|
||||
|
||||
class QAircraftTypeSelector(QComboBox):
|
||||
"""Combo box for selecting among the given aircraft types."""
|
||||
|
||||
def __init__(self, aircraft_types: Iterable[Type[FlyingType]]) -> None:
|
||||
def __init__(self, aircraft_types: Iterable[Type[FlyingType]], country: str, mission_type: str) -> None:
|
||||
super().__init__()
|
||||
for aircraft in aircraft_types:
|
||||
self.addItem(f"{aircraft.id}", userData=aircraft)
|
||||
|
||||
self.model().sort(0)
|
||||
self.setSizeAdjustPolicy(self.AdjustToContents)
|
||||
self.country = country
|
||||
self.updateItems(mission_type, aircraft_types)
|
||||
|
||||
def updateItems(self, mission_type: str, aircraft_types):
|
||||
current_aircraft = self.currentData()
|
||||
self.clear()
|
||||
for aircraft in aircraft_types:
|
||||
if mission_type in [FlightType.BARCAP, FlightType.ESCORT, FlightType.INTERCEPTION, FlightType.SWEEP, FlightType.TARCAP]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.CAP_CAPABLE:
|
||||
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
|
||||
elif mission_type in [FlightType.CAS, FlightType.BAI, FlightType.OCA_AIRCRAFT]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.CAS_CAPABLE or aircraft in gen.flights.ai_flight_planner_db.TRANSPORT_CAPABLE:
|
||||
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
|
||||
elif mission_type in [FlightType.SEAD]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.SEAD_CAPABLE:
|
||||
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
|
||||
elif mission_type in [FlightType.DEAD]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.DEAD_CAPABLE:
|
||||
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
|
||||
elif mission_type in [FlightType.STRIKE]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.STRIKE_CAPABLE or aircraft in gen.flights.ai_flight_planner_db.TRANSPORT_CAPABLE:
|
||||
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
|
||||
elif mission_type in [FlightType.ANTISHIP]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.ANTISHIP_CAPABLE:
|
||||
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
|
||||
elif mission_type in [FlightType.OCA_RUNWAY]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.RUNWAY_ATTACK_CAPABLE:
|
||||
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
|
||||
current_aircraft_index = self.findData(current_aircraft)
|
||||
if current_aircraft_index != -1:
|
||||
self.setCurrentIndex(current_aircraft_index)
|
||||
if self.count() == 0:
|
||||
self.addItem("No capable aircraft available", userData=None)
|
||||
@@ -27,6 +27,7 @@ class QOriginAirfieldSelector(QComboBox):
|
||||
self.aircraft = aircraft
|
||||
self.rebuild_selector()
|
||||
self.currentIndexChanged.connect(self.index_changed)
|
||||
self.setSizeAdjustPolicy(self.AdjustToContents)
|
||||
|
||||
def change_aircraft(self, aircraft: FlyingType) -> None:
|
||||
if self.aircraft == aircraft:
|
||||
|
||||
@@ -2,6 +2,7 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||
|
||||
from game import Game
|
||||
from game.theater import ControlPointType
|
||||
from game.utils import Distance
|
||||
from gen import BuildingGroundObject, Conflict, FlightWaypointType
|
||||
from gen.flights.flight import FlightWaypoint
|
||||
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
|
||||
@@ -59,7 +60,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
FlightWaypointType.CUSTOM,
|
||||
pos.x,
|
||||
pos.y,
|
||||
800)
|
||||
Distance.from_meters(800))
|
||||
wpt.name = "Frontline " + cp.name + "/" + ecp.name + " [CAS]"
|
||||
wpt.alt_type = "RADIO"
|
||||
wpt.pretty_name = wpt.name
|
||||
@@ -70,12 +71,12 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured):
|
||||
for ground_object in cp.ground_objects:
|
||||
if not ground_object.is_dead and not isinstance(ground_object, BuildingGroundObject):
|
||||
if not ground_object.is_dead and isinstance(ground_object, BuildingGroundObject):
|
||||
wpt = FlightWaypoint(
|
||||
FlightWaypointType.CUSTOM,
|
||||
ground_object.position.x,
|
||||
ground_object.position.y,
|
||||
0
|
||||
Distance.from_meters(0)
|
||||
)
|
||||
wpt.alt_type = "RADIO"
|
||||
wpt.name = ground_object.waypoint_name
|
||||
@@ -99,7 +100,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
FlightWaypointType.CUSTOM,
|
||||
u.position.x,
|
||||
u.position.y,
|
||||
0
|
||||
Distance.from_meters(0)
|
||||
)
|
||||
wpt.alt_type = "RADIO"
|
||||
wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + u.type + " #" + str(j)
|
||||
@@ -120,7 +121,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
FlightWaypointType.CUSTOM,
|
||||
cp.position.x,
|
||||
cp.position.y,
|
||||
0
|
||||
Distance.from_meters(0)
|
||||
)
|
||||
wpt.alt_type = "RADIO"
|
||||
wpt.name = cp.name
|
||||
|
||||
@@ -58,13 +58,14 @@ class QFrontLine(QGraphicsLineItem):
|
||||
new_package_action.triggered.connect(self.open_new_package_dialog)
|
||||
menu.addAction(new_package_action)
|
||||
|
||||
cheat_forward = QAction(f"CHEAT: Advance Frontline")
|
||||
cheat_forward.triggered.connect(self.cheat_forward)
|
||||
menu.addAction(cheat_forward)
|
||||
if self.game_model.game.settings.enable_frontline_cheats:
|
||||
cheat_forward = QAction(f"CHEAT: Advance Frontline")
|
||||
cheat_forward.triggered.connect(self.cheat_forward)
|
||||
menu.addAction(cheat_forward)
|
||||
|
||||
cheat_backward = QAction(f"CHEAT: Retreat Frontline")
|
||||
cheat_backward.triggered.connect(self.cheat_backward)
|
||||
menu.addAction(cheat_backward)
|
||||
cheat_backward = QAction(f"CHEAT: Retreat Frontline")
|
||||
cheat_backward.triggered.connect(self.cheat_backward)
|
||||
menu.addAction(cheat_backward)
|
||||
|
||||
menu.exec_(event.screenPos())
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ from __future__ import annotations
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
from functools import singledispatchmethod
|
||||
from typing import Iterable, Iterator, List, Optional, Tuple
|
||||
|
||||
from PySide2 import QtWidgets, QtCore
|
||||
from PySide2.QtCore import QPointF, Qt, QLineF, QRectF
|
||||
from PySide2 import QtCore, QtWidgets
|
||||
from PySide2.QtCore import QLineF, QPointF, QRectF, Qt
|
||||
from PySide2.QtGui import (
|
||||
QBrush,
|
||||
QColor,
|
||||
@@ -14,30 +15,52 @@ from PySide2.QtGui import (
|
||||
QPen,
|
||||
QPixmap,
|
||||
QPolygonF,
|
||||
QWheelEvent, )
|
||||
QWheelEvent,
|
||||
)
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
QGraphicsItem,
|
||||
QGraphicsOpacityEffect,
|
||||
QGraphicsScene,
|
||||
QGraphicsView, QGraphicsSceneMouseEvent,
|
||||
QGraphicsSceneMouseEvent,
|
||||
QGraphicsView,
|
||||
)
|
||||
from dcs import Point
|
||||
from dcs.planes import F_16C_50
|
||||
from dcs.mapping import point_from_heading
|
||||
from dcs.unitgroup import Group
|
||||
from shapely.geometry import (
|
||||
LineString,
|
||||
MultiPolygon,
|
||||
Point as ShapelyPoint,
|
||||
Polygon,
|
||||
)
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game, db
|
||||
from game import Game
|
||||
from game.navmesh import NavMesh
|
||||
from game.theater import ControlPoint, Enum
|
||||
from game.theater.conflicttheater import FrontLine, ReferencePoint
|
||||
from game.theater.theatergroundobject import (
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.utils import meter_to_feet, nm_to_meter, meter_to_nm
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from game.weather import TimeOfDay
|
||||
from gen import Conflict
|
||||
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
from gen.flights.flightplan import FlightPlan
|
||||
from qt_ui.displayoptions import DisplayOptions
|
||||
from gen import Conflict, Package
|
||||
from gen.flights.flight import (
|
||||
Flight,
|
||||
FlightType,
|
||||
FlightWaypoint,
|
||||
FlightWaypointType,
|
||||
)
|
||||
from gen.flights.flightplan import (
|
||||
BarCapFlightPlan,
|
||||
FlightPlan,
|
||||
FlightPlanBuilder,
|
||||
InvalidObjectiveLocation,
|
||||
)
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.displayoptions import DisplayOptions, ThreatZoneOptions
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.widgets.map.QFrontLine import QFrontLine
|
||||
from qt_ui.widgets.map.QLiberationScene import QLiberationScene
|
||||
@@ -45,7 +68,7 @@ from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
|
||||
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
|
||||
MAX_SHIP_DISTANCE = 80
|
||||
MAX_SHIP_DISTANCE = nautical_miles(80)
|
||||
|
||||
def binomial(i: int, n: int) -> float:
|
||||
"""Binomial coefficient"""
|
||||
@@ -157,6 +180,9 @@ class QLiberationMap(QGraphicsView):
|
||||
|
||||
self.nm_to_pixel_ratio: int = 0
|
||||
|
||||
self.navmesh_highlight: Optional[QPolygonF] = None
|
||||
self.shortest_path_segments: List[QLineF] = []
|
||||
|
||||
|
||||
def init_scene(self):
|
||||
scene = QLiberationScene(self)
|
||||
@@ -171,7 +197,7 @@ class QLiberationMap(QGraphicsView):
|
||||
self.game = game
|
||||
if self.game is not None:
|
||||
logging.debug("Reloading Map Canvas")
|
||||
self.nm_to_pixel_ratio = self.km_to_pixel(float(nm_to_meter(1)) / 1000.0)
|
||||
self.nm_to_pixel_ratio = self.distance_to_pixels(nautical_miles(1))
|
||||
self.reload_scene()
|
||||
|
||||
"""
|
||||
@@ -239,29 +265,6 @@ class QLiberationMap(QGraphicsView):
|
||||
def update_reference_point(point: ReferencePoint, change: Point) -> None:
|
||||
point.image_coordinates += change
|
||||
|
||||
@staticmethod
|
||||
def aa_ranges(ground_object: TheaterGroundObject) -> Tuple[int, int]:
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
for g in ground_object.groups:
|
||||
for u in g.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 and threat_range
|
||||
# defined, but explicitly set to None.
|
||||
unit_detection_range = getattr(unit, "detection_range", None)
|
||||
if unit_detection_range is not None:
|
||||
detection_range = max(detection_range, unit_detection_range)
|
||||
|
||||
unit_threat_range = getattr(unit, "threat_range", None)
|
||||
if unit_threat_range is not None:
|
||||
threat_range = max(threat_range, unit_threat_range)
|
||||
|
||||
return detection_range, threat_range
|
||||
|
||||
def display_culling(self, scene: QGraphicsScene) -> None:
|
||||
"""Draws the culling distance rings on the map"""
|
||||
culling_points = self.game_model.game.get_culling_points()
|
||||
@@ -273,19 +276,186 @@ class QLiberationMap(QGraphicsView):
|
||||
radius = distance_point[0] - transformed[0]
|
||||
scene.addEllipse(transformed[0]-radius, transformed[1]-radius, 2*radius, 2*radius, CONST.COLORS["transparent"], CONST.COLORS["light_green_transparent"])
|
||||
|
||||
def draw_shapely_poly(self, scene: QGraphicsScene, poly: Polygon, pen: QPen,
|
||||
brush: QBrush) -> Optional[QPolygonF]:
|
||||
if poly.is_empty:
|
||||
return None
|
||||
points = []
|
||||
for x, y in poly.exterior.coords:
|
||||
x, y = self._transform_point(Point(x, y))
|
||||
points.append(QPointF(x, y))
|
||||
return scene.addPolygon(QPolygonF(points), pen, brush)
|
||||
|
||||
def draw_threat_zone(self, scene: QGraphicsScene, poly: Polygon,
|
||||
player: bool) -> None:
|
||||
if player:
|
||||
brush = QColor(0, 132, 255, 100)
|
||||
else:
|
||||
brush = QColor(227, 32, 0, 100)
|
||||
self.draw_shapely_poly(scene, poly, CONST.COLORS["transparent"], brush)
|
||||
|
||||
def display_threat_zones(self, scene: QGraphicsScene,
|
||||
options: ThreatZoneOptions, player: bool) -> None:
|
||||
"""Draws the threat zones on the map."""
|
||||
threat_zones = self.game.threat_zone_for(player)
|
||||
if options.all:
|
||||
threat_poly = threat_zones.all
|
||||
elif options.aircraft:
|
||||
threat_poly = threat_zones.airbases
|
||||
elif options.air_defenses:
|
||||
threat_poly = threat_zones.air_defenses
|
||||
else:
|
||||
return
|
||||
|
||||
if isinstance(threat_poly, MultiPolygon):
|
||||
polys = threat_poly.geoms
|
||||
else:
|
||||
polys = [threat_poly]
|
||||
for poly in polys:
|
||||
self.draw_threat_zone(scene, poly, player)
|
||||
|
||||
def draw_navmesh_neighbor_line(self, scene: QGraphicsScene, poly: Polygon,
|
||||
begin: ShapelyPoint) -> None:
|
||||
vertex = Point(begin.x, begin.y)
|
||||
centroid = poly.centroid
|
||||
direction = Point(centroid.x, centroid.y)
|
||||
end = vertex.point_from_heading(vertex.heading_between_point(direction),
|
||||
nautical_miles(2).meters)
|
||||
|
||||
scene.addLine(QLineF(QPointF(*self._transform_point(vertex)),
|
||||
QPointF(*self._transform_point(end))),
|
||||
CONST.COLORS["yellow"])
|
||||
|
||||
@singledispatchmethod
|
||||
def draw_navmesh_border(self, intersection, scene: QGraphicsScene,
|
||||
poly: Polygon) -> None:
|
||||
raise NotImplementedError("draw_navmesh_border not implemented for %s",
|
||||
intersection.__class__.__name__)
|
||||
|
||||
@draw_navmesh_border.register
|
||||
def draw_navmesh_point_border(self, intersection: ShapelyPoint,
|
||||
scene: QGraphicsScene, poly: Polygon) -> None:
|
||||
# Draw a line from the vertex toward the center of the polygon.
|
||||
self.draw_navmesh_neighbor_line(scene, poly, intersection)
|
||||
|
||||
@draw_navmesh_border.register
|
||||
def draw_navmesh_edge_border(self, intersection: LineString,
|
||||
scene: QGraphicsScene, poly: Polygon) -> None:
|
||||
# Draw a line from the center of the edge toward the center of the
|
||||
# polygon.
|
||||
edge_center = intersection.interpolate(0.5, normalized=True)
|
||||
self.draw_navmesh_neighbor_line(scene, poly, edge_center)
|
||||
|
||||
def display_navmesh(self, scene: QGraphicsScene, player: bool) -> None:
|
||||
for navpoly in self.game.navmesh_for(player).polys:
|
||||
self.draw_shapely_poly(scene, navpoly.poly, CONST.COLORS["black"],
|
||||
CONST.COLORS["transparent"])
|
||||
|
||||
position = self._transform_point(
|
||||
Point(navpoly.poly.centroid.x, navpoly.poly.centroid.y))
|
||||
text = scene.addSimpleText(f"Navmesh {navpoly.ident}",
|
||||
self.waypoint_info_font)
|
||||
text.setBrush(QColor(255, 255, 255))
|
||||
text.setPen(QColor(255, 255, 255))
|
||||
text.moveBy(position[0] + 8, position[1])
|
||||
text.setZValue(2)
|
||||
|
||||
for border in navpoly.neighbors.values():
|
||||
self.draw_navmesh_border(border, scene, navpoly.poly)
|
||||
|
||||
def highlight_mouse_navmesh(self, scene: QGraphicsScene, navmesh: NavMesh,
|
||||
mouse_position: Point) -> None:
|
||||
if self.navmesh_highlight is not None:
|
||||
try:
|
||||
scene.removeItem(self.navmesh_highlight)
|
||||
except RuntimeError:
|
||||
pass
|
||||
navpoly = navmesh.localize(mouse_position)
|
||||
if navpoly is None:
|
||||
return
|
||||
self.navmesh_highlight = self.draw_shapely_poly(
|
||||
scene, navpoly.poly, CONST.COLORS["transparent"],
|
||||
CONST.COLORS["light_green_transparent"])
|
||||
|
||||
def draw_shortest_path(self, scene: QGraphicsScene, navmesh: NavMesh,
|
||||
destination: Point, player: bool) -> None:
|
||||
for line in self.shortest_path_segments:
|
||||
try:
|
||||
scene.removeItem(line)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
if player:
|
||||
origin = self.game.theater.player_points()[0]
|
||||
else:
|
||||
origin = self.game.theater.enemy_points()[0]
|
||||
|
||||
prev_pos = self._transform_point(origin.position)
|
||||
try:
|
||||
path = navmesh.shortest_path(origin.position, destination)
|
||||
except ValueError:
|
||||
return
|
||||
for waypoint in path[1:]:
|
||||
new_pos = self._transform_point(waypoint)
|
||||
flight_path_pen = self.flight_path_pen(player, selected=True)
|
||||
# Draw the line to the *middle* of the waypoint.
|
||||
offset = self.WAYPOINT_SIZE // 2
|
||||
self.shortest_path_segments.append(scene.addLine(
|
||||
prev_pos[0] + offset, prev_pos[1] + offset,
|
||||
new_pos[0] + offset, new_pos[1] + offset,
|
||||
flight_path_pen
|
||||
))
|
||||
|
||||
self.shortest_path_segments.append(scene.addEllipse(
|
||||
new_pos[0], new_pos[1], self.WAYPOINT_SIZE,
|
||||
self.WAYPOINT_SIZE, flight_path_pen, flight_path_pen
|
||||
))
|
||||
|
||||
prev_pos = new_pos
|
||||
|
||||
def draw_test_flight_plan(self, scene: QGraphicsScene, task: FlightType,
|
||||
point_near_target: Point, player: bool) -> None:
|
||||
for line in self.shortest_path_segments:
|
||||
try:
|
||||
scene.removeItem(line)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
self.clear_flight_paths(scene)
|
||||
|
||||
target = self.game.theater.closest_target(point_near_target)
|
||||
|
||||
if player:
|
||||
origin = self.game.theater.player_points()[0]
|
||||
else:
|
||||
origin = self.game.theater.enemy_points()[0]
|
||||
|
||||
package = Package(target)
|
||||
flight = Flight(package, F_16C_50, 2, task, start_type="Warm",
|
||||
departure=origin, arrival=origin, divert=None)
|
||||
package.add_flight(flight)
|
||||
planner = FlightPlanBuilder(self.game, package, is_player=player)
|
||||
try:
|
||||
planner.populate_flight_plan(flight)
|
||||
except InvalidObjectiveLocation:
|
||||
return
|
||||
|
||||
package.time_over_target = TotEstimator(package).earliest_tot()
|
||||
self.draw_flight_plan(scene, flight, selected=True)
|
||||
|
||||
@staticmethod
|
||||
def should_display_ground_objects_at(cp: ControlPoint) -> bool:
|
||||
return ((DisplayOptions.sam_ranges and cp.captured) or
|
||||
(DisplayOptions.enemy_sam_ranges and not cp.captured))
|
||||
|
||||
def draw_threat_range(self, scene: QGraphicsScene, ground_object: TheaterGroundObject, cp: ControlPoint) -> None:
|
||||
def draw_threat_range(self, scene: QGraphicsScene, group: Group, ground_object: TheaterGroundObject, cp: ControlPoint) -> None:
|
||||
go_pos = self._transform_point(ground_object.position)
|
||||
detection_range, threat_range = self.aa_ranges(
|
||||
ground_object
|
||||
)
|
||||
detection_range = ground_object.detection_range(group)
|
||||
threat_range = ground_object.threat_range(group)
|
||||
if threat_range:
|
||||
threat_pos = self._transform_point(Point(ground_object.position.x+threat_range,
|
||||
ground_object.position.y+threat_range))
|
||||
threat_pos = self._transform_point(
|
||||
ground_object.position + Point(threat_range.meters,
|
||||
threat_range.meters))
|
||||
threat_radius = Point(*go_pos).distance_to_point(Point(*threat_pos))
|
||||
|
||||
# Add threat range circle
|
||||
@@ -294,8 +464,9 @@ class QLiberationMap(QGraphicsView):
|
||||
|
||||
if detection_range and DisplayOptions.detection_range:
|
||||
# Add detection range circle
|
||||
detection_pos = self._transform_point(Point(ground_object.position.x+detection_range,
|
||||
ground_object.position.y+detection_range))
|
||||
detection_pos = self._transform_point(
|
||||
ground_object.position + Point(detection_range.meters,
|
||||
detection_range.meters))
|
||||
detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos))
|
||||
scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6,
|
||||
detection_radius, detection_radius, self.detection_pen(cp.captured))
|
||||
@@ -313,7 +484,8 @@ class QLiberationMap(QGraphicsView):
|
||||
|
||||
should_display = self.should_display_ground_objects_at(cp)
|
||||
if ground_object.might_have_aa and should_display:
|
||||
self.draw_threat_range(scene, ground_object, cp)
|
||||
for group in ground_object.groups:
|
||||
self.draw_threat_range(scene, group, ground_object, cp)
|
||||
added_objects.append(ground_object.obj_name)
|
||||
|
||||
def reload_scene(self):
|
||||
@@ -329,6 +501,16 @@ class QLiberationMap(QGraphicsView):
|
||||
if DisplayOptions.culling and self.game.settings.perf_culling:
|
||||
self.display_culling(scene)
|
||||
|
||||
self.display_threat_zones(scene, DisplayOptions.blue_threat_zones,
|
||||
player=True)
|
||||
self.display_threat_zones(scene, DisplayOptions.red_threat_zones,
|
||||
player=False)
|
||||
|
||||
if DisplayOptions.navmeshes.blue_navmesh:
|
||||
self.display_navmesh(scene, player=True)
|
||||
if DisplayOptions.navmeshes.red_navmesh:
|
||||
self.display_navmesh(scene, player=False)
|
||||
|
||||
for cp in self.game.theater.controlpoints:
|
||||
|
||||
pos = self._transform_point(cp.position)
|
||||
@@ -428,8 +610,30 @@ class QLiberationMap(QGraphicsView):
|
||||
flight.flight_plan)
|
||||
prev_pos = tuple(new_pos)
|
||||
|
||||
def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int],
|
||||
player: bool, selected: bool) -> None:
|
||||
if selected and DisplayOptions.barcap_commit_range:
|
||||
self.draw_barcap_commit_range(scene, flight)
|
||||
|
||||
def draw_barcap_commit_range(self, scene: QGraphicsScene,
|
||||
flight: Flight) -> None:
|
||||
if flight.flight_type is not FlightType.BARCAP:
|
||||
return
|
||||
if not isinstance(flight.flight_plan, BarCapFlightPlan):
|
||||
return
|
||||
start = flight.flight_plan.patrol_start
|
||||
end = flight.flight_plan.patrol_end
|
||||
line = LineString([
|
||||
ShapelyPoint(start.x, start.y),
|
||||
ShapelyPoint(end.x, end.y),
|
||||
])
|
||||
doctrine = self.game.faction_for(flight.departure.captured).doctrine
|
||||
bubble = line.buffer(doctrine.cap_engagement_range.meters)
|
||||
self.flight_path_items.append(self.draw_shapely_poly(
|
||||
scene, bubble, CONST.COLORS["yellow"], CONST.COLORS["transparent"]
|
||||
))
|
||||
|
||||
def draw_waypoint(self, scene: QGraphicsScene,
|
||||
position: Tuple[float, float], player: bool,
|
||||
selected: bool) -> None:
|
||||
waypoint_pen = self.waypoint_pen(player, selected)
|
||||
waypoint_brush = self.waypoint_brush(player, selected)
|
||||
self.flight_path_items.append(scene.addEllipse(
|
||||
@@ -441,7 +645,7 @@ class QLiberationMap(QGraphicsView):
|
||||
waypoint: FlightWaypoint, position: Tuple[int, int],
|
||||
flight_plan: FlightPlan) -> None:
|
||||
|
||||
altitude = meter_to_feet(waypoint.alt)
|
||||
altitude = int(waypoint.alt.feet)
|
||||
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
|
||||
|
||||
prefix = "TOT"
|
||||
@@ -472,8 +676,8 @@ class QLiberationMap(QGraphicsView):
|
||||
item.setZValue(2)
|
||||
self.flight_path_items.append(item)
|
||||
|
||||
def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int],
|
||||
pos1: Tuple[int, int], player: bool,
|
||||
def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[float, float],
|
||||
pos1: Tuple[float, float], player: bool,
|
||||
selected: bool) -> None:
|
||||
flight_path_pen = self.flight_path_pen(player, selected)
|
||||
# Draw the line to the *middle* of the waypoint.
|
||||
@@ -567,7 +771,7 @@ class QLiberationMap(QGraphicsView):
|
||||
BIG_LINE = 5
|
||||
SMALL_LINE = 2
|
||||
|
||||
dist = self.km_to_pixel(nm_to_meter(scale_distance_nm)/1000.0)
|
||||
dist = self.distance_to_pixels(nautical_miles(scale_distance_nm))
|
||||
self.scene().addRect(POS_X, POS_Y-PADDING, PADDING*2 + dist, BIG_LINE*2+3*PADDING, pen=CONST.COLORS["black"], brush=CONST.COLORS["black"])
|
||||
l = self.scene().addLine(POS_X + PADDING, POS_Y + BIG_LINE*2, POS_X + PADDING + dist, POS_Y + BIG_LINE*2)
|
||||
|
||||
@@ -663,12 +867,12 @@ class QLiberationMap(QGraphicsView):
|
||||
Point(offset.x / scale.x, offset.y / scale.y))
|
||||
return point_a.world_coordinates - scaled
|
||||
|
||||
def km_to_pixel(self, km):
|
||||
def distance_to_pixels(self, distance: Distance) -> int:
|
||||
p1 = Point(0, 0)
|
||||
p2 = Point(0, 1000*km)
|
||||
p2 = Point(0, distance.meters)
|
||||
p1a = Point(*self._transform_point(p1))
|
||||
p2a = Point(*self._transform_point(p2))
|
||||
return p1a.distance_to_point(p2a)
|
||||
return int(p1a.distance_to_point(p2a))
|
||||
|
||||
def highlight_color(self, transparent: Optional[bool] = False) -> QColor:
|
||||
return QColor(255, 255, 0, 20 if transparent else 255)
|
||||
@@ -742,7 +946,7 @@ class QLiberationMap(QGraphicsView):
|
||||
# Polygon display mode
|
||||
if self.game.theater.landmap is not None:
|
||||
|
||||
for sea_zone in self.game.theater.landmap[2]:
|
||||
for sea_zone in self.game.theater.landmap.sea_zones:
|
||||
print(sea_zone)
|
||||
poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in sea_zone.exterior.coords])
|
||||
if self.reference_point_setup_mode:
|
||||
@@ -751,14 +955,14 @@ class QLiberationMap(QGraphicsView):
|
||||
color = "sea_blue"
|
||||
scene.addPolygon(poly, CONST.COLORS[color], CONST.COLORS[color])
|
||||
|
||||
for inclusion_zone in self.game.theater.landmap[0]:
|
||||
for inclusion_zone in self.game.theater.landmap.inclusion_zones:
|
||||
poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in inclusion_zone.exterior.coords])
|
||||
if self.reference_point_setup_mode:
|
||||
scene.addPolygon(poly, CONST.COLORS["grey_transparent"], CONST.COLORS["dark_grey_transparent"])
|
||||
else:
|
||||
scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_grey"])
|
||||
|
||||
for exclusion_zone in self.game.theater.landmap[1]:
|
||||
for exclusion_zone in self.game.theater.landmap.exclusion_zones:
|
||||
poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in exclusion_zone.exterior.coords])
|
||||
if self.reference_point_setup_mode:
|
||||
scene.addPolygon(poly, CONST.COLORS["grey_transparent"], CONST.COLORS["dark_dark_grey_transparent"])
|
||||
@@ -820,22 +1024,60 @@ class QLiberationMap(QGraphicsView):
|
||||
distance = self.selected_cp.control_point.position.distance_to_point(
|
||||
world_destination
|
||||
)
|
||||
if meter_to_nm(distance) > MAX_SHIP_DISTANCE:
|
||||
if meters(distance) > MAX_SHIP_DISTANCE:
|
||||
return False
|
||||
return self.game.theater.is_in_sea(world_destination)
|
||||
|
||||
def sceneMouseMovedEvent(self, event: QGraphicsSceneMouseEvent):
|
||||
if self.game is None:
|
||||
return
|
||||
|
||||
mouse_position = Point(event.scenePos().x(), event.scenePos().y())
|
||||
if self.state == QLiberationMapState.MOVING_UNIT:
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
self.movement_line.setLine(
|
||||
QLineF(self.movement_line.line().p1(), event.scenePos()))
|
||||
|
||||
pos = Point(event.scenePos().x(), event.scenePos().y())
|
||||
if self.is_valid_ship_pos(pos):
|
||||
if self.is_valid_ship_pos(mouse_position):
|
||||
self.movement_line.setPen(CONST.COLORS["green"])
|
||||
else:
|
||||
self.movement_line.setPen(CONST.COLORS["red"])
|
||||
|
||||
mouse_world_pos = self._scene_to_dcs_coords(mouse_position)
|
||||
if DisplayOptions.navmeshes.blue_navmesh:
|
||||
self.highlight_mouse_navmesh(
|
||||
self.scene(), self.game.blue_navmesh,
|
||||
self._scene_to_dcs_coords(mouse_position))
|
||||
if DisplayOptions.path_debug.shortest_path:
|
||||
self.draw_shortest_path(self.scene(), self.game.blue_navmesh,
|
||||
mouse_world_pos, player=True)
|
||||
|
||||
if DisplayOptions.navmeshes.red_navmesh:
|
||||
self.highlight_mouse_navmesh(
|
||||
self.scene(), self.game.red_navmesh, mouse_world_pos)
|
||||
|
||||
debug_blue = DisplayOptions.path_debug_faction.blue
|
||||
if DisplayOptions.path_debug.shortest_path:
|
||||
self.draw_shortest_path(
|
||||
self.scene(), self.game.navmesh_for(player=debug_blue),
|
||||
mouse_world_pos, player=False)
|
||||
elif not DisplayOptions.path_debug.hide:
|
||||
if DisplayOptions.path_debug.barcap:
|
||||
task = FlightType.BARCAP
|
||||
elif DisplayOptions.path_debug.cas:
|
||||
task = FlightType.CAS
|
||||
elif DisplayOptions.path_debug.sweep:
|
||||
task = FlightType.SWEEP
|
||||
elif DisplayOptions.path_debug.strike:
|
||||
task = FlightType.STRIKE
|
||||
elif DisplayOptions.path_debug.tarcap:
|
||||
task = FlightType.TARCAP
|
||||
else:
|
||||
raise ValueError(
|
||||
"Unexpected value for DisplayOptions.path_debug")
|
||||
self.draw_test_flight_plan(self.scene(), task, mouse_world_pos,
|
||||
player=debug_blue)
|
||||
|
||||
def sceneMousePressEvent(self, event: QGraphicsSceneMouseEvent):
|
||||
if self.state == QLiberationMapState.MOVING_UNIT:
|
||||
if event.buttons() == Qt.RightButton:
|
||||
|
||||
@@ -4,7 +4,7 @@ from PySide2.QtGui import QColor, QPainter
|
||||
from PySide2.QtWidgets import QAction, QMenu
|
||||
|
||||
import qt_ui.uiconstants as const
|
||||
from game.theater import ControlPoint
|
||||
from game.theater import ControlPoint, NavalControlPoint
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
|
||||
from .QMapObject import QMapObject
|
||||
@@ -89,7 +89,7 @@ class QMapControlPoint(QMapObject):
|
||||
return
|
||||
|
||||
for connected in self.control_point.connected_points:
|
||||
if connected.captured:
|
||||
if connected.captured and self.game_model.game.settings.enable_base_capture_cheat:
|
||||
menu.addAction(self.capture_action)
|
||||
break
|
||||
|
||||
@@ -108,7 +108,8 @@ class QMapControlPoint(QMapObject):
|
||||
|
||||
def open_new_package_dialog(self) -> None:
|
||||
"""Extends the default packagedialog to redirect to base menu for red air base."""
|
||||
if not self.control_point.captured:
|
||||
self.on_click()
|
||||
else:
|
||||
super(QMapControlPoint, self).open_new_package_dialog()
|
||||
is_navy = isinstance(self.control_point, NavalControlPoint)
|
||||
if self.control_point.captured or is_navy:
|
||||
super().open_new_package_dialog()
|
||||
return
|
||||
self.on_click()
|
||||
|
||||
@@ -58,9 +58,11 @@ class QMapGroundObject(QMapObject):
|
||||
@property
|
||||
def production_per_turn(self) -> int:
|
||||
production = 0
|
||||
for g in self.control_point.ground_objects:
|
||||
if g.category in REWARDS.keys():
|
||||
production += REWARDS[g.category]
|
||||
for building in self.buildings:
|
||||
if building.is_dead:
|
||||
continue
|
||||
if building.category in REWARDS.keys():
|
||||
production += REWARDS[building.category]
|
||||
return production
|
||||
|
||||
def paint(self, painter, option, widget=None) -> None:
|
||||
@@ -85,10 +87,22 @@ class QMapGroundObject(QMapObject):
|
||||
is_dead = False
|
||||
break
|
||||
|
||||
if cat == "aa":
|
||||
has_threat = False
|
||||
for group in self.ground_object.groups:
|
||||
if self.ground_object.threat_range(group).distance_in_meters > 0:
|
||||
has_threat = True
|
||||
|
||||
if not is_dead and not self.control_point.captured:
|
||||
painter.drawPixmap(rect, const.ICONS[cat + enemy_icons])
|
||||
if cat == "aa" and not has_threat:
|
||||
painter.drawPixmap(rect, const.ICONS["nothreat" + enemy_icons])
|
||||
else:
|
||||
painter.drawPixmap(rect, const.ICONS[cat + enemy_icons])
|
||||
elif not is_dead:
|
||||
painter.drawPixmap(rect, const.ICONS[cat + player_icons])
|
||||
if cat == "aa" and not has_threat:
|
||||
painter.drawPixmap(rect, const.ICONS["nothreat" + player_icons])
|
||||
else:
|
||||
painter.drawPixmap(rect, const.ICONS[cat + player_icons])
|
||||
else:
|
||||
painter.drawPixmap(rect, const.ICONS["destroyed"])
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class QMapObject(QGraphicsRectItem):
|
||||
object_details_action.triggered.connect(self.on_click)
|
||||
menu.addAction(object_details_action)
|
||||
|
||||
# Not all locations have valid objetives. Off-map spawns, for example,
|
||||
# Not all locations have valid objectives. Off-map spawns, for example,
|
||||
# have no mission types.
|
||||
if list(self.mission_target.mission_types(for_player=True)):
|
||||
new_package_action = QAction(f"New package")
|
||||
|
||||
@@ -52,7 +52,7 @@ class QDebriefingWindow(QDialog):
|
||||
for unit_type, count in player_air_losses.items():
|
||||
try:
|
||||
lostUnitsLayout.addWidget(
|
||||
QLabel(db.unit_type_name(unit_type)), row, 0)
|
||||
QLabel(db.unit_get_expanded_info(self.debriefing.player_country, unit_type, 'name')), row, 0)
|
||||
lostUnitsLayout.addWidget(QLabel(str(count)), row, 1)
|
||||
row += 1
|
||||
except AttributeError:
|
||||
@@ -94,7 +94,7 @@ class QDebriefingWindow(QDialog):
|
||||
for unit_type, count in enemy_air_losses.items():
|
||||
try:
|
||||
enemylostUnitsLayout.addWidget(
|
||||
QLabel(db.unit_type_name(unit_type)), row, 0)
|
||||
QLabel(db.unit_get_expanded_info(self.debriefing.enemy_country, unit_type, 'name')), row, 0)
|
||||
enemylostUnitsLayout.addWidget(QLabel(str(count)), row, 1)
|
||||
row += 1
|
||||
except AttributeError:
|
||||
|
||||
@@ -168,18 +168,21 @@ class QLiberationWindow(QMainWindow):
|
||||
|
||||
displayMenu = self.menu.addMenu("&Display")
|
||||
|
||||
last_was_group = True
|
||||
|
||||
last_was_group = False
|
||||
for item in DisplayOptions.menu_items():
|
||||
if isinstance(item, DisplayRule):
|
||||
if last_was_group:
|
||||
displayMenu.addSeparator()
|
||||
self.display_bar.addSeparator()
|
||||
action = self.make_display_rule_action(item)
|
||||
displayMenu.addAction(action)
|
||||
if action.icon():
|
||||
self.display_bar.addAction(action)
|
||||
last_was_group = False
|
||||
elif isinstance(item, DisplayGroup):
|
||||
if not last_was_group:
|
||||
displayMenu.addSeparator()
|
||||
self.display_bar.addSeparator()
|
||||
displayMenu.addSeparator()
|
||||
self.display_bar.addSeparator()
|
||||
group = QActionGroup(displayMenu)
|
||||
for display_rule in item:
|
||||
action = self.make_display_rule_action(display_rule, group)
|
||||
@@ -257,8 +260,6 @@ class QLiberationWindow(QMainWindow):
|
||||
|
||||
def setGame(self, game: Optional[Game]):
|
||||
try:
|
||||
if game is not None:
|
||||
game.on_load()
|
||||
self.game = game
|
||||
if self.info_panel is not None:
|
||||
self.info_panel.setGame(game)
|
||||
@@ -284,7 +285,7 @@ class QLiberationWindow(QMainWindow):
|
||||
"<h4>Authors</h4>" + \
|
||||
"<p>DCS Liberation was originally developed by <b>shdwp</b>, DCS Liberation 2.0 is a partial rewrite based on this work by <b>Khopa</b>." \
|
||||
"<h4>Contributors</h4>" + \
|
||||
"shdwp, Khopa, ColonelPanic, Roach, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl, davidp57, Plob" + \
|
||||
"shdwp, Khopa, ColonelPanic, Roach, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl, davidp57, Plob, Hawkmoon" + \
|
||||
"<h4>Special Thanks :</h4>" \
|
||||
"<b>rp-</b> <i>for the pydcs framework</i><br/>"\
|
||||
"<b>Grimes (mrSkortch)</b> & <b>Speed</b> <i>for the MIST framework</i><br/>"\
|
||||
|
||||
118
qt_ui/windows/QUnitInfoWindow.py
Normal file
118
qt_ui/windows/QUnitInfoWindow.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import logging
|
||||
from typing import Type
|
||||
|
||||
from PySide2 import QtCore
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtGui import QIcon, QMovie, QPixmap
|
||||
from PySide2.QtWidgets import (
|
||||
QDialog,
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QTextBrowser,
|
||||
QFrame,
|
||||
)
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
from dcs.unittype import UnitType, FlyingType, VehicleType
|
||||
import dcs
|
||||
from qt_ui.uiconstants import AIRCRAFT_BANNERS, VEHICLE_BANNERS
|
||||
|
||||
from game.game import Game
|
||||
from game import db
|
||||
|
||||
import gen.flights.ai_flight_planner_db
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
class QUnitInfoWindow(QDialog):
|
||||
|
||||
def __init__(self, game: Game, unit_type: Type[UnitType]) -> None:
|
||||
super(QUnitInfoWindow, self).__init__()
|
||||
self.setModal(True)
|
||||
self.game = game
|
||||
self.unit_type = unit_type
|
||||
self.setWindowTitle(f"Unit Info: {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'name')}")
|
||||
self.setWindowIcon(QIcon("./resources/icon.png"))
|
||||
self.setMinimumHeight(570)
|
||||
self.setMaximumWidth(640)
|
||||
self.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||
|
||||
self.initUi()
|
||||
|
||||
def initUi(self):
|
||||
self.layout = QGridLayout()
|
||||
|
||||
header = QLabel(self)
|
||||
header.setGeometry(0, 0, 720, 360)
|
||||
if dcs.planes.plane_map.get(self.unit_type.id) is not None or dcs.helicopters.helicopter_map.get(self.unit_type.id) is not None:
|
||||
pixmap = AIRCRAFT_BANNERS.get(self.unit_type.id)
|
||||
elif dcs.vehicles.vehicle_map.get(self.unit_type.id) is not None:
|
||||
pixmap = VEHICLE_BANNERS.get(self.unit_type.id)
|
||||
if pixmap is None:
|
||||
pixmap = AIRCRAFT_BANNERS.get("Missing")
|
||||
header.setPixmap(pixmap.scaled(header.width(), header.height()))
|
||||
self.layout.addWidget(header, 0, 0)
|
||||
|
||||
self.gridLayout = QGridLayout()
|
||||
|
||||
# Build the topmost details grid.
|
||||
self.details_grid = QFrame()
|
||||
self.details_grid_layout = QGridLayout()
|
||||
self.details_grid_layout.setMargin(0)
|
||||
|
||||
self.name_box = QLabel(f"<b>Name:</b> {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'manufacturer')} {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'name')}")
|
||||
self.name_box.setProperty("style", "info-element")
|
||||
|
||||
self.country_box = QLabel(f"<b>Country of Origin:</b> {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'country-of-origin')}")
|
||||
self.country_box.setProperty("style", "info-element")
|
||||
|
||||
self.role_box = QLabel(f"<b>Role:</b> {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'role')}")
|
||||
self.role_box.setProperty("style", "info-element")
|
||||
|
||||
self.year_box = QLabel(f"<b>Variant Introduction:</b> {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'year-of-variant-introduction')}")
|
||||
self.year_box.setProperty("style", "info-element")
|
||||
|
||||
self.details_grid_layout.addWidget(self.name_box, 0, 0)
|
||||
self.details_grid_layout.addWidget(self.country_box, 0, 1)
|
||||
self.details_grid_layout.addWidget(self.role_box, 1, 0)
|
||||
self.details_grid_layout.addWidget(self.year_box, 1, 1)
|
||||
|
||||
self.details_grid.setLayout(self.details_grid_layout)
|
||||
|
||||
self.gridLayout.addWidget(self.details_grid, 1, 0)
|
||||
|
||||
# If it's an aircraft, include the task list.
|
||||
if dcs.planes.plane_map.get(self.unit_type.id) is not None or dcs.helicopters.helicopter_map.get(self.unit_type.id) is not None:
|
||||
self.tasks_box = QLabel(f"<b>In-Game Tasks:</b> {self.generateAircraftTasks()}")
|
||||
self.tasks_box.setProperty("style", "info-element")
|
||||
self.gridLayout.addWidget(self.tasks_box, 2, 0)
|
||||
|
||||
# Finally, add the description box.
|
||||
self.details_text = QTextBrowser()
|
||||
self.details_text.setProperty("style", "info-desc")
|
||||
self.details_text.setText(db.unit_get_expanded_info(self.game.player_country, self.unit_type, "text"))
|
||||
self.gridLayout.addWidget(self.details_text, 3, 0)
|
||||
|
||||
self.layout.addLayout(self.gridLayout, 1, 0)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def generateAircraftTasks(self) -> str:
|
||||
aircraft_tasks = ""
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.CAP_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.BARCAP}, {FlightType.ESCORT}, {FlightType.INTERCEPTION}, {FlightType.SWEEP}, {FlightType.TARCAP}, "
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.CAS_CAPABLE or self.unit_type in gen.flights.ai_flight_planner_db.TRANSPORT_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.CAS}, {FlightType.BAI}, {FlightType.OCA_AIRCRAFT}, "
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.SEAD_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.SEAD}, "
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.DEAD_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.DEAD}, "
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.ANTISHIP_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.ANTISHIP}, "
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.RUNWAY_ATTACK_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.OCA_RUNWAY}, "
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.STRIKE_CAPABLE or self.unit_type in gen.flights.ai_flight_planner_db.TRANSPORT_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.STRIKE}, "
|
||||
return aircraft_tasks[:-2]
|
||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import timeit
|
||||
from datetime import timedelta
|
||||
|
||||
from PySide2 import QtCore
|
||||
from PySide2.QtCore import QObject, Qt, Signal
|
||||
@@ -184,11 +186,14 @@ class QWaitingForMissionResultWindow(QDialog):
|
||||
lambda d: self.on_debriefing_update(d), self.game, self.unit_map)
|
||||
|
||||
def process_debriefing(self):
|
||||
start = timeit.default_timer()
|
||||
self.game.finish_event(event=self.gameEvent, debriefing=self.debriefing)
|
||||
self.game.pass_turn()
|
||||
|
||||
GameUpdateSignal.get_instance().sendDebriefing(self.debriefing)
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
end = timeit.default_timer()
|
||||
logging.info("Turn processing took %s", timedelta(seconds=end - start))
|
||||
self.close()
|
||||
|
||||
def debriefing_directory_location(self) -> str:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from typing import Type
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import (
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
@@ -17,6 +18,7 @@ from game.event import UnitsDeliveryEvent
|
||||
from game.theater import ControlPoint
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.QUnitInfoWindow import QUnitInfoWindow
|
||||
|
||||
|
||||
class QRecruitBehaviour:
|
||||
@@ -36,7 +38,6 @@ class QRecruitBehaviour:
|
||||
|
||||
@property
|
||||
def pending_deliveries(self) -> UnitsDeliveryEvent:
|
||||
assert self.cp.pending_unit_deliveries
|
||||
return self.cp.pending_unit_deliveries
|
||||
|
||||
@property
|
||||
@@ -59,7 +60,7 @@ class QRecruitBehaviour:
|
||||
existing_units = self.cp.base.total_units_of_type(unit_type)
|
||||
scheduled_units = self.pending_deliveries.units.get(unit_type, 0)
|
||||
|
||||
unitName = QLabel("<b>" + db.unit_type_name_2(unit_type) + "</b>")
|
||||
unitName = QLabel("<b>" + db.unit_get_expanded_info(self.game_model.game.player_country, unit_type, 'name') + "</b>")
|
||||
unitName.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
|
||||
|
||||
existing_units = QLabel(str(existing_units))
|
||||
@@ -97,6 +98,21 @@ class QRecruitBehaviour:
|
||||
sell.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
||||
sell.clicked.connect(lambda: self.sell(unit_type))
|
||||
|
||||
info = QGroupBox()
|
||||
info.setProperty("style", "buy-box")
|
||||
info.setMaximumHeight(36)
|
||||
info.setMinimumHeight(36)
|
||||
infolayout = QHBoxLayout()
|
||||
info.setLayout(infolayout)
|
||||
|
||||
unitInfo = QPushButton("i")
|
||||
unitInfo.setProperty("style", "btn-info")
|
||||
unitInfo.setDisabled(disabled)
|
||||
unitInfo.setMinimumSize(16, 16)
|
||||
unitInfo.setMaximumSize(16, 16)
|
||||
unitInfo.clicked.connect(lambda: self.info(unit_type))
|
||||
unitInfo.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
||||
|
||||
existLayout.addWidget(unitName)
|
||||
existLayout.addItem(QSpacerItem(20, 0, QSizePolicy.Minimum, QSizePolicy.Minimum))
|
||||
existLayout.addWidget(existing_units)
|
||||
@@ -107,8 +123,11 @@ class QRecruitBehaviour:
|
||||
buysellayout.addWidget(amount_bought)
|
||||
buysellayout.addWidget(buy)
|
||||
|
||||
infolayout.addWidget(unitInfo)
|
||||
|
||||
layout.addWidget(exist, row, 1)
|
||||
layout.addWidget(buysell, row, 2)
|
||||
layout.addWidget(info, row, 3)
|
||||
|
||||
return row + 1
|
||||
|
||||
@@ -128,7 +147,7 @@ class QRecruitBehaviour:
|
||||
def buy(self, unit_type: Type[UnitType]):
|
||||
price = db.PRICES[unit_type]
|
||||
if self.budget >= price:
|
||||
self.pending_deliveries.deliver({unit_type: 1})
|
||||
self.pending_deliveries.order({unit_type: 1})
|
||||
self.budget -= price
|
||||
else:
|
||||
# TODO : display modal warning
|
||||
@@ -137,20 +156,19 @@ class QRecruitBehaviour:
|
||||
self.update_available_budget()
|
||||
|
||||
def sell(self, unit_type):
|
||||
if self.pending_deliveries.units.get(unit_type, 0) > 0:
|
||||
if self.pending_deliveries.available_next_turn(unit_type) > 0:
|
||||
price = db.PRICES[unit_type]
|
||||
self.budget += price
|
||||
self.pending_deliveries.units[unit_type] = self.pending_deliveries.units[unit_type] - 1
|
||||
self.pending_deliveries.sell({unit_type: 1})
|
||||
if self.pending_deliveries.units[unit_type] == 0:
|
||||
del self.pending_deliveries.units[unit_type]
|
||||
elif self.cp.base.total_units_of_type(unit_type) > 0:
|
||||
price = db.PRICES[unit_type]
|
||||
self.budget += price
|
||||
self.cp.base.commit_losses({unit_type: 1})
|
||||
|
||||
self._update_count_label(unit_type)
|
||||
self.update_available_budget()
|
||||
|
||||
def info(self, unit_type):
|
||||
self.info_window = QUnitInfoWindow(self.game_model.game, unit_type)
|
||||
self.info_window.show()
|
||||
|
||||
def set_maximum_units(self, maximum_units):
|
||||
"""
|
||||
Set the maximum number of units that can be bought
|
||||
|
||||
@@ -65,7 +65,7 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
continue
|
||||
unit_types.add(unit)
|
||||
|
||||
sorted_units = sorted(unit_types, key=lambda u: db.unit_type_name_2(u))
|
||||
sorted_units = sorted(unit_types, key=lambda u: db.unit_get_expanded_info(self.game_model.game.player_country, u, 'name'))
|
||||
for unit_type in sorted_units:
|
||||
row = self.add_purchase_row(
|
||||
unit_type, task_box_layout, row,
|
||||
@@ -88,8 +88,17 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
if self.maximum_units > 0:
|
||||
if self.cp.unclaimed_parking(self.game_model.game) <= 0:
|
||||
logging.debug(f"No space for additional aircraft at {self.cp}.")
|
||||
QMessageBox.warning(
|
||||
self, "No space for additional aircraft",
|
||||
f"There is no parking space left at {self.cp.name} to accommodate another plane.", QMessageBox.Ok)
|
||||
return
|
||||
|
||||
# If we change our mind about selling, we want the aircraft to be put
|
||||
# back in the inventory immediately.
|
||||
elif self.pending_deliveries.units.get(unit_type, 0) < 0:
|
||||
global_inventory = self.game_model.game.aircraft_inventory
|
||||
inventory = global_inventory.for_control_point(self.cp)
|
||||
inventory.add_aircraft(unit_type, 1)
|
||||
|
||||
super().buy(unit_type)
|
||||
self.hangar_status.update_label()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ class QAirfieldCommand(QFrame):
|
||||
|
||||
def init_ui(self):
|
||||
layout = QGridLayout()
|
||||
layout.setHorizontalSpacing(1)
|
||||
layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game_model), 0, 0)
|
||||
|
||||
planned = QGroupBox("Planned Flights")
|
||||
|
||||
@@ -7,7 +7,8 @@ from PySide2.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from game.theater import Airport, ControlPoint
|
||||
from game.theater import Airport, ControlPoint, Fob
|
||||
from game.theater.theatergroundobject import BuildingGroundObject
|
||||
from qt_ui.windows.basemenu.base_defenses.QBaseDefenseGroupInfo import \
|
||||
QBaseDefenseGroupInfo
|
||||
|
||||
@@ -30,9 +31,18 @@ class QBaseInformation(QFrame):
|
||||
scroll_content.setLayout(task_box_layout)
|
||||
|
||||
for g in self.cp.ground_objects:
|
||||
if g.airbase_group and len(g.groups) > 0:
|
||||
group_info = QBaseDefenseGroupInfo(self.cp, g, self.game)
|
||||
task_box_layout.addWidget(group_info)
|
||||
# Airbase groups are the objects that are hidden on the map because
|
||||
# they're shown in the base menu.
|
||||
if not g.airbase_group:
|
||||
continue
|
||||
|
||||
# Of these, we need to ignore the FOB structure itself since that's
|
||||
# not supposed to be targetable.
|
||||
if isinstance(self.cp, Fob) and isinstance(g, BuildingGroundObject):
|
||||
continue
|
||||
|
||||
group_info = QBaseDefenseGroupInfo(self.cp, g, self.game)
|
||||
task_box_layout.addWidget(group_info)
|
||||
|
||||
scroll_content.setLayout(task_box_layout)
|
||||
scroll = QScrollArea()
|
||||
|
||||
@@ -5,8 +5,10 @@ from PySide2.QtWidgets import (
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
QMessageBox,
|
||||
)
|
||||
from dcs.task import PinpointStrike
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.theater import ControlPoint
|
||||
@@ -42,7 +44,7 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
for task_type in units.keys():
|
||||
units_column = list(set(units[task_type]))
|
||||
if len(units_column) == 0: continue
|
||||
units_column.sort(key=lambda x: db.PRICES[x])
|
||||
units_column.sort(key=lambda u: db.unit_get_expanded_info(self.game_model.game.player_country, u, 'name'))
|
||||
for unit_type in units_column:
|
||||
row = self.add_purchase_row(unit_type, task_box_layout, row)
|
||||
stretch = QVBoxLayout()
|
||||
@@ -57,3 +59,12 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
scroll.setWidget(scroll_content)
|
||||
main_layout.addWidget(scroll)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
def sell(self, unit_type: UnitType):
|
||||
if self.pending_deliveries.available_next_turn(unit_type) <= 0:
|
||||
QMessageBox.critical(
|
||||
self, "Could not sell ground unit",
|
||||
f"Attempted to sell one {unit_type.id} at {self.cp.name} "
|
||||
"but none are available.", QMessageBox.Ok)
|
||||
return
|
||||
super().sell(unit_type)
|
||||
@@ -4,7 +4,10 @@ from PySide2.QtWidgets import (
|
||||
QGroupBox,
|
||||
QLabel,
|
||||
QVBoxLayout,
|
||||
QScrollArea,
|
||||
QWidget,
|
||||
)
|
||||
from PySide2.QtCore import Qt
|
||||
from dcs.task import CAP, CAS, Embarking, PinpointStrike
|
||||
|
||||
from game import Game, db
|
||||
@@ -21,10 +24,11 @@ class QIntelInfo(QFrame):
|
||||
|
||||
def init_ui(self):
|
||||
layout = QVBoxLayout()
|
||||
|
||||
intel = QGroupBox("Intel")
|
||||
scroll_content = QWidget()
|
||||
intelLayout = QVBoxLayout()
|
||||
|
||||
|
||||
|
||||
units = {
|
||||
CAP: db.find_unittype(CAP, self.game.enemy_name),
|
||||
Embarking: db.find_unittype(Embarking, self.game.enemy_name),
|
||||
@@ -46,14 +50,19 @@ class QIntelInfo(QFrame):
|
||||
existing_units = self.cp.base.total_units_of_type(unit_type)
|
||||
if existing_units == 0:
|
||||
continue
|
||||
groupLayout.addWidget(QLabel("<b>" + db.unit_type_name(unit_type) + "</b>"), row, 0)
|
||||
groupLayout.addWidget(QLabel("<b>" + db.unit_get_expanded_info(self.game.enemy_country, unit_type, 'name') + "</b>"), row, 0)
|
||||
groupLayout.addWidget(QLabel(str(existing_units)), row, 1)
|
||||
row += 1
|
||||
|
||||
intelLayout.addWidget(group)
|
||||
|
||||
intelLayout.addStretch()
|
||||
intel.setLayout(intelLayout)
|
||||
layout.addWidget(intel)
|
||||
layout.addStretch()
|
||||
scroll_content.setLayout(intelLayout)
|
||||
scroll = QScrollArea()
|
||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setWidget(scroll_content)
|
||||
|
||||
layout.addWidget(scroll)
|
||||
|
||||
self.setLayout(layout)
|
||||
@@ -1,19 +1,94 @@
|
||||
from PySide2.QtWidgets import QDialog, QGridLayout, QLabel, QFrame, QSizePolicy
|
||||
import itertools
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtWidgets import (
|
||||
QDialog,
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QLabel,
|
||||
QSizePolicy,
|
||||
)
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game.db import REWARDS, PLAYER_BUDGET_BASE
|
||||
from game.game import Game
|
||||
from game.income import BuildingIncome, Income
|
||||
from game.theater import ControlPoint
|
||||
|
||||
|
||||
class QHorizontalSeparationLine(QFrame):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setMinimumWidth(1)
|
||||
self.setFixedHeight(20)
|
||||
self.setFrameShape(QFrame.HLine)
|
||||
self.setFrameShadow(QFrame.Sunken)
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setMinimumWidth(1)
|
||||
self.setFixedHeight(20)
|
||||
self.setFrameShape(QFrame.HLine)
|
||||
self.setFrameShadow(QFrame.Sunken)
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
|
||||
|
||||
|
||||
class FinancesLayout(QGridLayout):
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
super().__init__()
|
||||
self.row = itertools.count(0)
|
||||
|
||||
income = Income(game, player)
|
||||
|
||||
control_points = reversed(
|
||||
sorted(income.control_points, key=lambda c: c.income_per_turn))
|
||||
for control_point in control_points:
|
||||
self.add_control_point(control_point)
|
||||
|
||||
self.add_line()
|
||||
|
||||
buildings = reversed(sorted(income.buildings, key=lambda b: b.income))
|
||||
for building in buildings:
|
||||
self.add_building(building)
|
||||
|
||||
self.add_line()
|
||||
|
||||
self.add_row(middle=f"Income multiplier: {income.multiplier:.1f}",
|
||||
right=f"<b>{income.total}M</b>")
|
||||
|
||||
if player:
|
||||
budget = game.budget
|
||||
else:
|
||||
budget = game.enemy_budget
|
||||
|
||||
self.add_row(middle="Balance", right=f"<b>{budget}M</b>")
|
||||
self.setRowStretch(next(self.row), 1)
|
||||
|
||||
def add_row(self, left: Optional[str] = None, middle: Optional[str] = None,
|
||||
right: Optional[str] = None) -> None:
|
||||
if not any([left, middle, right]):
|
||||
raise ValueError
|
||||
|
||||
row = next(self.row)
|
||||
if left is not None:
|
||||
self.addWidget(QLabel(left), row, 0)
|
||||
if middle is not None:
|
||||
self.addWidget(QLabel(middle), row, 1)
|
||||
if right is not None:
|
||||
self.addWidget(QLabel(right), row, 2)
|
||||
|
||||
def add_control_point(self, control_point: ControlPoint) -> None:
|
||||
self.add_row(left=f"<b>{control_point.name}</b>",
|
||||
right=f"{control_point.income_per_turn}M")
|
||||
|
||||
def add_building(self, building: BuildingIncome) -> None:
|
||||
row = next(self.row)
|
||||
self.addWidget(
|
||||
QLabel(f"<b>{building.category.upper()} [{building.name}]</b>"),
|
||||
row, 0)
|
||||
self.addWidget(QLabel(
|
||||
f"{building.number} buildings x {building.income_per_building}M"),
|
||||
row, 1)
|
||||
rlabel = QLabel(f"{building.income}M")
|
||||
rlabel.setProperty("style", "green")
|
||||
self.addWidget(rlabel, row, 2)
|
||||
|
||||
def add_line(self) -> None:
|
||||
self.addWidget(QHorizontalSeparationLine(), next(self.row), 0, 1, 3)
|
||||
|
||||
|
||||
class QFinancesMenu(QDialog):
|
||||
|
||||
@@ -26,49 +101,4 @@ class QFinancesMenu(QDialog):
|
||||
self.setWindowIcon(CONST.ICONS["Money"])
|
||||
self.setMinimumSize(450, 200)
|
||||
|
||||
reward = PLAYER_BUDGET_BASE * len(self.game.theater.player_points())
|
||||
layout = QGridLayout()
|
||||
layout.addWidget(QLabel("<b>Control Points</b>"), 0, 0)
|
||||
layout.addWidget(QLabel(str(len(self.game.theater.player_points())) + " bases x " + str(PLAYER_BUDGET_BASE) + "M"), 0, 1)
|
||||
layout.addWidget(QLabel(str(reward) + "M"), 0, 2)
|
||||
|
||||
layout.addWidget(QHorizontalSeparationLine(), 1, 0, 1, 3)
|
||||
|
||||
i = 2
|
||||
for cp in self.game.theater.player_points():
|
||||
obj_names = []
|
||||
[obj_names.append(ground_object.obj_name) for ground_object in cp.ground_objects if ground_object.obj_name not in obj_names]
|
||||
for obj_name in obj_names:
|
||||
reward = 0
|
||||
g = None
|
||||
cat = None
|
||||
number = 0
|
||||
for ground_object in cp.ground_objects:
|
||||
if ground_object.obj_name != obj_name or ground_object.is_dead:
|
||||
continue
|
||||
else:
|
||||
if g is None:
|
||||
g = ground_object
|
||||
cat = g.category
|
||||
if cat in REWARDS.keys():
|
||||
number = number + 1
|
||||
reward += REWARDS[cat]
|
||||
|
||||
if g is not None and cat in REWARDS.keys():
|
||||
layout.addWidget(QLabel("<b>" + g.category.upper() + " [" + obj_name + "]</b>"), i, 0)
|
||||
layout.addWidget(QLabel(str(number) + " buildings x " + str(REWARDS[cat]) + "M"), i, 1)
|
||||
rlabel = QLabel(str(reward) + "M")
|
||||
rlabel.setProperty("style", "green")
|
||||
layout.addWidget(rlabel, i, 2)
|
||||
i = i + 1
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
layout.addWidget(QHorizontalSeparationLine(), i + 1, 0, 1, 3)
|
||||
layout.addWidget(QLabel(
|
||||
f"Income multiplier: {game.settings.player_income_multiplier:.1f}"),
|
||||
i + 2, 1
|
||||
)
|
||||
layout.addWidget(
|
||||
QLabel("<b>" + str(self.game.budget_reward_amount) + "M </b>"),
|
||||
i + 2, 2)
|
||||
self.setLayout(FinancesLayout(game, player=True))
|
||||
|
||||
@@ -21,6 +21,7 @@ from game import Game, db
|
||||
from game.data.building_data import FORTIFICATION_BUILDINGS
|
||||
from game.db import PRICES, PinpointStrike, REWARDS, unit_type_of
|
||||
from game.theater import ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import NavalGroundObject
|
||||
from gen.defenses.armor_group_generator import \
|
||||
generate_armor_group_of_type_and_size
|
||||
from gen.sam.sam_group_generator import get_faction_possible_sams_generator
|
||||
@@ -81,9 +82,10 @@ class QGroundObjectMenu(QDialog):
|
||||
self.buy_replace.clicked.connect(self.buy_group)
|
||||
self.buy_replace.setProperty("style", "btn-success")
|
||||
|
||||
if self.total_value > 0:
|
||||
self.actionLayout.addWidget(self.sell_all_button)
|
||||
self.actionLayout.addWidget(self.buy_replace)
|
||||
if not isinstance(self.ground_object, NavalGroundObject):
|
||||
if self.total_value > 0:
|
||||
self.actionLayout.addWidget(self.sell_all_button)
|
||||
self.actionLayout.addWidget(self.buy_replace)
|
||||
|
||||
if self.cp.captured and self.ground_object.dcs_identifier == "AA":
|
||||
self.mainLayout.addLayout(self.actionLayout)
|
||||
@@ -355,8 +357,7 @@ class QBuyGroupForGroundObjectDialog(QDialog):
|
||||
# Generate SAM
|
||||
generator = sam_generator(self.game, self.ground_object)
|
||||
generator.generate()
|
||||
generated_group = generator.get_generated_group()
|
||||
self.ground_object.groups = [generated_group]
|
||||
self.ground_object.groups = list(generator.groups)
|
||||
|
||||
GameUpdateSignal.get_instance().updateBudget(self.game)
|
||||
|
||||
|
||||
147
qt_ui/windows/intel.py
Normal file
147
qt_ui/windows/intel.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import itertools
|
||||
|
||||
from PySide2.QtWidgets import (
|
||||
QDialog,
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QLabel,
|
||||
QLayout,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QSpacerItem,
|
||||
QTabWidget,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from game.game import Game, db
|
||||
from qt_ui.uiconstants import ICONS
|
||||
from qt_ui.windows.finances.QFinancesMenu import FinancesLayout
|
||||
|
||||
|
||||
class ScrollingFrame(QFrame):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
widget = QWidget()
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setWidget(widget)
|
||||
|
||||
self.scrolling_layout = QVBoxLayout()
|
||||
widget.setLayout(self.scrolling_layout)
|
||||
|
||||
self.setLayout(QVBoxLayout())
|
||||
self.layout().addWidget(scroll_area)
|
||||
|
||||
def addWidget(self, widget: QWidget, *args, **kwargs) -> None:
|
||||
self.scrolling_layout.addWidget(widget, *args, **kwargs)
|
||||
|
||||
def addLayout(self, layout: QLayout, *args, **kwargs) -> None:
|
||||
self.scrolling_layout.addLayout(layout, *args, **kwargs)
|
||||
|
||||
|
||||
class EconomyIntelTab(ScrollingFrame):
|
||||
def __init__(self, game: Game) -> None:
|
||||
super().__init__()
|
||||
self.addLayout(FinancesLayout(game, player=False))
|
||||
|
||||
|
||||
class IntelTableLayout(QGridLayout):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.row = itertools.count(0)
|
||||
|
||||
def add_header(self, text: str) -> None:
|
||||
self.addWidget(QLabel(f"<b>{text}</b>"), next(self.row), 0)
|
||||
|
||||
def add_spacer(self) -> None:
|
||||
self.addItem(
|
||||
QSpacerItem(0, 0, QSizePolicy.Preferred, QSizePolicy.Expanding),
|
||||
next(self.row), 0)
|
||||
|
||||
def add_row(self, text: str, count: int) -> None:
|
||||
row = next(self.row)
|
||||
self.addWidget(QLabel(text), row, 0)
|
||||
self.addWidget(QLabel(str(count)), row, 1)
|
||||
|
||||
|
||||
class AircraftIntelLayout(IntelTableLayout):
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
super().__init__()
|
||||
|
||||
total = 0
|
||||
for control_point in game.theater.control_points_for(player):
|
||||
base = control_point.base
|
||||
total += base.total_aircraft
|
||||
if not base.total_aircraft:
|
||||
continue
|
||||
|
||||
self.add_header(control_point.name)
|
||||
for airframe, count in base.aircraft.items():
|
||||
if not count:
|
||||
continue
|
||||
self.add_row(db.unit_get_expanded_info(game.enemy_country, airframe, 'name'), count)
|
||||
|
||||
self.add_spacer()
|
||||
self.add_row("<b>Total</b>", total)
|
||||
|
||||
|
||||
class AircraftIntelTab(ScrollingFrame):
|
||||
def __init__(self, game: Game) -> None:
|
||||
super().__init__()
|
||||
self.addLayout(AircraftIntelLayout(game, player=False))
|
||||
|
||||
|
||||
class ArmyIntelLayout(IntelTableLayout):
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
super().__init__()
|
||||
|
||||
total = 0
|
||||
for control_point in game.theater.control_points_for(player):
|
||||
base = control_point.base
|
||||
total += base.total_armor
|
||||
if not base.total_armor:
|
||||
continue
|
||||
|
||||
self.add_header(control_point.name)
|
||||
for vehicle, count in base.armor.items():
|
||||
if not count:
|
||||
continue
|
||||
self.add_row(vehicle.id, count)
|
||||
|
||||
self.add_spacer()
|
||||
self.add_row("<b>Total</b>", total)
|
||||
|
||||
|
||||
class ArmyIntelTab(ScrollingFrame):
|
||||
def __init__(self, game: Game) -> None:
|
||||
super().__init__()
|
||||
self.addLayout(ArmyIntelLayout(game, player=False))
|
||||
|
||||
|
||||
class IntelTabs(QTabWidget):
|
||||
|
||||
def __init__(self, game: Game):
|
||||
super().__init__()
|
||||
|
||||
self.addTab(EconomyIntelTab(game), "Economy")
|
||||
self.addTab(AircraftIntelTab(game), "Air forces")
|
||||
self.addTab(ArmyIntelTab(game), "Ground forces")
|
||||
|
||||
|
||||
class IntelWindow(QDialog):
|
||||
|
||||
def __init__(self, game: Game):
|
||||
super().__init__()
|
||||
|
||||
self.game = game
|
||||
self.setModal(True)
|
||||
self.setWindowTitle("Intelligence")
|
||||
self.setWindowIcon(ICONS["Statistics"])
|
||||
self.setMinimumSize(600, 500)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
layout.addWidget(IntelTabs(game), stretch=1)
|
||||
@@ -4,9 +4,8 @@ from PySide2.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.models import GameModel, PackageModel
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
|
||||
@@ -15,17 +14,19 @@ from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
|
||||
class QEditFlightDialog(QDialog):
|
||||
"""Dialog window for editing flight plans and loadouts."""
|
||||
|
||||
def __init__(self, game_model: GameModel, package: Package, flight: Flight, parent=None) -> None:
|
||||
def __init__(self, game_model: GameModel, package_model: PackageModel,
|
||||
flight: Flight, parent=None) -> None:
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.game_model = game_model
|
||||
|
||||
self.setWindowTitle("Create flight")
|
||||
self.setWindowTitle("Edit flight")
|
||||
self.setWindowIcon(EVENT_ICONS["strike"])
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
self.flight_planner = QFlightPlanner(package, flight, game_model.game)
|
||||
self.flight_planner = QFlightPlanner(package_model, flight,
|
||||
game_model.game)
|
||||
layout.addWidget(self.flight_planner)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
@@ -3,8 +3,9 @@ import logging
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import QItemSelection, QTime, Signal
|
||||
from PySide2.QtCore import QItemSelection, QTime, Qt, Signal
|
||||
from PySide2.QtWidgets import (
|
||||
QCheckBox,
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
@@ -15,16 +16,15 @@ from PySide2.QtWidgets import (
|
||||
)
|
||||
|
||||
from game.game import Game
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.flightplan import FlightPlanBuilder, PlanningError
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.models import AtoModel, GameModel, PackageModel
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.widgets.ato import QFlightList
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
|
||||
|
||||
class QPackageDialog(QDialog):
|
||||
@@ -78,15 +78,23 @@ class QPackageDialog(QDialog):
|
||||
self.tot_spinner.setMinimumTime(QTime(0, 0))
|
||||
self.tot_spinner.setDisplayFormat("T+hh:mm:ss")
|
||||
self.tot_spinner.timeChanged.connect(self.save_tot)
|
||||
self.tot_spinner.setToolTip("Package TOT relative to mission TOT")
|
||||
self.tot_spinner.setEnabled(not self.package_model.package.auto_asap)
|
||||
self.tot_column.addWidget(self.tot_spinner)
|
||||
|
||||
self.reset_tot_button = QPushButton("ASAP")
|
||||
self.reset_tot_button.setToolTip(
|
||||
self.auto_asap = QCheckBox("ASAP")
|
||||
self.auto_asap.setToolTip(
|
||||
"Sets the package TOT to the earliest time that all flights can "
|
||||
"arrive at the target."
|
||||
)
|
||||
self.reset_tot_button.clicked.connect(self.reset_tot)
|
||||
self.tot_column.addWidget(self.reset_tot_button)
|
||||
self.auto_asap.setChecked(self.package_model.package.auto_asap)
|
||||
self.auto_asap.toggled.connect(self.set_asap)
|
||||
self.tot_column.addWidget(self.auto_asap)
|
||||
|
||||
self.tot_help_label = QLabel("<a href=\"https://github.com/Khopa/dcs_liberation/wiki/Mission-planning\"><span style=\"color:#FFFFFF;\">Help</span></a>")
|
||||
self.tot_help_label.setAlignment(Qt.AlignCenter)
|
||||
self.tot_help_label.setOpenExternalLinks(True)
|
||||
self.tot_column.addWidget(self.tot_help_label)
|
||||
|
||||
self.package_view = QFlightList(self.game_model, self.package_model)
|
||||
self.package_view.selectionModel().selectionChanged.connect(
|
||||
@@ -107,6 +115,8 @@ class QPackageDialog(QDialog):
|
||||
self.delete_flight_button.setEnabled(model.rowCount() > 0)
|
||||
self.button_layout.addWidget(self.delete_flight_button)
|
||||
|
||||
self.package_model.tot_changed.connect(self.update_tot)
|
||||
|
||||
self.button_layout.addStretch()
|
||||
|
||||
self.setLayout(self.layout)
|
||||
@@ -139,14 +149,14 @@ class QPackageDialog(QDialog):
|
||||
def save_tot(self) -> None:
|
||||
time = self.tot_spinner.time()
|
||||
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
|
||||
self.package_model.update_tot(timedelta(seconds=seconds))
|
||||
self.package_model.set_tot(timedelta(seconds=seconds))
|
||||
|
||||
def reset_tot(self) -> None:
|
||||
if not list(self.package_model.flights):
|
||||
self.package_model.update_tot(timedelta())
|
||||
else:
|
||||
self.package_model.update_tot(
|
||||
TotEstimator(self.package_model.package).earliest_tot())
|
||||
def set_asap(self, checked: bool) -> None:
|
||||
self.package_model.set_asap(checked)
|
||||
self.tot_spinner.setEnabled(not self.package_model.package.auto_asap)
|
||||
self.update_tot()
|
||||
|
||||
def update_tot(self) -> None:
|
||||
self.tot_spinner.setTime(self.tot_qtime())
|
||||
|
||||
def on_selection_changed(self, selected: QItemSelection,
|
||||
@@ -177,6 +187,7 @@ class QPackageDialog(QDialog):
|
||||
QMessageBox.critical(
|
||||
self, "Could not create flight", str(ex), QMessageBox.Ok
|
||||
)
|
||||
self.package_model.update_tot()
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.package_changed.emit()
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ from typing import Optional
|
||||
|
||||
from PySide2.QtCore import Qt, Signal
|
||||
from PySide2.QtWidgets import (
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QLineEdit,
|
||||
)
|
||||
from dcs.planes import PlaneType
|
||||
|
||||
@@ -31,6 +34,8 @@ class QFlightCreator(QDialog):
|
||||
|
||||
self.game = game
|
||||
self.package = package
|
||||
self.custom_name_text = None
|
||||
self.country = self.game.player_country
|
||||
|
||||
self.setWindowTitle("Create flight")
|
||||
self.setWindowIcon(EVENT_ICONS["strike"])
|
||||
@@ -41,10 +46,12 @@ class QFlightCreator(QDialog):
|
||||
self.game.theater, package.target
|
||||
)
|
||||
self.task_selector.setCurrentIndex(0)
|
||||
self.task_selector.currentTextChanged.connect(
|
||||
self.on_task_changed)
|
||||
layout.addLayout(QLabeledWidget("Task:", self.task_selector))
|
||||
|
||||
self.aircraft_selector = QAircraftTypeSelector(
|
||||
self.game.aircraft_inventory.available_types_for_player
|
||||
self.game.aircraft_inventory.available_types_for_player, self.game.player_country, self.task_selector.currentData()
|
||||
)
|
||||
self.aircraft_selector.setCurrentIndex(0)
|
||||
self.aircraft_selector.currentIndexChanged.connect(
|
||||
@@ -57,6 +64,7 @@ class QFlightCreator(QDialog):
|
||||
self.aircraft_selector.currentData()
|
||||
)
|
||||
self.departure.availability_changed.connect(self.update_max_size)
|
||||
self.departure.currentIndexChanged.connect(self.on_departure_changed)
|
||||
layout.addLayout(QLabeledWidget("Departure:", self.departure))
|
||||
|
||||
self.arrival = QArrivalAirfieldSelector(
|
||||
@@ -88,6 +96,28 @@ class QFlightCreator(QDialog):
|
||||
layout.addLayout(
|
||||
QLabeledWidget("Client Slots:", self.client_slots_spinner))
|
||||
|
||||
# When an off-map spawn overrides the start type to in-flight, we save
|
||||
# the selected type into this value. If a non-off-map spawn is selected
|
||||
# we restore the previous choice.
|
||||
self.restore_start_type: Optional[str] = None
|
||||
self.start_type = QComboBox()
|
||||
self.start_type.addItems(["Cold", "Warm", "Runway", "In Flight"])
|
||||
self.start_type.setCurrentText(self.game.settings.default_start_type)
|
||||
layout.addLayout(QLabeledWidget(
|
||||
"Start type:", self.start_type,
|
||||
tooltip="Selects the start type for this flight."))
|
||||
layout.addWidget(QLabel(
|
||||
"Any option other than Cold will make this flight " +
|
||||
"non-targetable<br />by OCA/Aircraft missions. This will affect " +
|
||||
"game balance."
|
||||
))
|
||||
|
||||
self.custom_name = QLineEdit()
|
||||
self.custom_name.textChanged.connect(self.set_custom_name_text)
|
||||
layout.addLayout(
|
||||
QLabeledWidget("Custom Flight Name (Optional)", self.custom_name)
|
||||
)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
self.create_button = QPushButton("Create")
|
||||
@@ -96,12 +126,19 @@ class QFlightCreator(QDialog):
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
self.on_departure_changed(self.departure.currentIndex())
|
||||
|
||||
def set_custom_name_text(self, text: str):
|
||||
self.custom_name_text = text
|
||||
|
||||
def verify_form(self) -> Optional[str]:
|
||||
aircraft: PlaneType = self.aircraft_selector.currentData()
|
||||
origin: ControlPoint = self.departure.currentData()
|
||||
arrival: ControlPoint = self.arrival.currentData()
|
||||
divert: ControlPoint = self.divert.currentData()
|
||||
size: int = self.flight_size_spinner.value()
|
||||
if aircraft is None:
|
||||
return "You must select an aircraft type."
|
||||
if not origin.captured:
|
||||
return f"{origin.name} is not owned by your coalition."
|
||||
if arrival is not None and not arrival.captured:
|
||||
@@ -115,6 +152,8 @@ class QFlightCreator(QDialog):
|
||||
return f"{origin.name} has only {available} {aircraft.id} available."
|
||||
if size <= 0:
|
||||
return f"Flight must have at least one aircraft."
|
||||
if self.custom_name_text and "|" in self.custom_name_text:
|
||||
return f"Cannot include | in flight name"
|
||||
return None
|
||||
|
||||
def create_flight(self) -> None:
|
||||
@@ -134,14 +173,9 @@ class QFlightCreator(QDialog):
|
||||
if arrival is None:
|
||||
arrival = origin
|
||||
|
||||
if isinstance(origin, OffMapSpawn):
|
||||
start_type = "In Flight"
|
||||
elif self.game.settings.perf_ai_parking_start:
|
||||
start_type = "Cold"
|
||||
else:
|
||||
start_type = "Warm"
|
||||
flight = Flight(self.package, aircraft, size, task, start_type, origin,
|
||||
arrival, divert)
|
||||
flight = Flight(self.package, self.country, aircraft, size, task,
|
||||
self.start_type.currentText(), origin, arrival, divert,
|
||||
custom_name=self.custom_name_text)
|
||||
flight.client_count = self.client_slots_spinner.value()
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
@@ -154,6 +188,23 @@ class QFlightCreator(QDialog):
|
||||
self.arrival.change_aircraft(new_aircraft)
|
||||
self.divert.change_aircraft(new_aircraft)
|
||||
|
||||
def on_departure_changed(self, index: int) -> None:
|
||||
departure = self.departure.itemData(index)
|
||||
if isinstance(departure, OffMapSpawn):
|
||||
previous_type = self.start_type.currentText()
|
||||
if previous_type != "In Flight":
|
||||
self.restore_start_type = previous_type
|
||||
self.start_type.setCurrentText("In Flight")
|
||||
self.start_type.setEnabled(False)
|
||||
else:
|
||||
self.start_type.setEnabled(True)
|
||||
if self.restore_start_type is not None:
|
||||
self.start_type.setCurrentText(self.restore_start_type)
|
||||
self.restore_start_type = None
|
||||
|
||||
def on_task_changed(self) -> None:
|
||||
self.aircraft_selector.updateItems(self.task_selector.currentData(), self.game.aircraft_inventory.available_types_for_player)
|
||||
|
||||
def update_max_size(self, available: int) -> None:
|
||||
self.flight_size_spinner.setMaximum(min(available, 4))
|
||||
if self.flight_size_spinner.maximum() >= 2:
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from PySide2.QtCore import Signal
|
||||
from PySide2.QtWidgets import QTabWidget
|
||||
|
||||
from game import Game
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.models import PackageModel
|
||||
from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import \
|
||||
QFlightPayloadTab
|
||||
from qt_ui.windows.mission.flight.settings.QGeneralFlightSettingsTab import \
|
||||
@@ -14,22 +13,15 @@ from qt_ui.windows.mission.flight.waypoints.QFlightWaypointTab import \
|
||||
|
||||
class QFlightPlanner(QTabWidget):
|
||||
|
||||
on_planned_flight_changed = Signal()
|
||||
|
||||
def __init__(self, package: Package, flight: Flight, game: Game):
|
||||
def __init__(self, package_model: PackageModel, flight: Flight, game: Game):
|
||||
super().__init__()
|
||||
|
||||
self.general_settings_tab = QGeneralFlightSettingsTab(
|
||||
game, package, flight
|
||||
game, package_model, flight
|
||||
)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.general_settings_tab.on_flight_settings_changed.connect(
|
||||
lambda: self.on_planned_flight_changed.emit())
|
||||
self.payload_tab = QFlightPayloadTab(flight, game)
|
||||
self.waypoint_tab = QFlightWaypointTab(game, package, flight)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.waypoint_tab.on_flight_changed.connect(
|
||||
lambda: self.on_planned_flight_changed.emit())
|
||||
self.waypoint_tab = QFlightWaypointTab(game, package_model.package,
|
||||
flight)
|
||||
self.addTab(self.general_settings_tab, "General Flight settings")
|
||||
self.addTab(self.payload_tab, "Payload")
|
||||
self.addTab(self.waypoint_tab, "Waypoints")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user