mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Compare commits
347 Commits
main
...
test/1.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f962032dc8 | ||
|
|
f498c08dc9 | ||
|
|
f461a46cdd | ||
|
|
f9d27f810b | ||
|
|
b49c99d5cf | ||
|
|
f05a6888d3 | ||
|
|
0413474576 | ||
|
|
b4277c25c5 | ||
|
|
d4efd72f6d | ||
|
|
3060645a26 | ||
|
|
0c6c578e35 | ||
|
|
e08f134c32 | ||
|
|
23038671b3 | ||
|
|
ae24b5e1ea | ||
|
|
918903f831 | ||
|
|
f3241c286c | ||
|
|
e682176040 | ||
|
|
8bea0c5cf8 | ||
|
|
497fd56efc | ||
|
|
100f4b66fd | ||
|
|
e2797fd796 | ||
|
|
e827d6d1d0 | ||
|
|
b9609a00f6 | ||
|
|
aaa06a5e88 | ||
|
|
26851520c8 | ||
|
|
cf0b5dc931 | ||
|
|
b2592b506f | ||
|
|
e84551b301 | ||
|
|
cebcee8d32 | ||
|
|
d75c6d7e9a | ||
|
|
eed514a432 | ||
|
|
218e783bb1 | ||
|
|
e1e1689d23 | ||
|
|
5c7a87dbfd | ||
|
|
5e7f5a68c2 | ||
|
|
5b19e0fdcd | ||
|
|
648fad33d8 | ||
|
|
843bb30b99 | ||
|
|
0f093d5f54 | ||
|
|
3a09a15970 | ||
|
|
83acf8b93d | ||
|
|
1793703e47 | ||
|
|
6713ee155c | ||
|
|
17f8830fbf | ||
|
|
24d995828f | ||
|
|
ae379bd8f5 | ||
|
|
f2cebc82cc | ||
|
|
2fa2e16b21 | ||
|
|
f670ccd8a6 | ||
|
|
9ffaec6d16 | ||
|
|
60de35ddf3 | ||
|
|
41faf794cc | ||
|
|
be37b4b3e7 | ||
|
|
3598b07185 | ||
|
|
602f88dab9 | ||
|
|
a4f5c290b5 | ||
|
|
8e42df11f3 | ||
|
|
8c6daa0976 | ||
|
|
bb629a7be2 | ||
|
|
80a7d7da2d | ||
|
|
c45795434a | ||
|
|
04971f3f65 | ||
|
|
6bf5356d00 | ||
|
|
0cd7db633b | ||
|
|
d9967bda8f | ||
|
|
34b9d8c334 | ||
|
|
12ff6679e4 | ||
|
|
4f9a250a9e | ||
|
|
c622cacadc | ||
|
|
786fb4e1c0 | ||
|
|
75bc521fa5 | ||
|
|
71296f9180 | ||
|
|
86672d882e | ||
|
|
3fc82af3ac | ||
|
|
22835e7535 | ||
|
|
f979a0c23d | ||
|
|
5f1f2b34b7 | ||
|
|
9ab2fc1714 | ||
|
|
b25f484ef3 | ||
|
|
dd0b565de9 | ||
|
|
29d88fd61d | ||
|
|
7625725fd8 | ||
|
|
cef25bd2f3 | ||
|
|
1e9cee1003 | ||
|
|
15bcc2d947 | ||
|
|
763dbebf13 | ||
|
|
667addf25b | ||
|
|
1033923f68 | ||
|
|
d410f9d3c4 | ||
|
|
a1cc3412e8 | ||
|
|
a98c9ffe8d | ||
|
|
5d506ca97e | ||
|
|
51c0bfa970 | ||
|
|
5c8e783980 | ||
|
|
b7d95819c1 | ||
|
|
bb0bf66eac | ||
|
|
be7008722b | ||
|
|
5535282894 | ||
|
|
2fb5f2b738 | ||
|
|
e87621576d | ||
|
|
531625ff08 | ||
|
|
0d57f710aa | ||
|
|
0da431fe33 | ||
|
|
88f984b0a8 | ||
|
|
ddb9d6968b | ||
|
|
3313535b27 | ||
|
|
cccc6e5a44 | ||
|
|
8b802a9cbc | ||
|
|
0a3a56d4bc | ||
|
|
7bae6cd562 | ||
|
|
e8151678dd | ||
|
|
9dd6022907 | ||
|
|
48e0d579cf | ||
|
|
cc9f61005b | ||
|
|
48ba8ff255 | ||
|
|
3f570cce5e | ||
|
|
01ee24c8cf | ||
|
|
1deff6fa49 | ||
|
|
4404d31c7b | ||
|
|
989562b90f | ||
|
|
bd63e4484d | ||
|
|
df642fe7a1 | ||
|
|
cb8f2619f2 | ||
|
|
2aed461b3a | ||
|
|
f4d75a2c6f | ||
|
|
e36d51a7c2 | ||
|
|
7267b70066 | ||
|
|
62d1a89269 | ||
|
|
6d0ac603f2 | ||
|
|
37f2e5bf3a | ||
|
|
9ddaf50ad4 | ||
|
|
42f488df0b | ||
|
|
d2a7de2775 | ||
|
|
ee1fc53dd3 | ||
|
|
192741af36 | ||
|
|
cd4ace4ad5 | ||
|
|
39426dde45 | ||
|
|
bd4eb3e105 | ||
|
|
c37c56c879 | ||
|
|
d26fc84316 | ||
|
|
33586aaf4f | ||
|
|
650b06d7c8 | ||
|
|
09807b25a8 | ||
|
|
13d42c3f83 | ||
|
|
6966b14c6b | ||
|
|
00ab0c4be2 | ||
|
|
5c06e74659 | ||
|
|
b0a0050725 | ||
|
|
c5a7fbd15a | ||
|
|
fa093ea237 | ||
|
|
782759bd48 | ||
|
|
653e507e89 | ||
|
|
4f9c7a7370 | ||
|
|
c114256b82 | ||
|
|
cdd72820b4 | ||
|
|
4b8e7e972e | ||
|
|
68b04b0e46 | ||
|
|
d00b7a176e | ||
|
|
3c3abd484f | ||
|
|
9d5eece5a5 | ||
|
|
f08df08c7e | ||
|
|
44e5bc646d | ||
|
|
74eb8f38f9 | ||
|
|
5ff8fb170d | ||
|
|
39a5b486d3 | ||
|
|
6664a139d8 | ||
|
|
d5de5b3a78 | ||
|
|
c3024e38a6 | ||
|
|
cfd05400d2 | ||
|
|
64992a2949 | ||
|
|
8ea7656362 | ||
|
|
cd760c45a8 | ||
|
|
15f6c8dd85 | ||
|
|
7d2dd0bcbe | ||
|
|
608525d5b4 | ||
|
|
49d4d508d4 | ||
|
|
0e286ab7b5 | ||
|
|
4e27591afa | ||
|
|
01c5f3e2a9 | ||
|
|
436d2f87da | ||
|
|
2e2a397312 | ||
|
|
54a5461ecb | ||
|
|
49ddd5ce96 | ||
|
|
e9c1e7b770 | ||
|
|
c298e5bc24 | ||
|
|
48e5e7ca95 | ||
|
|
55fb5befb9 | ||
|
|
22c7e21a66 | ||
|
|
a88f48f3ce | ||
|
|
6c673697f0 | ||
|
|
4af7255917 | ||
|
|
dfa33724e9 | ||
|
|
568fd35ddc | ||
|
|
bc674edb3a | ||
|
|
f5c7935993 | ||
|
|
0c30ce3bb3 | ||
|
|
e38a6cd907 | ||
|
|
dd98a1d078 | ||
|
|
c9611cca99 | ||
|
|
8a17184ccc | ||
|
|
e5b193f21a | ||
|
|
b9c62bc44a | ||
|
|
243ded448d | ||
|
|
ae561e5564 | ||
|
|
5ffb8cbd96 | ||
|
|
07d0e555e5 | ||
|
|
e5c80ef3af | ||
|
|
11cc62d9a9 | ||
|
|
8c9f07643f | ||
|
|
85607b97d5 | ||
|
|
0bc67adea8 | ||
|
|
1cfab07b8b | ||
|
|
499e63e2c3 | ||
|
|
bc8624d605 | ||
|
|
11a3c49464 | ||
|
|
23becb49b2 | ||
|
|
72b594934e | ||
|
|
d535d312a5 | ||
|
|
4b0215c16f | ||
|
|
8292da414d | ||
|
|
b2cce52b34 | ||
|
|
6ec248dfbf | ||
|
|
634d48e3e7 | ||
|
|
391efb1b2b | ||
|
|
07e64cfa02 | ||
|
|
594a8dd3cc | ||
|
|
f5ef509af6 | ||
|
|
e541111a27 | ||
|
|
c0c310862a | ||
|
|
65f09c46ca | ||
|
|
f199dd5b9f | ||
|
|
56e53219c2 | ||
|
|
cee00d8315 | ||
|
|
cbc124eff6 | ||
|
|
219b52fc12 | ||
|
|
98e8fc88bd | ||
|
|
9a73a10a09 | ||
|
|
6ff3a34c14 | ||
|
|
0499ded1b3 | ||
|
|
9fa04702af | ||
|
|
89c4cc9d79 | ||
|
|
b49562f4bc | ||
|
|
5f15ddc52c | ||
|
|
5bde9804d6 | ||
|
|
547f1aed75 | ||
|
|
5aed3fbb87 | ||
|
|
13cb4d321a | ||
|
|
004a99e080 | ||
|
|
2aee878a40 | ||
|
|
ca042631b7 | ||
|
|
7e51061582 | ||
|
|
e71ff17a23 | ||
|
|
2270300177 | ||
|
|
3f79fa5b9d | ||
|
|
322a6a700a | ||
|
|
ba66c18f28 | ||
|
|
5f10154000 | ||
|
|
66290fe1ce | ||
|
|
0c3340b543 | ||
|
|
c7ba4e8db6 | ||
|
|
65ec41c6ce | ||
|
|
395969a371 | ||
|
|
f2f818acb3 | ||
|
|
5668c37127 | ||
|
|
5f67f02cde | ||
|
|
f0fcd7155c | ||
|
|
f96348cf59 | ||
|
|
f7f4803fdc | ||
|
|
810c202f1b | ||
|
|
2d770ae05e | ||
|
|
5dfeb60a13 | ||
|
|
5896bdccc1 | ||
|
|
206c046382 | ||
|
|
3ce66894f0 | ||
|
|
cfcdcd0838 | ||
|
|
474980060e | ||
|
|
e816b4e58b | ||
|
|
3f4460ad6f | ||
|
|
92e2d45f0d | ||
|
|
7e7c920816 | ||
|
|
6606f0f58b | ||
|
|
b64a0f17eb | ||
|
|
7a5e878733 | ||
|
|
5edec0c0cb | ||
|
|
8912b1de62 | ||
|
|
93328d4443 | ||
|
|
dbb9152b9a | ||
|
|
5b8caea9ff | ||
|
|
24e9936f28 | ||
|
|
3e9776dc89 | ||
|
|
07de598aec | ||
|
|
2ea74d7435 | ||
|
|
3c3b81cb1f | ||
|
|
a736bf9b09 | ||
|
|
ada297d331 | ||
|
|
515d28313d | ||
|
|
303ae3ac14 | ||
|
|
426cbdec89 | ||
|
|
dcc2256195 | ||
|
|
bdbfc19c7c | ||
|
|
041202d242 | ||
|
|
92199b7b95 | ||
|
|
8bc530ee81 | ||
|
|
ffb9bee5af | ||
|
|
7413df4ccc | ||
|
|
12d67c71c8 | ||
|
|
8f40a507b9 | ||
|
|
4adf1ee898 | ||
|
|
4d155f0a26 | ||
|
|
040addc1f5 | ||
|
|
42d1ef648e | ||
|
|
f681598104 | ||
|
|
5810bd6201 | ||
|
|
547136adc7 | ||
|
|
ce12047fa3 | ||
|
|
5ab4c70013 | ||
|
|
7bbb07c927 | ||
|
|
db20ad3232 | ||
|
|
eccbfa7a64 | ||
|
|
6e126bd562 | ||
|
|
2ac9d6bd98 | ||
|
|
9ad92d26d4 | ||
|
|
ace3af8f50 | ||
|
|
81f459c571 | ||
|
|
a215af13f5 | ||
|
|
1bbe023e20 | ||
|
|
34d208c19e | ||
|
|
b0c53152ef | ||
|
|
35f9d6ed99 | ||
|
|
d508c1855c | ||
|
|
9e5f1d08d2 | ||
|
|
0135e40f1a | ||
|
|
33ae428506 | ||
|
|
d89ad18825 | ||
|
|
9f3f9ad808 | ||
|
|
d53bfd4542 | ||
|
|
febcbce413 | ||
|
|
b478e9c3af | ||
|
|
6c388bd557 | ||
|
|
f0a30b5cb7 | ||
|
|
daa5227f07 | ||
|
|
8920d5890a | ||
|
|
04a5497f3b | ||
|
|
139e42dbed | ||
|
|
8c905f95e3 | ||
|
|
33305cfb46 | ||
|
|
9d65c450b6 |
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -3,7 +3,7 @@ name: Bug report
|
||||
description: >
|
||||
Use for any bug that happens after campaign generation. If the New Game wizard
|
||||
failed, use the "New Game wizard failed" template instead.
|
||||
labels: bug
|
||||
labels: ["Bug", "Priority Medium"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/campaign_update.md
vendored
8
.github/ISSUE_TEMPLATE/campaign_update.md
vendored
@@ -2,19 +2,19 @@
|
||||
name: Campaign update submission
|
||||
about: Submit an update to a campaign you maintain.
|
||||
title: 'Update for <campaign name>'
|
||||
labels: campaign-update-submission
|
||||
labels: Campaign Update Submission
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
This form should only be used for submitted updated miz/json files for campaigns
|
||||
distributed with Liberation. If you are _requesting_ an update to a campaign, see
|
||||
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance. If the
|
||||
distributed with Retribution. If you are _requesting_ an update to a campaign, see
|
||||
https://github.com/dcs-retribution/dcs-retribution/wiki/Campaign-maintenance. If the
|
||||
campaign has an owner, it will be updated before release. If it does not, you can
|
||||
volunteer to own it.
|
||||
|
||||
If you are not the owner of the campaign listed on
|
||||
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance, please start
|
||||
https://github.com/dcs-retribution/dcs-retribution/wiki/Campaign-maintenance, please start
|
||||
there.
|
||||
|
||||
Otherwise, delete everything above the line below and fill out the following form. Note:
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/code_architecture.yml
vendored
Normal file
17
.github/ISSUE_TEMPLATE/code_architecture.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Code Architecture
|
||||
description: >
|
||||
Suggest a refactor, or other issues related to the codebase.
|
||||
labels: ["Refactor", "Python", "Priority Low"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Before filing, please search the issues to see if the problem has already been reported.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe your proposal.
|
||||
description: >
|
||||
Indicate where the issue lies and briefly explain how you plan to tackle it.
|
||||
validations:
|
||||
required: true
|
||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,12 +1,12 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: FAQ
|
||||
url: https://discord.gg/PXHA6AXw
|
||||
url: https://discord.gg/b4x34Bg4We
|
||||
about: Check to see if your issue is in the FAQ.
|
||||
- name: Manual
|
||||
url: https://github.com/dcs-liberation/dcs_liberation/wiki/
|
||||
url: https://github.com/dcs-retribution/dcs-retribution/wiki/
|
||||
- name: Feature blocking DCS AI bugs
|
||||
url: https://github.com/dcs-liberation/dcs_liberation#dcs-bugs
|
||||
url: https://github.com/dcs-retribution/dcs-retribution#dcs-bugs
|
||||
about: >
|
||||
A list of known DCS bugs that prevent us from improving AI behavior. Check
|
||||
the list before filing AI bugs here to see if it's something we know about
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Before filing, please search the issue tracker to see if this feature has already been requested.
|
||||
|
||||
If requesting a DCS AI feature, check If reporting a DCS AI bug, check https://github.com/dcs-liberation/dcs_liberation#dcs-bugs.
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
31
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
31
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Feature Request
|
||||
description: >
|
||||
Suggest an idea for this project.
|
||||
labels: ["Enhancement", "Priority Medium"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Before filing, please search the issue tracker to see if this feature has already been requested.
|
||||
If requesting a DCS AI feature, check If reporting a DCS AI bug,
|
||||
check https://github.com/dcs-retribution/dcs-retribution#dcs-bugs.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: >
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: >
|
||||
A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: >
|
||||
Add any other context or screenshots about the feature request here.
|
||||
4
.github/ISSUE_TEMPLATE/mod_support.md
vendored
4
.github/ISSUE_TEMPLATE/mod_support.md
vendored
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Mod support request
|
||||
about: Request Liberation support for new mods, or updates to existing mods
|
||||
about: Request Retribution support for new mods, or updates to existing mods
|
||||
title: Add/update <mod name>
|
||||
labels: mod support
|
||||
labels: Mod Request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
2
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
@@ -3,7 +3,7 @@ name: New Game wizard failed
|
||||
description: >
|
||||
Use for bugs that prevent the "New Game" wizard from completing successfully.
|
||||
If the wizard completes without issue, use the normal bug report template.
|
||||
labels: bug
|
||||
labels: ["Bug", "Priority High"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
19
.github/workflows/build.yml
vendored
19
.github/workflows/build.yml
vendored
@@ -25,6 +25,7 @@ jobs:
|
||||
- name: Set build number
|
||||
run: |
|
||||
[IO.File]::WriteAllLines($pwd.path + "\resources\buildnumber", $env:GITHUB_RUN_NUMBER)
|
||||
[IO.File]::WriteAllLines($pwd.path + "\resources\gitsha", $env:GITHUB_SHA)
|
||||
|
||||
- name: Build app
|
||||
uses: ./.github/actions/build-app
|
||||
@@ -42,11 +43,21 @@ jobs:
|
||||
- name: Send status to Discord
|
||||
uses: stegzilla/discord-notify@v2
|
||||
with:
|
||||
webhook_url: ${{ secrets.discord_webhook_betas }} # required
|
||||
webhook_url: ${{ secrets.discord_webhook_betas }}
|
||||
title: "Build #${{ github.run_number }} available!"
|
||||
message: "**Download link:\n**
|
||||
https://github.com/dcs-retribution/dcs-retribution/actions/runs/${{ github.run_id }}"
|
||||
message: "**Branch:** ${{ github.ref_name }}\n**Download link:\n** https://github.com/dcs-retribution/dcs-retribution/actions/runs/${{ github.run_id }}"
|
||||
include_image: true
|
||||
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
||||
username: GitHub Build Notifier
|
||||
if: success() # or failure() or success()
|
||||
if: startsWith(github.ref_name, 'test')
|
||||
|
||||
- name: Send status to Discord
|
||||
uses: stegzilla/discord-notify@v2
|
||||
with:
|
||||
webhook_url: ${{ secrets.discord_webhook_alphas }}
|
||||
title: "Build #${{ github.run_number }} available!"
|
||||
message: "**Branch:** ${{ github.ref_name }}\n**Download link:\n** https://github.com/dcs-retribution/dcs-retribution/actions/runs/${{ github.run_id }}"
|
||||
include_image: true
|
||||
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
||||
username: GitHub Build Notifier
|
||||
if: github.ref_name == 'dev'
|
||||
|
||||
@@ -26,7 +26,6 @@ the patreon link will keep pointing to [Khopa's](https://github.com/Khopa) accou
|
||||
|
||||
Currently, Retribution supports the following new features:
|
||||
* A livery selector in the air wing configuration dialog
|
||||
* Fixed mod support (still unfixed in Liberation)
|
||||
* Support for campaigns with quad-point triggers
|
||||
* Support for the F-15D 'Baz' mod
|
||||
* Support for the Su-30 mod
|
||||
@@ -34,12 +33,13 @@ Currently, Retribution supports the following new features:
|
||||
* Variation in flight-size for BAI/ANTISHIP/DEAD/STRIKE main flight (randomize 2/3/4-ships)
|
||||
* More performance options
|
||||
* Various bug-fixes (including a fix for the zombie carrier)
|
||||
* And more... (See [changelog](https://github.com/dcs-retribution/dcs-retribution/blob/main/changelog.md))
|
||||
|
||||
## Downloads
|
||||
|
||||
Latest release is available here : https://github.com/dcs-retribution/dcs-retribution/releases
|
||||
|
||||
To download preview builds of the next version of DCS Retribution, see https://github.com/dcs-retribution/dcs-retribution/wiki/Preview-builds.
|
||||
To download preview builds of the next version of DCS Retribution, see https://github.com/dcs-retribution/dcs-retribution/wiki/Betas.
|
||||
|
||||
## DCS bugs
|
||||
|
||||
@@ -62,7 +62,7 @@ Our plans for future releases can be found on our
|
||||
[Projects page](https://github.com/dcs-retribution/dcs-retribution/projects).
|
||||
Each planned release has a Project, and the page for that project has columns for to do,
|
||||
in progress, and done. Items in the Done column are in the
|
||||
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Preview-builds)
|
||||
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Betas)
|
||||
for that release. Items in the To do column are planned to be added to that release.
|
||||
|
||||
## Resources
|
||||
|
||||
180
changelog.md
180
changelog.md
@@ -1,57 +1,199 @@
|
||||
# Retribution 1.1.1 (hotfix)
|
||||
|
||||
## Features/Improvements
|
||||
* **[Modding]** Support for IDF Mod Project F-16I Sufa & F-16D v3.6 mod
|
||||
* **[Modding]** Support for JAS-39 Gripen v1.8.5-beta mod
|
||||
|
||||
## Fixes
|
||||
* **[Plugins]** Fix bug where changes to plugin options doesn't do anything.
|
||||
* **[Campaign Management]** Fix bug in procurement when no squadrons are present.
|
||||
* **[Layouts]** Fix edge-case bug layout's group size.
|
||||
* **[Campaign Design]** Preset groups assigned to specific TGOs not working as intended.
|
||||
|
||||
# Retribution 1.1.0
|
||||
|
||||
## Features/Improvements
|
||||
* **[Mission Generation]** Given a CAS flight was planned, delay ground force attack until first CAS flight is on station
|
||||
* **[Mission Generation]** Add option to switch ATFLIR to LITENING automatically for ground based F/A-18C flights
|
||||
* **[Mission Generation]** Add option to configure OPFOR autoplanner aggressiveness and have the AI take risks and plan missions against defended targets
|
||||
* **[Mission Generation]** Add option to configure the desired tanker on-station time in settings
|
||||
* **[Mission Generation]** Reserve GUARD frequency on VHF/UHF
|
||||
* **[Mission Generation]** Randomization in radio frequency allocation
|
||||
* **[Mission Generation]** Configurable number of Combined Arms slots
|
||||
* **[Mission Generation]** Enable spectating & F11 free camera when the "Allow external views" option is selected
|
||||
* **[Cheat Menu]** Option to instantly transfer squadrons across bases.
|
||||
* **[Modding]** Support for IDF Mod Project F-16I Sufa & F-16D v3.2 mod
|
||||
* **[Modding]** Support for F/A-18E/F/G mod version 2.1
|
||||
* **[Modding]** Support for Swedish Military Assets for DCS by Currenthill Version 1.10
|
||||
* **[UI]** Add selectable units in faction overview during campaign generation.
|
||||
* **[UI]** Add button to rename pilots in Air Wing's Squadron dialog.
|
||||
* **[UI]** Add clone buttons for flights & packages.
|
||||
* **[UI]** Editing of flight's custom name.
|
||||
* **[UI]** Introduce custom names for packages (purely for organizational purposes).
|
||||
* **[UI]** Configurable UHF frequency (225-400MHz) for Packages, Carriers, LHAs, FOBs & FARPs.
|
||||
* **[UI]** Configurable Intra-Flight frequency for Flights.
|
||||
* **[UI]** Configurable TACAN for Carriers, LHAs & Tankers.
|
||||
* **[UI]** Configurable ICLS for capable Carriers & LHAs.
|
||||
* **[UI]** Configurable LINK4 for Carriers.
|
||||
* **[Kneeboard]** Show package information in Support page
|
||||
* **[Kneeboard]** Show extra weather information on 'Mission info' page
|
||||
* **[Kneeboard]** Show BRC in 'RWY' column for aircraft carriers on 'Mission info' page
|
||||
* **[Campaign Design]** Ability to define designated CTLD zones for Control Points (Airbases & FOBs/FARPs)
|
||||
* **[Campaign Design]** Ability to define preset groups for specific TGOs, given the preset group is accessible for the faction and the task matches.
|
||||
* **[Campaign Management]** Additional options for automated budget management.
|
||||
* **[Campaign Management]** New options to allow more control of randomized flight sizes (applicable for BARCAP/CAS/OCA/ANTI-SHIP).
|
||||
* **[Plugins]** Updated Splash Damage script to v2.0 by RotorOps.
|
||||
* **[Mission Generation]** Improvements to DEAD & STRIKE flights, allowing AI to handle a larger variety of weapons.
|
||||
|
||||
## Fixes
|
||||
* **[UI]** Removed deprecated options
|
||||
* **[UI]** Add missing icon & banner for C130 Hercules mod
|
||||
* **[Mission Generation]** Avoid aircraft from being assigned to helicopter parking spots, resulting into air starts that usually crash.
|
||||
* **[Mission Generation]** Use stacking algorithm to create vertical separation between flights spawning mid-mission over their departure, usually resulting into mid-air collisions.
|
||||
* **[Mission Generation]** Fixed all callsigns being "Enfield 1-1" on dedicated servers.
|
||||
* **[Mission Generation]** Fixed AI ferry flights for helicopters when transferring to a FOB/FARP.
|
||||
* **[Mission Generation]** Fixed 'Uninitialized flight' exception when adding flights after aborted take-off.
|
||||
* **[Modding]** Fixed conflicts caused by HDS units
|
||||
* **[UX]** Gracefully handle corrupted preferences file.
|
||||
* **[Mission Generation]** Aircraft not using decoys during SEAD.
|
||||
* **[Campaign]** New campaign (1968 Yankee Station) by Adecarcer
|
||||
|
||||
# Retribution 1.0.1 (hotfix)
|
||||
* **[Mission Generation]** Fix serialization issue when STRIKE flight has no escorts
|
||||
|
||||
# Retribution 1.0.0
|
||||
|
||||
## Features/Improvements
|
||||
* **[Engine]** Support for DCS v2.8.1.34437.
|
||||
* **[Briefing]** Add tanker info to mission briefing
|
||||
* **[Campaign]** Add 5 new campaigns by Oscar Juliet from WRL
|
||||
* **[Campaign]** Add ability to define livery overrides also for ground/naval units
|
||||
* **[Data]** Added data to support C-47 Skytrain.
|
||||
* **[Data]** Added data to support F-16A MLU.
|
||||
* **[Data]** Added data to support KS-19 & SON-9, including support for "AAA Site" layout.
|
||||
* **[Mission Generation]** Add option to configure the maximum front-line length in settings
|
||||
* **[Mission Generation]** Variable flight-size (2/3/4-ship) for BAI/ANTISHIP/DEAD/STRIKE (main) missions
|
||||
* **[Mission Generation]** Use Escort & SEAD tasks for Escort & SEAD Escort flights
|
||||
* **[Mission Generation]** Variable flight-size (2/3/4-ship) for
|
||||
BAI/ANTISHIP/DEAD/STRIKE/BARCAP/CAS/OCA/AIR-ASSAULT (main) missions
|
||||
* **[Mission Generation]** Add option to only generate night missions
|
||||
* **[Modding]** Support for F-15D 'Baz' mod version 1.0
|
||||
* **[Modding]** Support for Su-30 mod version 2.01B
|
||||
* **[Modding]** Support for A-6A Intruder version 2.7.5.01
|
||||
* **[Modding]** Support for F-4B Phantom II mod version v2.7.10.02, patch 2022.10.02
|
||||
* **[Modding]** Support for F-100 Super Sabre mod versions v2.7.18.01 & 2.7.18.30765 and patch 20.10.22
|
||||
* **[Modding]** Support for F-105 mod version 2.7.12.23x
|
||||
* **[Modding]** Support IDF Mod Project F-16I Sufa & F-16D v2.2 mod
|
||||
* **[Modding]** Support for F-84G mod version 2.5.7.01
|
||||
* **[Modding]** Updated F-104 mod version support to 2.7.11.222.01
|
||||
* **[Modding]** Updated Community A-4E-C mod version support to 2.1.0 release.
|
||||
* **[UI]** Add livery selector to Air Wing Configurator's squadrons.
|
||||
* **[Performance]** Added performance option: Maximum front-line unit supply per control point.
|
||||
* **[Performance]** Added performance option: Disable convoys.
|
||||
* **[Performance]** Added performance option: Front-line troops prefer roads.
|
||||
* **[Data]** Added data to support C-47 Skytrain.
|
||||
* **[Performance]** Added performance option: Disable idle aircraft at airfields.
|
||||
* **[Squadrons]** Squadron pilot limits enabled by default.
|
||||
* **[UI]** Add livery selector to Air Wing Configurator's squadrons.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Mission Generation]** Fixed issue where aircraft carriers would return after being killed.
|
||||
* **[Mission Generation]** Kneeboard STRIKE coordinates would sometimes get clipped when not fitting.
|
||||
* **[UI]** Fix exception when trying to add a waypoints to a flightplan.
|
||||
|
||||
# Liberation:
|
||||
# 7.0.0
|
||||
|
||||
Saves from 6.x are not compatible with 7.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.8.3.37556.
|
||||
* **[Engine]** Saved games are now a zip file of save assets for easier bug reporting. The new extension is .liberation.zip. Drag and drop that file into bug reports.
|
||||
* **[Flight Planning]** Package TOT and composition can be modified after advancing time in Liberation.
|
||||
* **[Mission Generation]** Units on the front line are now hidden on MFDs.
|
||||
* **[Mission Generation]** Preset radio channels will now be configured for both A-10C modules.
|
||||
* **[Mission Generation]** Both A-10C modules now use separate radios for inter- and intra-flight comms (similar to other modern aircraft).
|
||||
* **[Modding]** Updated Community A-4E-C mod version support to 2.1.0 release.
|
||||
* **[Modding]** Add support for VSN F-4B and F-4C mod.
|
||||
|
||||
## Fixes
|
||||
|
||||
# 6.1.1
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Data]** Fixed unit ID for the KS-19 AAA. KS-19 would not previously generate correctly in missions. A new game is required for this fix to take effect.
|
||||
* **[Flight Planning]** Automatic flight planning will no longer accidentally plan a recovery tanker instead of a theater refueling package. This fixes a potential crash during mission generation when opfor plans a refueling task at a sunk carrier. You'll need to skip the current turn to force opfor to replan their flights to get the fix.
|
||||
* **[Mission Generation]** Using heliports (airports without any runways) will no longer cause mission generation to fail.
|
||||
|
||||
# 6.1.0
|
||||
|
||||
Saves from 6.0.0 are compatible with 6.1.0
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Factions]** Defaulted bluefor modern to use Georgian and Ukrainian liveries for Russian aircraft.
|
||||
* **[Factions]** Added Peru.
|
||||
* **[Modding]** Added support for the HMS Ariadne, Achilles, and Castle class.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Flight Planning]** Fixes CAS flights not having landing waypoints.
|
||||
* **[Squadrons]** Fixed the livery for the VF-33 F-14A squadron.
|
||||
* **[UI]** Fixed an issue where manual submit of mission results did not end the mission correctly.
|
||||
|
||||
# 6.0.0
|
||||
|
||||
Saves from 5.x are not compatible with 6.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.7.18.30348.
|
||||
* **[Mission Generation]** Added an option to fast-forward mission generation until the point of first contact (WIP).
|
||||
* **[Mission Generation]** Added performance option to not cull IADS when culling would effect how mission is played at target area.
|
||||
* **[Mission Generation]** Reworked the ground object generation which now uses a new layout system
|
||||
* **[Mission Generation]** Added information about the modulation (AM/FM) of the assigned frequencies to the kneeboard and assign AM modulation instead of FM for JTAC.
|
||||
* **[Engine]** Support for DCS 2.8.0.33006.
|
||||
* **[Factions]** Updated the Faction file structure. Older custom faction files will not work correctly and have to be updated to the new structure.
|
||||
* **[Flight Planning]** Added preset formations for different flight types at hold, join, ingress, and split waypoints. Air to Air flights will tend toward line-abreast and spread-four formations. Air to ground flights will tend towards trail formation.
|
||||
* **[Flight Planning]** Added the ability to plan tankers for recovery on package flights. AI does not plan.
|
||||
* **[Flight Planning]** Added preset formations for different flight types at hold, join, ingress, and split waypoints. Air to Air flights will tend toward line-abreast and spread-four formations. Air to ground flights will tend towards trail formation.
|
||||
* **[Flight Planning]** Added the ability to plan tankers for recovery on package flights. This mission type will not be planned automatically.
|
||||
* **[Flight Planning]** Air to Ground flights now have ECM enabled on lock at the join point, and SEAD/DEAD also have ECM enabled on detection and lock at ingress.
|
||||
* **[Flight Planning]** AWACS flightplan changed from orbit to a racetrack to reduce data link disconnects which were caused by blind spots as a result of the bank angle.
|
||||
* **[Flight Planning]** Added a new helo mission type: AirAssault which can be used to load and transport infantry troops from a pickup zone or a carrier to an enemy CP to capture it.
|
||||
* **[Flight Planning]** Improved the Airlift mission type so that it now can be enforced within the unit transfer dialog and implemented CTLD support. This allows user to spawn sling loadable crates at the pickup location and fly transport flights.
|
||||
* **[Flight Planning]** Added a new helo mission type: AirAssault which can be used to load and transport infantry troops from a pickup zone or a carrier to an enemy CP to capture it.
|
||||
* **[Flight Planning]** Improved the Airlift mission type so that it now can be enforced within the unit transfer dialog and implemented CTLD support. This allows user to spawn sling loadable crates at the pickup location and fly transport flights.
|
||||
* **[Mission Generation]** Added an option to fast-forward mission generation until the point of first contact (WIP).
|
||||
* **[Mission Generation]** Added performance option to not cull IADS when culling would affect how mission is played at target area.
|
||||
* **[Mission Generation]** Reworked the ground object generation which now uses a new layout system
|
||||
* **[Mission Generation]** Added information about the modulation (AM/FM) of the assigned frequencies to the kneeboard and assign AM modulation instead of FM for JTAC.
|
||||
* **[Mission Generation]** Added ice halos.
|
||||
* **[Mission Generation]** Adjusted wind speeds. Wind speeds at high altitude are generally higher now.
|
||||
* **[Mission Generation]** Added turbulence. Higher in Summer and Winter, also higher at day time than at nighttime.
|
||||
* **[Modding]** Updated UH-60L mod version support to 1.3.1
|
||||
* **[Modding]** Updated the High Digit SAMs implementation and added the HQ-2 as well as the upgraded SA-2 and SA-3 Launchers from the mod. Threat range circles will now also be displayed correctly.
|
||||
* **[Modding]** Theater information such as climate properties is now moddable.
|
||||
* **[Modding]** Allow campaign designers to define default values for the economy settings (starting budget and multiplier).
|
||||
* **[Modding]** Campaigns can now optionally define their start time by including a time in the `recommended_start_date` field. There is not currently a way to override the start time in the UI.
|
||||
* **[Plugins]** Allow full support of the SkynetIADS plugin with all advanced features (connection nodes, power sources, command centers) if campaign supports it.
|
||||
* **[Plugins]** Added support for the CTLD script by ciribob with many possible customization options and updated the JTAC Autolase to the CTLD included script.
|
||||
* **[UI]** Added options to the loadout editor for setting properties such as HMD choice.
|
||||
* **[UI]** Added separate images for the different carrier types.
|
||||
* **[UI]** Add Accept/Reset buttons to Air Wing Configurator screen.
|
||||
* **[Campaign]** Allow campaign designers to define default values for the economy settings (starting budget and multiplier).
|
||||
* **[Plugins]** Allow full support of the SkynetIADS plugin with all advanced features (connection nodes, power sources, command centers) if campaign supports it.
|
||||
* **[Plugins]** Added support for the CTLD script by ciribob with many possible customization options and updated the JTAC Autolase to the CTLD included script.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Engine]** Fixed issue that prevented some weapon types like torpedoes from being recognized.
|
||||
* **[Flight Planning]** Fixed a miscalculation of waypoint TOTs that would require time travel.
|
||||
* **[Loadouts]** Improved the range of the F-16 CAS loadout by adding bags.
|
||||
* **[Mission Generation]** AAA ground units now spawn correctly at the frontline
|
||||
* **[Mission Generation]** Fixed SA-13 incorrectly created as SA-8 Loading Unit which will not be spawned in the generated mission.
|
||||
* **[Mission Generation]** Fixed adding additional mission types for a squadron causing error messages when the mission type is not supported by the aircraft type by default
|
||||
* **[Mission Generation]** Fixed an issue where SEAD/DEAD/BAI flights fired all missiles / bombs against a single unit in a group instead of targeting the whole group.
|
||||
* **[Mission Generation]** Fixed an issue which generated the helipads at FARPs incorrectly and placed the helicopters within each other.
|
||||
* **[Mission Generation]** Fixed an issue with SEAD missions flown by the AI when using the Skynet Plugin and anti-radiation missiles (ARM). The AI now correctly engages the SAM when it comes alive instead of diving into it.
|
||||
* **[Mission Generation]** Fixed an issue where SEAD/DEAD/BAI flights fired all missiles / bombs against a single unit in a group instead of targeting the whole group.
|
||||
* **[Mission Generation]** Fixed adding additional mission types for a squadron causing error messages when the mission type is not supported by the aircraft type by default
|
||||
* **[Mission Generation]** AAA ground units now spawn correctly at the frontline
|
||||
* **[UI]** Fixed and issue where the liberation main exe was still running after application close.
|
||||
* **[Mission Generation]** Fixed generation issue that would cause AI helicopters to get stuck after taking off from a FARP.
|
||||
* **[Mission Generation]** Fixed mission scripting error caused by control points with apostrophes in their names, such as Tha'lah.
|
||||
* **[Modding]** Campaigns that used quad zones for scenery targets will no longer load. Only circular zones were ever supported, but an implementation quirk allowed them to load in a way that would misbehave. A "No white triggerzones found" message during campaign generation is the sign of a broken campaign.
|
||||
* **[Modding]** Loadouts with invalid weapons (typically new DCS weapons not yet available in Liberation) will be ignored rather than causing an error.
|
||||
* **[Squadrons]** Fixed issue in air wing configuration that would allow squadrons to be created with no home base if no base was available.
|
||||
* **[Squadrons]** Helicopter squadrons can no longer be assigned to FOBs that are not FARPs.
|
||||
* **[UI]** Add vanilla theme weather and time of day icons
|
||||
* **[UI]** Disable player slots for non-flyable aircraft.
|
||||
|
||||
* **[UI]** Fixed and issue where the liberation main exe was still running after application close.
|
||||
|
||||
# 5.2.0
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DCS Liberation Client
|
||||
# DCS Retribution Client
|
||||
|
||||
This is a React app for the front-end of DCS Liberation. It is a work in
|
||||
This is a React app for the front-end of DCS Retribution. It is a work in
|
||||
progress that just barely implements the map. This is not useful for players
|
||||
yet.
|
||||
|
||||
|
||||
20154
client/package-lock.json
generated
20154
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -69,7 +69,9 @@
|
||||
"electron": "^21.1.0",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"generate-license-file": "^2.0.0",
|
||||
"got": "^11.8.5",
|
||||
"license-checker": "^25.0.1",
|
||||
"nth-check": "^2.0.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"wait-on": "^6.0.1"
|
||||
|
||||
@@ -1,270 +1,15 @@
|
||||
import backend from "../../api/backend";
|
||||
import { ControlPoint as ControlPointModel } from "../../api/liberationApi";
|
||||
import {
|
||||
useClearControlPointDestinationMutation,
|
||||
useSetControlPointDestinationMutation,
|
||||
} from "../../api/liberationApi";
|
||||
import {
|
||||
Icon,
|
||||
LatLng,
|
||||
Point,
|
||||
Marker as LMarker,
|
||||
Polyline as LPolyline,
|
||||
LatLngLiteral,
|
||||
} from "leaflet";
|
||||
import { Symbol as MilSymbol } from "milsymbol";
|
||||
import {
|
||||
MutableRefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import { Marker, Polyline, Tooltip } from "react-leaflet";
|
||||
|
||||
function iconForControlPoint(cp: ControlPointModel) {
|
||||
const symbol = new MilSymbol(cp.sidc, {
|
||||
size: 24,
|
||||
colorMode: "Dark",
|
||||
});
|
||||
|
||||
return new Icon({
|
||||
iconUrl: symbol.toDataURL(),
|
||||
iconAnchor: new Point(symbol.getAnchor().x, symbol.getAnchor().y),
|
||||
});
|
||||
}
|
||||
|
||||
function openInfoDialog(controlPoint: ControlPointModel) {
|
||||
backend.post(`/qt/info/control-point/${controlPoint.id}`);
|
||||
}
|
||||
|
||||
function openNewPackageDialog(controlPoint: ControlPointModel) {
|
||||
backend.post(`/qt/create-package/control-point/${controlPoint.id}`);
|
||||
}
|
||||
import { MobileControlPoint } from "./MobileControlPoint";
|
||||
import { StaticControlPoint } from "./StaticControlPoint";
|
||||
|
||||
interface ControlPointProps {
|
||||
controlPoint: ControlPointModel;
|
||||
}
|
||||
|
||||
function LocationTooltipText(props: ControlPointProps) {
|
||||
return <h3 style={{ margin: 0 }}>{props.controlPoint.name}</h3>;
|
||||
}
|
||||
|
||||
function metersToNauticalMiles(meters: number) {
|
||||
return meters * 0.000539957;
|
||||
}
|
||||
|
||||
function formatLatLng(latLng: LatLng) {
|
||||
const lat = latLng.lat.toFixed(2);
|
||||
const lng = latLng.lng.toFixed(2);
|
||||
const ns = latLng.lat >= 0 ? "N" : "S";
|
||||
const ew = latLng.lng >= 0 ? "E" : "W";
|
||||
return `${lat}°${ns} ${lng}°${ew}`;
|
||||
}
|
||||
|
||||
function destinationTooltipText(
|
||||
cp: ControlPointModel,
|
||||
destinationish: LatLngLiteral,
|
||||
inRange: boolean
|
||||
) {
|
||||
const destination = new LatLng(destinationish.lat, destinationish.lng);
|
||||
const distance = metersToNauticalMiles(
|
||||
destination.distanceTo(cp.position)
|
||||
).toFixed(1);
|
||||
if (!inRange) {
|
||||
return `Out of range (${distance}nm away)`;
|
||||
}
|
||||
const dest = formatLatLng(destination);
|
||||
return `${cp.name} moving ${distance}nm to ${dest} next turn`;
|
||||
}
|
||||
|
||||
function PrimaryMarker(props: ControlPointProps) {
|
||||
// We can't use normal state to update the marker tooltip or the line points
|
||||
// because if we set any state in the drag event it will re-render the
|
||||
// component and interrupt dragging. Instead, keep refs to the objects and
|
||||
// mutate them directly.
|
||||
//
|
||||
// For the same reason, the path is owned by this component, because updating
|
||||
// sibling state would be messy. Lifting the state into the parent would still
|
||||
// cause this component to redraw.
|
||||
const marker: MutableRefObject<LMarker | undefined> = useRef();
|
||||
const pathLine: MutableRefObject<LPolyline | undefined> = useRef();
|
||||
|
||||
const [hasDestination, setHasDestination] = useState<boolean>(
|
||||
props.controlPoint.destination != null
|
||||
);
|
||||
const [pathDestination, setPathDestination] = useState<LatLngLiteral>(
|
||||
props.controlPoint.destination
|
||||
? props.controlPoint.destination
|
||||
: props.controlPoint.position
|
||||
);
|
||||
const [position, setPosition] = useState<LatLngLiteral>(
|
||||
props.controlPoint.destination
|
||||
? props.controlPoint.destination
|
||||
: props.controlPoint.position
|
||||
);
|
||||
|
||||
const setDestination = useCallback((destination: LatLng) => {
|
||||
setPathDestination(destination);
|
||||
setPosition(destination);
|
||||
setHasDestination(true);
|
||||
}, []);
|
||||
|
||||
const resetDestination = useCallback(() => {
|
||||
setPathDestination(props.controlPoint.position);
|
||||
setPosition(props.controlPoint.position);
|
||||
setHasDestination(false);
|
||||
}, [props.controlPoint.position]);
|
||||
|
||||
const [putDestination, { isLoading }] =
|
||||
useSetControlPointDestinationMutation();
|
||||
const [cancelTravel] = useClearControlPointDestinationMutation();
|
||||
|
||||
useEffect(() => {
|
||||
marker.current?.setTooltipContent(
|
||||
props.controlPoint.destination
|
||||
? destinationTooltipText(
|
||||
props.controlPoint,
|
||||
props.controlPoint.destination,
|
||||
true
|
||||
)
|
||||
: ReactDOMServer.renderToString(
|
||||
<LocationTooltipText controlPoint={props.controlPoint} />
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Marker
|
||||
position={position}
|
||||
icon={iconForControlPoint(props.controlPoint)}
|
||||
draggable={props.controlPoint.mobile && !isLoading}
|
||||
autoPan
|
||||
// We might draw other markers on top of the CP. The tooltips from the
|
||||
// other markers are helpful so we want to keep them, but make sure the CP
|
||||
// is always the clickable thing.
|
||||
zIndexOffset={1000}
|
||||
opacity={props.controlPoint.destination ? 0.5 : 1}
|
||||
ref={(ref) => {
|
||||
if (ref != null) {
|
||||
marker.current = ref;
|
||||
}
|
||||
}}
|
||||
eventHandlers={{
|
||||
click: () => {
|
||||
if (!hasDestination) {
|
||||
openInfoDialog(props.controlPoint);
|
||||
}
|
||||
},
|
||||
contextmenu: () => {
|
||||
if (props.controlPoint.destination) {
|
||||
cancelTravel({ cpId: props.controlPoint.id }).then(() => {
|
||||
resetDestination();
|
||||
});
|
||||
} else {
|
||||
openNewPackageDialog(props.controlPoint);
|
||||
}
|
||||
},
|
||||
drag: (event) => {
|
||||
const destination = event.target.getLatLng();
|
||||
backend
|
||||
.get(
|
||||
`/control-points/${props.controlPoint.id}/destination-in-range?lat=${destination.lat}&lng=${destination.lng}`
|
||||
)
|
||||
.then((inRange) => {
|
||||
marker.current?.setTooltipContent(
|
||||
destinationTooltipText(
|
||||
props.controlPoint,
|
||||
destination,
|
||||
inRange.data
|
||||
)
|
||||
);
|
||||
});
|
||||
pathLine.current?.setLatLngs([
|
||||
props.controlPoint.position,
|
||||
destination,
|
||||
]);
|
||||
},
|
||||
dragend: async (event) => {
|
||||
const currentPosition = new LatLng(
|
||||
pathDestination.lat,
|
||||
pathDestination.lng
|
||||
);
|
||||
const destination = event.target.getLatLng();
|
||||
setDestination(destination);
|
||||
try {
|
||||
await putDestination({
|
||||
cpId: props.controlPoint.id,
|
||||
body: { lat: destination.lat, lng: destination.lng },
|
||||
}).unwrap();
|
||||
} catch (error) {
|
||||
console.error("setDestination failed", error);
|
||||
setDestination(currentPosition);
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tooltip />
|
||||
</Marker>
|
||||
<Polyline
|
||||
positions={[props.controlPoint.position, pathDestination]}
|
||||
weight={1}
|
||||
color="#80BA80"
|
||||
interactive
|
||||
ref={(ref) => {
|
||||
if (ref != null) {
|
||||
pathLine.current = ref;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface SecondaryMarkerProps {
|
||||
controlPoint: ControlPointModel;
|
||||
destination: LatLngLiteral | undefined;
|
||||
}
|
||||
|
||||
function SecondaryMarker(props: SecondaryMarkerProps) {
|
||||
if (!props.destination) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Marker
|
||||
position={props.controlPoint.position}
|
||||
icon={iconForControlPoint(props.controlPoint)}
|
||||
// We might draw other markers on top of the CP. The tooltips from the
|
||||
// other markers are helpful so we want to keep them, but make sure the CP
|
||||
// is always the clickable thing.
|
||||
zIndexOffset={1000}
|
||||
eventHandlers={{
|
||||
click: () => {
|
||||
openInfoDialog(props.controlPoint);
|
||||
},
|
||||
contextmenu: () => {
|
||||
openNewPackageDialog(props.controlPoint);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tooltip>
|
||||
<LocationTooltipText {...props} />
|
||||
</Tooltip>
|
||||
</Marker>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ControlPoint(props: ControlPointProps) {
|
||||
return (
|
||||
<>
|
||||
<PrimaryMarker {...props} />
|
||||
<SecondaryMarker
|
||||
controlPoint={props.controlPoint}
|
||||
destination={props.controlPoint.destination}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
if (props.controlPoint.mobile) {
|
||||
return <MobileControlPoint {...props} />;
|
||||
} else {
|
||||
return <StaticControlPoint {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
22
client/src/components/controlpoints/EventHandlers.ts
Normal file
22
client/src/components/controlpoints/EventHandlers.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ControlPoint } from "../../api/_liberationApi";
|
||||
import backend from "../../api/backend";
|
||||
|
||||
function openInfoDialog(controlPoint: ControlPoint) {
|
||||
backend.post(`/qt/info/control-point/${controlPoint.id}`);
|
||||
}
|
||||
|
||||
function openNewPackageDialog(controlPoint: ControlPoint) {
|
||||
backend.post(`/qt/create-package/control-point/${controlPoint.id}`);
|
||||
}
|
||||
|
||||
export const makeLocationMarkerEventHandlers = (controlPoint: ControlPoint) => {
|
||||
return {
|
||||
click: () => {
|
||||
openInfoDialog(controlPoint);
|
||||
},
|
||||
|
||||
contextmenu: () => {
|
||||
openNewPackageDialog(controlPoint);
|
||||
},
|
||||
};
|
||||
};
|
||||
15
client/src/components/controlpoints/Icons.tsx
Normal file
15
client/src/components/controlpoints/Icons.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ControlPoint } from "../../api/_liberationApi";
|
||||
import { Icon, Point } from "leaflet";
|
||||
import { Symbol } from "milsymbol";
|
||||
|
||||
export const iconForControlPoint = (cp: ControlPoint) => {
|
||||
const symbol = new Symbol(cp.sidc, {
|
||||
size: 24,
|
||||
colorMode: "Dark",
|
||||
});
|
||||
|
||||
return new Icon({
|
||||
iconUrl: symbol.toDataURL(),
|
||||
iconAnchor: new Point(symbol.getAnchor().x, symbol.getAnchor().y),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
interface LocationTooltipTextProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const LocationTooltipText = (props: LocationTooltipTextProps) => {
|
||||
return <h3 style={{ margin: 0 }}>{props.name}</h3>;
|
||||
};
|
||||
|
||||
export default LocationTooltipText;
|
||||
228
client/src/components/controlpoints/MobileControlPoint.tsx
Normal file
228
client/src/components/controlpoints/MobileControlPoint.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { ControlPoint } from "../../api/_liberationApi";
|
||||
import backend from "../../api/backend";
|
||||
import {
|
||||
useClearControlPointDestinationMutation,
|
||||
useSetControlPointDestinationMutation,
|
||||
} from "../../api/liberationApi";
|
||||
import { makeLocationMarkerEventHandlers } from "./EventHandlers";
|
||||
import { iconForControlPoint } from "./Icons";
|
||||
import LocationTooltipText from "./LocationTooltipText";
|
||||
import { MovementPath, MovementPathHandle } from "./MovementPath";
|
||||
import { StaticControlPoint } from "./StaticControlPoint";
|
||||
import { LatLng, Marker as LMarker, LatLngLiteral } from "leaflet";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import { Marker, Tooltip } from "react-leaflet";
|
||||
|
||||
function metersToNauticalMiles(meters: number) {
|
||||
return meters * 0.000539957;
|
||||
}
|
||||
|
||||
function formatLatLng(latLng: LatLng) {
|
||||
const lat = latLng.lat.toFixed(2);
|
||||
const lng = latLng.lng.toFixed(2);
|
||||
const ns = latLng.lat >= 0 ? "N" : "S";
|
||||
const ew = latLng.lng >= 0 ? "E" : "W";
|
||||
return `${lat}°${ns} ${lng}°${ew}`;
|
||||
}
|
||||
|
||||
function destinationTooltipText(
|
||||
cp: ControlPoint,
|
||||
destinationish: LatLngLiteral,
|
||||
inRange: boolean
|
||||
) {
|
||||
const destination = new LatLng(destinationish.lat, destinationish.lng);
|
||||
const distance = metersToNauticalMiles(
|
||||
destination.distanceTo(cp.position)
|
||||
).toFixed(1);
|
||||
if (!inRange) {
|
||||
return `Out of range (${distance}nm away)`;
|
||||
}
|
||||
const dest = formatLatLng(destination);
|
||||
return `${cp.name} moving ${distance}nm to ${dest} next turn`;
|
||||
}
|
||||
|
||||
interface PrimaryMarkerProps {
|
||||
controlPoint: ControlPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* The primary control point marker. For non-mobile control points, this has
|
||||
* fairly simple behavior: it's a marker in a fixed location that can manage
|
||||
* units and can have missions planned against it.
|
||||
*
|
||||
* For mobile control points, this is a draggable marker. If the control point
|
||||
* has a destination (either because it was dragged after render, or because it
|
||||
* had a destination in the game that was loaded), the unit management and
|
||||
* mission planning behaviors are delegated to SecondaryMarker, and the primary
|
||||
* marker becomes only a destination marker. It can be dragged to change the
|
||||
* destination, and can be right clicked to cancel movement.
|
||||
*/
|
||||
function PrimaryMarker(props: PrimaryMarkerProps) {
|
||||
// We can't use normal state to update the marker tooltip or the line points
|
||||
// because if we set any state in the drag event it will re-render this
|
||||
// component and all children, interrupting dragging. Instead, keep refs to
|
||||
// the objects and mutate them directly.
|
||||
//
|
||||
// For the same reason, the path is owned by this component, because updating
|
||||
// sibling state would be messy. Lifting the state into the parent would still
|
||||
// cause this component to redraw.
|
||||
const markerRef = useRef<LMarker | null>(null);
|
||||
const pathRef = useRef<MovementPathHandle | null>(null);
|
||||
|
||||
const [hasDestination, setHasDestination] = useState<boolean>(
|
||||
props.controlPoint.destination != null
|
||||
);
|
||||
const [position, setPosition] = useState<LatLngLiteral>(
|
||||
props.controlPoint.destination
|
||||
? props.controlPoint.destination
|
||||
: props.controlPoint.position
|
||||
);
|
||||
|
||||
const setDestination = useCallback((destination: LatLng) => {
|
||||
setPosition(destination);
|
||||
setHasDestination(true);
|
||||
}, []);
|
||||
|
||||
const resetDestination = useCallback(() => {
|
||||
setPosition(props.controlPoint.position);
|
||||
setHasDestination(false);
|
||||
}, [props]);
|
||||
|
||||
const [putDestination, { isLoading }] =
|
||||
useSetControlPointDestinationMutation();
|
||||
const [cancelTravel] = useClearControlPointDestinationMutation();
|
||||
|
||||
useEffect(() => {
|
||||
markerRef.current?.setTooltipContent(
|
||||
props.controlPoint.destination
|
||||
? destinationTooltipText(
|
||||
props.controlPoint,
|
||||
props.controlPoint.destination,
|
||||
true
|
||||
)
|
||||
: ReactDOMServer.renderToString(
|
||||
<LocationTooltipText name={props.controlPoint.name} />
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const locationClickHandlers = makeLocationMarkerEventHandlers(
|
||||
props.controlPoint
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Marker
|
||||
position={position}
|
||||
icon={iconForControlPoint(props.controlPoint)}
|
||||
draggable={!isLoading}
|
||||
autoPan
|
||||
// We might draw other markers on top of the CP. The tooltips from the
|
||||
// other markers are helpful so we want to keep them, but make sure the CP
|
||||
// is always the clickable thing.
|
||||
zIndexOffset={1000}
|
||||
opacity={props.controlPoint.destination ? 0.5 : 1}
|
||||
ref={(ref) => {
|
||||
if (ref != null) {
|
||||
markerRef.current = ref;
|
||||
}
|
||||
}}
|
||||
eventHandlers={{
|
||||
click: () => {
|
||||
if (!hasDestination) {
|
||||
locationClickHandlers.click();
|
||||
}
|
||||
},
|
||||
contextmenu: () => {
|
||||
if (props.controlPoint.destination) {
|
||||
cancelTravel({ cpId: props.controlPoint.id }).then(() => {
|
||||
resetDestination();
|
||||
});
|
||||
} else {
|
||||
locationClickHandlers.contextmenu();
|
||||
}
|
||||
},
|
||||
drag: (event) => {
|
||||
const destination = event.target.getLatLng();
|
||||
backend
|
||||
.get(
|
||||
`/control-points/${props.controlPoint.id}/destination-in-range?lat=${destination.lat}&lng=${destination.lng}`
|
||||
)
|
||||
.then((inRange) => {
|
||||
markerRef.current?.setTooltipContent(
|
||||
destinationTooltipText(
|
||||
props.controlPoint,
|
||||
destination,
|
||||
inRange.data
|
||||
)
|
||||
);
|
||||
});
|
||||
pathRef.current?.setDestination(destination);
|
||||
},
|
||||
dragend: async (event) => {
|
||||
const currentPosition = new LatLng(position.lat, position.lng);
|
||||
const destination = event.target.getLatLng();
|
||||
setDestination(destination);
|
||||
try {
|
||||
await putDestination({
|
||||
cpId: props.controlPoint.id,
|
||||
body: { lat: destination.lat, lng: destination.lng },
|
||||
}).unwrap();
|
||||
} catch (error) {
|
||||
console.error("setDestination failed", error);
|
||||
setDestination(currentPosition);
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tooltip />
|
||||
</Marker>
|
||||
<MovementPath
|
||||
source={props.controlPoint.position}
|
||||
destination={position}
|
||||
ref={pathRef}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface SecondaryMarkerProps {
|
||||
controlPoint: ControlPoint;
|
||||
destination: LatLngLiteral | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The secondary marker for a control point. The secondary marker will only be
|
||||
* shown when the control point has a destination set. For mobile control
|
||||
* points, the primary marker is draggable, and the secondary marker will be
|
||||
* shown at the current location iff the control point has been dragged. The
|
||||
* secondary marker is also the marker that has the normal control point
|
||||
* interaction options (mission planning and unit management).
|
||||
*/
|
||||
function SecondaryMarker(props: SecondaryMarkerProps) {
|
||||
if (!props.destination) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return <StaticControlPoint controlPoint={props.controlPoint} />;
|
||||
}
|
||||
|
||||
interface MobileControlPointProps {
|
||||
controlPoint: ControlPoint;
|
||||
}
|
||||
|
||||
export const MobileControlPoint = (props: MobileControlPointProps) => {
|
||||
return (
|
||||
<>
|
||||
<PrimaryMarker
|
||||
controlPoint={props.controlPoint}
|
||||
key={props.controlPoint.destination ? 0 : 1}
|
||||
/>
|
||||
<SecondaryMarker
|
||||
controlPoint={props.controlPoint}
|
||||
destination={props.controlPoint.destination}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
35
client/src/components/controlpoints/MovementPath.tsx
Normal file
35
client/src/components/controlpoints/MovementPath.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { LatLngLiteral, Polyline as LPolyline } from "leaflet";
|
||||
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||
import { Polyline } from "react-leaflet";
|
||||
|
||||
interface MovementPathProps {
|
||||
source: LatLngLiteral;
|
||||
destination: LatLngLiteral;
|
||||
}
|
||||
|
||||
export interface MovementPathHandle {
|
||||
setDestination: (destination: LatLngLiteral) => void;
|
||||
}
|
||||
|
||||
export const MovementPath = forwardRef<MovementPathHandle, MovementPathProps>(
|
||||
(props: MovementPathProps, ref) => {
|
||||
const lineRef = useRef<LPolyline | null>(null);
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
setDestination: (destination: LatLngLiteral) => {
|
||||
lineRef.current?.setLatLngs([props.source, destination]);
|
||||
},
|
||||
}),
|
||||
[props]
|
||||
);
|
||||
return (
|
||||
<Polyline
|
||||
positions={[props.source, props.destination]}
|
||||
weight={1}
|
||||
color="#80BA80"
|
||||
ref={lineRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
27
client/src/components/controlpoints/StaticControlPoint.tsx
Normal file
27
client/src/components/controlpoints/StaticControlPoint.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ControlPoint } from "../../api/_liberationApi";
|
||||
import { makeLocationMarkerEventHandlers } from "./EventHandlers";
|
||||
import { iconForControlPoint } from "./Icons";
|
||||
import LocationTooltipText from "./LocationTooltipText";
|
||||
import { Marker, Tooltip } from "react-leaflet";
|
||||
|
||||
interface StaticControlPointProps {
|
||||
controlPoint: ControlPoint;
|
||||
}
|
||||
|
||||
export const StaticControlPoint = (props: StaticControlPointProps) => {
|
||||
return (
|
||||
<Marker
|
||||
position={props.controlPoint.position}
|
||||
icon={iconForControlPoint(props.controlPoint)}
|
||||
// We might draw other markers on top of the CP. The tooltips from the
|
||||
// other markers are helpful so we want to keep them, but make sure the CP
|
||||
// is always the clickable thing.
|
||||
zIndexOffset={1000}
|
||||
eventHandlers={makeLocationMarkerEventHandlers(props.controlPoint)}
|
||||
>
|
||||
<Tooltip>
|
||||
<LocationTooltipText name={props.controlPoint.name} />
|
||||
</Tooltip>
|
||||
</Marker>
|
||||
);
|
||||
};
|
||||
@@ -4,18 +4,20 @@ import { SocketProvider } from "./components/socketprovider/socketprovider";
|
||||
import "./index.css";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
ReactDOM.render(
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<SocketProvider>
|
||||
<App />
|
||||
</SocketProvider>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from .game import Game
|
||||
from .version import VERSION
|
||||
from .migrator import Migrator
|
||||
|
||||
@@ -2,8 +2,9 @@ from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Iterator, Optional
|
||||
from game.data.groups import GroupTask
|
||||
|
||||
from game.armedforces.forcegroup import ForceGroup
|
||||
from game.data.groups import GroupTask
|
||||
from game.layout import LAYOUTS
|
||||
from game.profiling import logged_duration
|
||||
|
||||
|
||||
@@ -10,19 +10,18 @@ import yaml
|
||||
from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType, VehicleType
|
||||
|
||||
from game.data.groups import GroupTask
|
||||
from game.data.radar_db import UNITS_WITH_RADAR
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.helpers import static_type_from_name
|
||||
from game.dcs.shipunittype import ShipUnitType
|
||||
from game.dcs.unittype import UnitType
|
||||
from game.layout import LAYOUTS
|
||||
from game.layout.layout import TgoLayout, TgoLayoutUnitGroup
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.theater.theatergroundobject import (
|
||||
IadsGroundObject,
|
||||
IadsBuildingGroundObject,
|
||||
NavalGroundObject,
|
||||
)
|
||||
from game.layout import LAYOUTS
|
||||
from game.layout.layout import TgoLayout, TgoLayoutUnitGroup
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.theater.theatergroup import IadsGroundGroup, IadsRole, TheaterGroup
|
||||
from game.utils import escape_string_for_lua
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from dcs.helicopters import (
|
||||
CH_47D,
|
||||
CH_53E,
|
||||
Ka_50,
|
||||
Ka_50_3,
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
Mi_26,
|
||||
@@ -45,11 +46,13 @@ from dcs.planes import (
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_117A,
|
||||
F_14A,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_15E,
|
||||
F_16A,
|
||||
F_16A_MLU,
|
||||
F_16C_50,
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
@@ -116,18 +119,35 @@ from dcs.planes import (
|
||||
Tu_95MS,
|
||||
WingLoong_I,
|
||||
Yak_40,
|
||||
MB_339A,
|
||||
)
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
from pydcs_extensions.f104.f104 import VSN_F104G, VSN_F104S, VSN_F104S_AG
|
||||
from pydcs_extensions.a6a.a6a import VSN_A6A
|
||||
from pydcs_extensions.f100.f100 import VSN_F100
|
||||
from pydcs_extensions.f104.f104 import VSN_F104C, VSN_F104G, VSN_F104S, VSN_F104S_AG
|
||||
from pydcs_extensions.f105.f105 import VSN_F105D, VSN_F105G
|
||||
from pydcs_extensions.f15d.f15d import F_15D
|
||||
from pydcs_extensions.f16i_idf.f16i_idf import (
|
||||
F_16D_50,
|
||||
F_16D_52,
|
||||
F_16D_50_NS,
|
||||
F_16D_52_NS,
|
||||
F_16D_Barak_30,
|
||||
F_16D_Barak_40,
|
||||
F_16I,
|
||||
)
|
||||
from pydcs_extensions.f22a.f22a import F_22A
|
||||
from pydcs_extensions.f4b.f4b import VSN_F4B, VSN_F4C
|
||||
from pydcs_extensions.f84g.f84g import VSN_F84G
|
||||
from pydcs_extensions.fa18efg.fa18efg import FA_18E, FA_18F, EA_18G
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
|
||||
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_BVR, JAS39Gripen_AG
|
||||
from pydcs_extensions.su30.su30 import Su_30MKA, Su_30MKI, Su_30MKM, Su_30SM
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
from pydcs_extensions.ov10a.ov10a import Bronco_OV_10A
|
||||
from pydcs_extensions.uh60l.uh60l import KC130J, UH_60L
|
||||
from .flighttype import FlightType
|
||||
|
||||
@@ -138,13 +158,14 @@ from .flighttype import FlightType
|
||||
# factions that also have F-4s should not.
|
||||
|
||||
# Used for CAP, Escort, and intercept if there is not a specialised aircraft available
|
||||
CAP_CAPABLE = [
|
||||
ESCORT_CAPABLE = [
|
||||
Su_57,
|
||||
F_22A,
|
||||
F_15C,
|
||||
F_15D,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
F_14A,
|
||||
Su_33,
|
||||
J_11A,
|
||||
Su_30,
|
||||
@@ -155,11 +176,24 @@ CAP_CAPABLE = [
|
||||
Su_27,
|
||||
MiG_29S,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak_40,
|
||||
F_16D_Barak_30,
|
||||
F_16D_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
FA_18E,
|
||||
FA_18F,
|
||||
FA_18C_hornet,
|
||||
JF_17,
|
||||
JAS39Gripen_BVR,
|
||||
JAS39Gripen,
|
||||
F_16A_MLU,
|
||||
F_16A,
|
||||
F_4E,
|
||||
VSN_F4C,
|
||||
VSN_F4B,
|
||||
MiG_31,
|
||||
MiG_25PD,
|
||||
MiG_29G,
|
||||
@@ -181,12 +215,14 @@ CAP_CAPABLE = [
|
||||
F_5E_3,
|
||||
VSN_F104S,
|
||||
VSN_F104G,
|
||||
VSN_F104C,
|
||||
MiG_19P,
|
||||
VSN_F100,
|
||||
A_4E_C,
|
||||
F_86F_Sabre,
|
||||
MiG_15bis,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
VSN_F84G,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
@@ -201,6 +237,11 @@ CAP_CAPABLE = [
|
||||
I_16,
|
||||
]
|
||||
|
||||
# Types to be skipped for escorts
|
||||
CAP_CAPABLE = ESCORT_CAPABLE + [
|
||||
L_39ZA,
|
||||
]
|
||||
|
||||
|
||||
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
|
||||
CAS_CAPABLE = [
|
||||
@@ -214,6 +255,15 @@ CAS_CAPABLE = [
|
||||
F_15D,
|
||||
F_15E,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak_40,
|
||||
F_16D_Barak_30,
|
||||
F_16D_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
FA_18E,
|
||||
FA_18F,
|
||||
FA_18C_hornet,
|
||||
Tornado_GR4,
|
||||
Tornado_IDS,
|
||||
@@ -221,6 +271,8 @@ CAS_CAPABLE = [
|
||||
JF_17,
|
||||
AV8BNA,
|
||||
A_10A,
|
||||
F_16A_MLU,
|
||||
F_16A,
|
||||
B_1B,
|
||||
A_4E_C,
|
||||
F_14B,
|
||||
@@ -248,6 +300,7 @@ CAS_CAPABLE = [
|
||||
SA342M,
|
||||
SA342L,
|
||||
Ka_50,
|
||||
Ka_50_3,
|
||||
Mi_28N,
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
@@ -268,9 +321,9 @@ CAS_CAPABLE = [
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
MB_339A,
|
||||
Bronco_OV_10A,
|
||||
UH_1H,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
P_47D_40,
|
||||
@@ -288,21 +341,39 @@ CAS_CAPABLE = [
|
||||
WingLoong_I,
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator,
|
||||
VSN_A6A,
|
||||
VSN_F4C,
|
||||
VSN_F4B,
|
||||
VSN_F100,
|
||||
VSN_F105G,
|
||||
VSN_F105D,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
VSN_F104C,
|
||||
VSN_F84G,
|
||||
]
|
||||
|
||||
|
||||
# Aircraft used for SEAD and SEAD Escort tasks. Must be capable of the CAS DCS task.
|
||||
SEAD_CAPABLE = [
|
||||
SEAD_ESCORT_CAPABLE = [
|
||||
JF_17,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak_40,
|
||||
F_16D_Barak_30,
|
||||
F_16D_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
EA_18G,
|
||||
FA_18E,
|
||||
FA_18F,
|
||||
FA_18C_hornet,
|
||||
Tornado_IDS,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
F_4E,
|
||||
A_4E_C,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
JAS39Gripen_AG,
|
||||
AV8BNA,
|
||||
Su_24M,
|
||||
@@ -315,14 +386,22 @@ SEAD_CAPABLE = [
|
||||
Su_30SM,
|
||||
MiG_27K,
|
||||
Tornado_GR4,
|
||||
VSN_F105G,
|
||||
VSN_F100,
|
||||
]
|
||||
|
||||
|
||||
SEAD_CAPABLE = SEAD_ESCORT_CAPABLE + [
|
||||
F_16A_MLU,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
]
|
||||
|
||||
# Aircraft used for DEAD tasks. Must be capable of the CAS DCS task.
|
||||
DEAD_CAPABLE = SEAD_CAPABLE + [
|
||||
AJS37,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
F_16A,
|
||||
F_15E,
|
||||
JAS39Gripen_AG,
|
||||
B_1B,
|
||||
B_52H,
|
||||
@@ -331,19 +410,25 @@ DEAD_CAPABLE = SEAD_CAPABLE + [
|
||||
H_6J,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
Bronco_OV_10A,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
MosquitoFBMkVI,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
VSN_A6A,
|
||||
VSN_F105D,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
VSN_F104C,
|
||||
VSN_F100,
|
||||
VSN_F84G,
|
||||
]
|
||||
|
||||
|
||||
@@ -361,9 +446,19 @@ STRIKE_CAPABLE = [
|
||||
AJS37,
|
||||
Tornado_GR4,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak_40,
|
||||
F_16D_Barak_30,
|
||||
F_16D_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
FA_18E,
|
||||
FA_18F,
|
||||
FA_18C_hornet,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
F_16A_MLU,
|
||||
F_16A,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
@@ -388,8 +483,11 @@ STRIKE_CAPABLE = [
|
||||
F_4E,
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
VSN_F4C,
|
||||
VSN_F4B,
|
||||
S_3B,
|
||||
A_4E_C,
|
||||
Bronco_OV_10A,
|
||||
M_2000C,
|
||||
Mirage_F1B,
|
||||
Mirage_F1BE,
|
||||
@@ -406,11 +504,10 @@ STRIKE_CAPABLE = [
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
MB_339A,
|
||||
B_17G,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
@@ -422,6 +519,14 @@ STRIKE_CAPABLE = [
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
VSN_A6A,
|
||||
VSN_F100,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
VSN_F104C,
|
||||
VSN_F105G,
|
||||
VSN_F105D,
|
||||
VSN_F84G,
|
||||
]
|
||||
|
||||
|
||||
@@ -430,8 +535,12 @@ ANTISHIP_CAPABLE = [
|
||||
Tu_142,
|
||||
Tu_22M3,
|
||||
H_6J,
|
||||
FA_18E,
|
||||
FA_18F,
|
||||
FA_18C_hornet,
|
||||
JAS39Gripen_AG,
|
||||
F_16A_MLU,
|
||||
F_16A,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
JF_17,
|
||||
@@ -460,6 +569,14 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
JF_17,
|
||||
Tornado_IDS,
|
||||
M_2000C,
|
||||
Mirage_F1B,
|
||||
Mirage_F1BE,
|
||||
Mirage_F1CE,
|
||||
Mirage_F1EE,
|
||||
Mirage_F1EQ,
|
||||
Mirage_F1M_CE,
|
||||
Mirage_F1M_EE,
|
||||
Mirage_F1CT,
|
||||
H_6J,
|
||||
B_1B,
|
||||
B_52H,
|
||||
@@ -468,9 +585,19 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
F_15E,
|
||||
AJS37,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak_40,
|
||||
F_16D_Barak_30,
|
||||
F_16D_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
FA_18E,
|
||||
FA_18F,
|
||||
FA_18C_hornet,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
F_16A_MLU,
|
||||
F_16A,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
@@ -495,17 +622,12 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
F_4E,
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
VSN_F4C,
|
||||
VSN_F4B,
|
||||
S_3B,
|
||||
A_4E_C,
|
||||
Bronco_OV_10A,
|
||||
M_2000C,
|
||||
Mirage_F1B,
|
||||
Mirage_F1BE,
|
||||
Mirage_F1CE,
|
||||
Mirage_F1EE,
|
||||
Mirage_F1EQ,
|
||||
Mirage_F1M_CE,
|
||||
Mirage_F1M_EE,
|
||||
Mirage_F1CT,
|
||||
MiG_27K,
|
||||
MiG_21Bis,
|
||||
MiG_15bis,
|
||||
@@ -513,11 +635,10 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
MB_339A,
|
||||
B_17G,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
@@ -529,6 +650,13 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
VSN_A6A,
|
||||
VSN_F105G,
|
||||
VSN_F105D,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
VSN_F104C,
|
||||
VSN_F100,
|
||||
]
|
||||
|
||||
# For any aircraft that isn't necessarily directly involved in strike
|
||||
@@ -564,7 +692,6 @@ AIR_ASSAULT_CAPABLE = [
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
Hercules,
|
||||
C_47,
|
||||
]
|
||||
|
||||
DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
|
||||
@@ -605,7 +732,7 @@ def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
|
||||
elif task == FlightType.SEAD:
|
||||
return SEAD_CAPABLE
|
||||
elif task == FlightType.SEAD_ESCORT:
|
||||
return SEAD_CAPABLE
|
||||
return SEAD_ESCORT_CAPABLE
|
||||
elif task == FlightType.DEAD:
|
||||
return DEAD_CAPABLE
|
||||
elif task == FlightType.OCA_AIRCRAFT:
|
||||
@@ -615,7 +742,7 @@ def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
|
||||
elif task == FlightType.STRIKE:
|
||||
return STRIKE_CAPABLE
|
||||
elif task == FlightType.ESCORT:
|
||||
return CAP_CAPABLE
|
||||
return ESCORT_CAPABLE
|
||||
elif task == FlightType.AEWC:
|
||||
return AEWC_CAPABLE
|
||||
elif task == FlightType.REFUELING:
|
||||
|
||||
@@ -5,12 +5,17 @@ from datetime import datetime, timedelta
|
||||
from typing import Any, List, Optional, TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
from dcs.planes import C_101CC, C_101EB, Su_33
|
||||
from dcs.planes import C_101CC, C_101EB, Su_33, FA_18C_hornet
|
||||
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from .flightroster import FlightRoster
|
||||
from .flightstate import FlightState, Navigating, Uninitialized
|
||||
from .flightstate.killed import Killed
|
||||
from .loadouts import Loadout
|
||||
from .loadouts import Loadout, Weapon
|
||||
from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
|
||||
from ..radio.TacanContainer import TacanContainer
|
||||
from ..radio.radios import RadioFrequency
|
||||
from ..radio.tacan import TacanChannel
|
||||
from ..sidc import (
|
||||
Entity,
|
||||
SidcDescribable,
|
||||
@@ -18,9 +23,9 @@ from ..sidc import (
|
||||
Status,
|
||||
SymbolSet,
|
||||
)
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
from game.sim.simulationresults import SimulationResults
|
||||
from game.squadrons import Squadron, Pilot
|
||||
@@ -32,8 +37,10 @@ if TYPE_CHECKING:
|
||||
from .package import Package
|
||||
from .starttype import StartType
|
||||
|
||||
F18_TGP_PYLON: int = 4
|
||||
|
||||
class Flight(SidcDescribable):
|
||||
|
||||
class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
|
||||
def __init__(
|
||||
self,
|
||||
package: Package,
|
||||
@@ -46,6 +53,9 @@ class Flight(SidcDescribable):
|
||||
custom_name: Optional[str] = None,
|
||||
cargo: Optional[TransferOrder] = None,
|
||||
roster: Optional[FlightRoster] = None,
|
||||
frequency: Optional[RadioFrequency] = None,
|
||||
channel: Optional[TacanChannel] = None,
|
||||
callsign: Optional[str] = None,
|
||||
) -> None:
|
||||
self.id = uuid.uuid4()
|
||||
self.package = package
|
||||
@@ -63,6 +73,12 @@ class Flight(SidcDescribable):
|
||||
self.start_type = start_type
|
||||
self.use_custom_loadout = False
|
||||
self.custom_name = custom_name
|
||||
self.group_id: int = 0
|
||||
|
||||
self.frequency = frequency
|
||||
if self.unit_type.dcs_unit_type.tacan:
|
||||
self.tacan = channel
|
||||
self.tcn_name = callsign
|
||||
|
||||
# Only used by transport missions.
|
||||
self.cargo = cargo
|
||||
@@ -86,10 +102,25 @@ class Flight(SidcDescribable):
|
||||
|
||||
self._flight_plan_builder = FlightPlanBuilderTypes.for_flight(self)(self)
|
||||
|
||||
is_f18 = self.squadron.aircraft.dcs_unit_type.id == FA_18C_hornet.id
|
||||
on_land = not self.squadron.location.is_fleet
|
||||
if on_land and is_f18 and self.coalition.game.settings.atflir_autoswap:
|
||||
self.loadout.pylons[F18_TGP_PYLON] = Weapon.with_clsid(
|
||||
str(
|
||||
FA_18C_hornet.Pylon4.AN_AAQ_28_LITENING___Targeting_Pod_[1]["clsid"]
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def flight_plan(self) -> FlightPlan[Any]:
|
||||
return self._flight_plan_builder.get_or_build()
|
||||
|
||||
def degrade_to_custom_flight_plan(self) -> None:
|
||||
from .flightplans.custom import Builder as CustomBuilder
|
||||
|
||||
self._flight_plan_builder = CustomBuilder(self, self.flight_plan.waypoints[1:])
|
||||
self.recreate_flight_plan()
|
||||
|
||||
def __getstate__(self) -> dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
# Avoid persisting the flight state since that's not (currently) used outside
|
||||
@@ -143,6 +174,10 @@ class Flight(SidcDescribable):
|
||||
def is_helo(self) -> bool:
|
||||
return self.unit_type.dcs_unit_type.helicopter
|
||||
|
||||
@property
|
||||
def is_hercules(self) -> bool:
|
||||
return self.unit_type == AircraftType.named("C-130J-30 Super Hercules")
|
||||
|
||||
@property
|
||||
def from_cp(self) -> ControlPoint:
|
||||
return self.departure
|
||||
@@ -179,17 +214,18 @@ class Flight(SidcDescribable):
|
||||
return Su_33.fuel_max * 0.8
|
||||
elif unit_type in {C_101EB, C_101CC}:
|
||||
return unit_type.fuel_max * 0.5
|
||||
elif unit_type == Hercules:
|
||||
return unit_type.fuel_max * 0.75
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self) -> str:
|
||||
string = f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
return f"{self.custom_name} - {string}"
|
||||
return string
|
||||
|
||||
def abort(self) -> None:
|
||||
from .flightplans.rtb import RtbFlightPlan
|
||||
@@ -247,3 +283,15 @@ class Flight(SidcDescribable):
|
||||
|
||||
def recreate_flight_plan(self) -> None:
|
||||
self._flight_plan_builder.regenerate()
|
||||
|
||||
@staticmethod
|
||||
def clone_flight(flight: Flight) -> Flight:
|
||||
return Flight(
|
||||
flight.package,
|
||||
flight.country,
|
||||
flight.squadron,
|
||||
flight.count,
|
||||
flight.flight_type,
|
||||
flight.start_type,
|
||||
flight.divert,
|
||||
)
|
||||
|
||||
20
game/ato/flightplans/_common_ctld.py
Normal file
20
game/ato/flightplans/_common_ctld.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Union, TYPE_CHECKING, Tuple
|
||||
|
||||
from game.theater.interfaces.CTLD import CTLD
|
||||
from game.theater import ControlPoint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dcs import Point
|
||||
|
||||
|
||||
def generate_random_ctld_point(cp: Union[ControlPoint, CTLD]) -> Point:
|
||||
if isinstance(cp, CTLD) and cp.ctld_zones:
|
||||
zone: Tuple[Point, float] = random.choice(cp.ctld_zones)
|
||||
pos, radius = zone
|
||||
return pos.random_point_within(radius)
|
||||
elif isinstance(cp, CTLD) and isinstance(cp, ControlPoint):
|
||||
return cp.position.random_point_within(2000, 200)
|
||||
raise RuntimeError("Could not generate CTLD point")
|
||||
@@ -4,29 +4,44 @@ from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Iterator, TYPE_CHECKING, Type
|
||||
|
||||
from game.ato.flightplans.airlift import AirliftLayout
|
||||
from game.ato.flightplans.standard import StandardFlightPlan
|
||||
from ._common_ctld import generate_random_ctld_point
|
||||
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
|
||||
from game.theater.controlpoint import ControlPointType
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.utils import Distance, feet, meters
|
||||
from .ibuilder import IBuilder
|
||||
from .planningerror import PlanningError
|
||||
from .uizonedisplay import UiZone, UiZoneDisplay
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ..flightwaypoint import FlightWaypointType
|
||||
from ...theater.interfaces.CTLD import CTLD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dcs import Point
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirAssaultLayout(AirliftLayout):
|
||||
class AirAssaultLayout(StandardLayout):
|
||||
# The pickup point is optional because we don't always need to load the cargo. When
|
||||
# departing from a carrier, LHA, or off-map spawn, the cargo is pre-loaded.
|
||||
pickup: FlightWaypoint | None
|
||||
nav_to_ingress: list[FlightWaypoint]
|
||||
ingress: FlightWaypoint
|
||||
drop_off: FlightWaypoint | None
|
||||
# This is an implementation detail used by CTLD. The aircraft will not go to this
|
||||
# waypoint. It is used by CTLD as the destination for unloaded troops.
|
||||
target: FlightWaypoint
|
||||
nav_to_home: list[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to_pickup
|
||||
if self.pickup:
|
||||
if self.pickup is not None:
|
||||
yield self.pickup
|
||||
yield from self.nav_to_drop_off
|
||||
yield self.drop_off
|
||||
yield from self.nav_to_ingress
|
||||
yield self.ingress
|
||||
if self.drop_off is not None:
|
||||
yield self.drop_off
|
||||
yield self.target
|
||||
yield from self.nav_to_home
|
||||
yield self.arrival
|
||||
@@ -35,14 +50,16 @@ class AirAssaultLayout(AirliftLayout):
|
||||
yield self.bullseye
|
||||
|
||||
|
||||
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
|
||||
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.drop_off
|
||||
if self.flight.is_helo and self.layout.drop_off is not None:
|
||||
return self.layout.drop_off
|
||||
return self.layout.target
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
if waypoint == self.tot_waypoint:
|
||||
@@ -53,65 +70,78 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
|
||||
return None
|
||||
|
||||
@property
|
||||
def engagement_distance(self) -> Distance:
|
||||
# The radius of the WaypointZone created at the target location
|
||||
def ctld_target_zone_radius(self) -> Distance:
|
||||
return meters(2500)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
|
||||
def ui_zone(self) -> UiZone:
|
||||
return UiZone(
|
||||
[self.layout.target.position],
|
||||
self.ctld_target_zone_radius,
|
||||
)
|
||||
|
||||
|
||||
class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
def layout(self) -> AirAssaultLayout:
|
||||
if not self.flight.is_helo and not self.flight.is_hercules:
|
||||
raise PlanningError(
|
||||
"Air assault is only usable by helicopters and Anubis' C-130 mod"
|
||||
)
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude
|
||||
altitude_is_agl = self.flight.is_helo
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
|
||||
if not self.flight.is_helo or self.flight.departure.cptype in [
|
||||
if self.flight.is_hercules or self.flight.departure.cptype in [
|
||||
ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||
ControlPointType.LHA_GROUP,
|
||||
ControlPointType.OFF_MAP,
|
||||
]:
|
||||
# Non-Helo flights or Off_Map will be preloaded
|
||||
# Off_Map spawns will be preloaded
|
||||
# Carrier operations load the logistics directly from the carrier
|
||||
pickup = None
|
||||
pickup_position = self.flight.departure.position
|
||||
else:
|
||||
# Create a special pickup zone for Helos from Airbase / FOB
|
||||
pickup = builder.pickup(
|
||||
pickup = builder.pickup_zone(
|
||||
MissionTarget(
|
||||
"Pickup Zone",
|
||||
self.flight.departure.position.random_point_within(1200, 600),
|
||||
self._generate_ctld_pickup(),
|
||||
)
|
||||
)
|
||||
pickup_position = pickup.position
|
||||
assault_area = builder.assault_area(self.package.target)
|
||||
heading = self.package.target.position.heading_between_point(pickup_position)
|
||||
if self.flight.is_hercules:
|
||||
assault_area.only_for_player = False
|
||||
assault_area.alt = feet(1000)
|
||||
|
||||
# TODO: define CTLD dropoff zones in campaign miz?
|
||||
drop_off_zone = MissionTarget(
|
||||
"Dropoff zone",
|
||||
self.package.target.position.point_from_heading(heading, 1200),
|
||||
)
|
||||
dz = builder.dropoff_zone(drop_off_zone) if self.flight.is_helo else None
|
||||
|
||||
return AirAssaultLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to_pickup=builder.nav_path(
|
||||
self.flight.departure.position,
|
||||
pickup_position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
pickup=pickup,
|
||||
nav_to_drop_off=builder.nav_path(
|
||||
nav_to_ingress=builder.nav_path(
|
||||
pickup_position,
|
||||
drop_off_zone.position,
|
||||
self.package.waypoints.ingress,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
drop_off=builder.drop_off(drop_off_zone),
|
||||
stopover=None,
|
||||
ingress=builder.ingress(
|
||||
FlightWaypointType.INGRESS_AIR_ASSAULT,
|
||||
self.package.waypoints.ingress,
|
||||
self.package.target,
|
||||
),
|
||||
drop_off=dz,
|
||||
target=assault_area,
|
||||
nav_to_home=builder.nav_path(
|
||||
drop_off_zone.position,
|
||||
@@ -126,3 +156,7 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
|
||||
def build(self) -> AirAssaultFlightPlan:
|
||||
return AirAssaultFlightPlan(self.flight, self.layout())
|
||||
|
||||
def _generate_ctld_pickup(self) -> Point:
|
||||
assert isinstance(self.flight.departure, CTLD)
|
||||
return generate_random_ctld_point(self.flight.departure)
|
||||
|
||||
@@ -5,24 +5,37 @@ from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from ._common_ctld import generate_random_ctld_point
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.utils import feet
|
||||
from .ibuilder import IBuilder
|
||||
from .planningerror import PlanningError
|
||||
from .standard import StandardFlightPlan, StandardLayout
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ...theater.interfaces.CTLD import CTLD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
from dcs import Point
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirliftLayout(StandardLayout):
|
||||
nav_to_pickup: list[FlightWaypoint]
|
||||
# There will not be a pickup waypoint when the pickup airfield is the departure
|
||||
# airfield for cargo planes, as the cargo is pre-loaded. Helicopters will still pick
|
||||
# up the cargo near the airfield.
|
||||
pickup: FlightWaypoint | None
|
||||
# pickup_zone will be used for player flights to create the CTLD stuff
|
||||
ctld_pickup_zone: FlightWaypoint | None
|
||||
nav_to_drop_off: list[FlightWaypoint]
|
||||
drop_off: FlightWaypoint
|
||||
stopover: FlightWaypoint | None
|
||||
# There will not be a drop-off waypoint when the drop-off airfield and the arrival
|
||||
# airfield is the same for a cargo plane, as planes will land to unload and we don't
|
||||
# want a double landing. Helicopters will still drop their cargo near the airfield
|
||||
# before landing.
|
||||
drop_off: FlightWaypoint | None
|
||||
# drop_off_zone will be used for player flights to create the CTLD stuff
|
||||
ctld_drop_off_zone: FlightWaypoint | None
|
||||
nav_to_home: list[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
@@ -30,10 +43,13 @@ class AirliftLayout(StandardLayout):
|
||||
yield from self.nav_to_pickup
|
||||
if self.pickup is not None:
|
||||
yield self.pickup
|
||||
if self.ctld_pickup_zone is not None:
|
||||
yield self.ctld_pickup_zone
|
||||
yield from self.nav_to_drop_off
|
||||
yield self.drop_off
|
||||
if self.stopover is not None:
|
||||
yield self.stopover
|
||||
if self.drop_off is not None:
|
||||
yield self.drop_off
|
||||
if self.ctld_drop_off_zone is not None:
|
||||
yield self.ctld_drop_off_zone
|
||||
yield from self.nav_to_home
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
@@ -48,7 +64,11 @@ class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.drop_off
|
||||
# The TOT is the time that the cargo will be dropped off. If the drop-off
|
||||
# location is the arrival airfield and this is not a helicopter flight, there
|
||||
# will not be a separate drop-off waypoint; the arrival landing waypoint is the
|
||||
# drop-off waypoint.
|
||||
return self.layout.drop_off or self.layout.arrival
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
# TOT planning isn't really useful for transports. They're behind the front
|
||||
@@ -77,31 +97,26 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
|
||||
pickup = None
|
||||
stopover = None
|
||||
drop_off = None
|
||||
pickup_zone = None
|
||||
drop_off_zone = None
|
||||
|
||||
if cargo.origin != self.flight.departure:
|
||||
pickup = builder.cargo_stop(cargo.origin)
|
||||
if cargo.next_stop != self.flight.arrival:
|
||||
drop_off = builder.cargo_stop(cargo.next_stop)
|
||||
|
||||
if self.flight.is_helo:
|
||||
# Create a pickupzone where the cargo will be spawned
|
||||
pickup_zone = MissionTarget(
|
||||
"Pickup Zone", cargo.origin.position.random_point_within(1000, 200)
|
||||
# Create CTLD Zones for Helo flights
|
||||
pickup_zone = builder.pickup_zone(
|
||||
MissionTarget("Pickup Zone", self._generate_ctld_pickup())
|
||||
)
|
||||
pickup = builder.pickup(pickup_zone)
|
||||
# If The cargo is at the departure controlpoint, the pickup waypoint should
|
||||
# only be created for client flights
|
||||
pickup.only_for_player = cargo.origin == self.flight.departure
|
||||
|
||||
# Create a dropoff zone where the cargo should be dropped
|
||||
drop_off_zone = MissionTarget(
|
||||
"Dropoff zone",
|
||||
cargo.next_stop.position.random_point_within(1000, 200),
|
||||
drop_off_zone = builder.dropoff_zone(
|
||||
MissionTarget("Dropoff zone", self._generate_ctld_dropoff())
|
||||
)
|
||||
drop_off = builder.drop_off(drop_off_zone)
|
||||
|
||||
# Add an additional stopover point so that the flight can refuel
|
||||
stopover = builder.stopover(cargo.next_stop)
|
||||
else:
|
||||
# Fixed Wing will get stopover points for pickup and dropoff
|
||||
if cargo.origin != self.flight.departure:
|
||||
pickup = builder.stopover(cargo.origin, "PICKUP")
|
||||
drop_off = builder.stopover(cargo.next_stop, "DROP OFF")
|
||||
# Show the zone waypoints only to the player
|
||||
pickup_zone.only_for_player = True
|
||||
drop_off_zone.only_for_player = True
|
||||
|
||||
nav_to_pickup = builder.nav_path(
|
||||
self.flight.departure.position,
|
||||
@@ -110,20 +125,11 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||
altitude_is_agl,
|
||||
)
|
||||
|
||||
if self.flight.client_count > 0:
|
||||
# Normal Landing Waypoint
|
||||
arrival = builder.land(self.flight.arrival)
|
||||
else:
|
||||
# The AI Needs another Stopover point to actually fly back to the original
|
||||
# base. Otherwise the Cargo drop will be the new Landing Waypoint and the
|
||||
# AI will end its mission there instead of flying back.
|
||||
# https://forum.dcs.world/topic/211775-landing-to-refuel-and-rearm-the-landingrefuar-waypoint/
|
||||
arrival = builder.stopover(self.flight.arrival, "LANDING")
|
||||
|
||||
return AirliftLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to_pickup=nav_to_pickup,
|
||||
pickup=pickup,
|
||||
ctld_pickup_zone=pickup_zone,
|
||||
nav_to_drop_off=builder.nav_path(
|
||||
cargo.origin.position,
|
||||
cargo.next_stop.position,
|
||||
@@ -131,17 +137,29 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||
altitude_is_agl,
|
||||
),
|
||||
drop_off=drop_off,
|
||||
stopover=stopover,
|
||||
ctld_drop_off_zone=drop_off_zone,
|
||||
nav_to_home=builder.nav_path(
|
||||
cargo.origin.position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
arrival=arrival,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
def build(self) -> AirliftFlightPlan:
|
||||
return AirliftFlightPlan(self.flight, self.layout())
|
||||
|
||||
def _generate_ctld_pickup(self) -> Point:
|
||||
cargo = self.flight.cargo
|
||||
if cargo and cargo.origin and isinstance(cargo.origin, CTLD):
|
||||
return generate_random_ctld_point(cargo.origin)
|
||||
raise RuntimeError("Could not generate CTLD pickup")
|
||||
|
||||
def _generate_ctld_dropoff(self) -> Point:
|
||||
cargo = self.flight.cargo
|
||||
if cargo and cargo.transport and isinstance(cargo.transport.destination, CTLD):
|
||||
return generate_random_ctld_point(cargo.transport.destination)
|
||||
raise RuntimeError("Could not generate CTLD dropoff")
|
||||
|
||||
@@ -10,6 +10,7 @@ from game.utils import Distance, Speed, kph, meters
|
||||
from .ibuilder import IBuilder
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||
from .uizonedisplay import UiZone, UiZoneDisplay
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
|
||||
@@ -28,13 +29,13 @@ class CasLayout(PatrollingLayout):
|
||||
yield self.target
|
||||
yield self.patrol_end
|
||||
yield from self.nav_from
|
||||
yield self.departure
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
|
||||
class CasFlightPlan(PatrollingFlightPlan[CasLayout]):
|
||||
class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
@@ -65,6 +66,12 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout]):
|
||||
def dismiss_escort_at(self) -> FlightWaypoint | None:
|
||||
return self.layout.patrol_end
|
||||
|
||||
def ui_zone(self) -> UiZone:
|
||||
return UiZone(
|
||||
[self.layout.target.position],
|
||||
self.engagement_distance,
|
||||
)
|
||||
|
||||
|
||||
class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
||||
def layout(self) -> CasLayout:
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Type
|
||||
from .flightplan import FlightPlan, Layout
|
||||
from .ibuilder import IBuilder
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from .. import Flight
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -55,9 +56,17 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
|
||||
|
||||
|
||||
class Builder(IBuilder[CustomFlightPlan, CustomLayout]):
|
||||
def __init__(
|
||||
self, flight: Flight, waypoints: list[FlightWaypoint] | None = None
|
||||
) -> None:
|
||||
super().__init__(flight)
|
||||
if waypoints is None:
|
||||
waypoints = []
|
||||
self.waypoints = waypoints
|
||||
|
||||
def layout(self) -> CustomLayout:
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
return CustomLayout(builder.takeoff(self.flight.departure), [])
|
||||
return CustomLayout(builder.takeoff(self.flight.departure), self.waypoints)
|
||||
|
||||
def build(self) -> CustomFlightPlan:
|
||||
return CustomFlightPlan(self.flight, self.layout())
|
||||
|
||||
@@ -24,12 +24,15 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||
ingress, target = builder.escort(
|
||||
self.package.waypoints.ingress, self.package.target
|
||||
)
|
||||
ingress.only_for_player = True
|
||||
target.only_for_player = True
|
||||
hold = builder.hold(self._hold_point())
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
refuel = None
|
||||
if self.package.waypoints.refuel is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
initial = builder.escort_hold(
|
||||
self.package.waypoints.initial, self.doctrine.ingress_altitude
|
||||
)
|
||||
|
||||
return FormationAttackLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
@@ -39,11 +42,12 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||
),
|
||||
join=join,
|
||||
ingress=ingress,
|
||||
initial=initial,
|
||||
targets=[target],
|
||||
split=split,
|
||||
refuel=refuel,
|
||||
nav_from=builder.nav_path(
|
||||
split.position,
|
||||
refuel.position,
|
||||
self.flight.arrival.position,
|
||||
self.doctrine.ingress_altitude,
|
||||
),
|
||||
|
||||
@@ -72,6 +72,15 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
"""A list of all waypoints in the flight plan, in order."""
|
||||
return list(self.iter_waypoints())
|
||||
|
||||
def get_index_of_wpt_by_type(self, wpt_type: FlightWaypointType) -> int:
|
||||
index = 0
|
||||
for wpt in self.waypoints:
|
||||
if wpt and not wpt.only_for_player:
|
||||
index += 1
|
||||
if wpt.waypoint_type == wpt_type:
|
||||
return index
|
||||
return -1
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
"""Iterates over all waypoints in the flight plan, in order."""
|
||||
yield from self.layout.iter_waypoints()
|
||||
|
||||
@@ -26,12 +26,13 @@ from .theaterrefueling import TheaterRefuelingFlightPlan
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato import Flight
|
||||
from game.theater import FrontLine
|
||||
|
||||
|
||||
class FlightPlanBuilderTypes:
|
||||
@staticmethod
|
||||
def for_flight(flight: Flight) -> Type[IBuilder[Any, Any]]:
|
||||
from game.theater import FrontLine
|
||||
|
||||
if flight.flight_type is FlightType.REFUELING:
|
||||
if flight.package.target.is_friendly(flight.squadron.player) or isinstance(
|
||||
flight.package.target, FrontLine
|
||||
|
||||
@@ -4,13 +4,13 @@ from abc import ABC
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
from typing import TYPE_CHECKING, TypeVar, Optional
|
||||
|
||||
from dcs import Point
|
||||
|
||||
from game.flightplan import HoldZoneGeometry
|
||||
from game.theater import MissionTarget
|
||||
from game.utils import Speed, meters
|
||||
from game.utils import Speed, meters, Distance
|
||||
from .flightplan import FlightPlan
|
||||
from .formation import FormationFlightPlan, FormationLayout
|
||||
from .ibuilder import IBuilder
|
||||
@@ -134,6 +134,7 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
class FormationAttackLayout(FormationLayout):
|
||||
ingress: FlightWaypoint
|
||||
targets: list[FlightWaypoint]
|
||||
initial: Optional[FlightWaypoint] = None
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
@@ -141,6 +142,8 @@ class FormationAttackLayout(FormationLayout):
|
||||
yield from self.nav_to
|
||||
yield self.join
|
||||
yield self.ingress
|
||||
if self.initial is not None:
|
||||
yield self.initial
|
||||
yield from self.targets
|
||||
yield self.split
|
||||
if self.refuel is not None:
|
||||
@@ -181,9 +184,11 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
hold = builder.hold(self._hold_point())
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
refuel = None
|
||||
if self.package.waypoints.refuel is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
|
||||
ingress = builder.ingress(
|
||||
ingress_type, self.package.waypoints.ingress, self.package.target
|
||||
)
|
||||
|
||||
return FormationAttackLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
@@ -192,14 +197,12 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
hold.position, join.position, self.doctrine.ingress_altitude
|
||||
),
|
||||
join=join,
|
||||
ingress=builder.ingress(
|
||||
ingress_type, self.package.waypoints.ingress, self.package.target
|
||||
),
|
||||
ingress=ingress,
|
||||
targets=target_waypoints,
|
||||
split=split,
|
||||
refuel=refuel,
|
||||
nav_from=builder.nav_path(
|
||||
split.position,
|
||||
refuel.position,
|
||||
self.flight.arrival.position,
|
||||
self.doctrine.ingress_altitude,
|
||||
),
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar
|
||||
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
|
||||
from game.typeguard import self_type_guard
|
||||
from game.utils import Distance, Speed
|
||||
from .uizonedisplay import UiZone, UiZoneDisplay
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
@@ -37,7 +38,7 @@ class PatrollingLayout(StandardLayout):
|
||||
LayoutT = TypeVar("LayoutT", bound=PatrollingLayout)
|
||||
|
||||
|
||||
class PatrollingFlightPlan(StandardFlightPlan[LayoutT], ABC):
|
||||
class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def patrol_duration(self) -> timedelta:
|
||||
@@ -97,3 +98,9 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], ABC):
|
||||
self, flight_plan: FlightPlan[Any]
|
||||
) -> TypeGuard[PatrollingFlightPlan[Any]]:
|
||||
return True
|
||||
|
||||
def ui_zone(self) -> UiZone:
|
||||
return UiZone(
|
||||
[self.layout.patrol_start.position, self.layout.patrol_end.position],
|
||||
self.engagement_distance,
|
||||
)
|
||||
|
||||
@@ -22,7 +22,6 @@ class SweepLayout(LoiterLayout):
|
||||
nav_to: list[FlightWaypoint]
|
||||
sweep_start: FlightWaypoint
|
||||
sweep_end: FlightWaypoint
|
||||
refuel: FlightWaypoint | None
|
||||
nav_from: list[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
@@ -31,8 +30,6 @@ class SweepLayout(LoiterLayout):
|
||||
yield from self.nav_to
|
||||
yield self.sweep_start
|
||||
yield self.sweep_end
|
||||
if self.refuel is not None:
|
||||
yield self.refuel
|
||||
yield from self.nav_from
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
@@ -92,6 +89,7 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
GroundSpeed.for_flight(self.flight, self.layout.hold.alt),
|
||||
)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.sweep_end_time
|
||||
|
||||
@@ -112,11 +110,6 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
|
||||
|
||||
hold = builder.hold(self._hold_point())
|
||||
|
||||
refuel = None
|
||||
|
||||
if self.package.waypoints is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
|
||||
return SweepLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
hold=hold,
|
||||
@@ -130,7 +123,6 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
|
||||
),
|
||||
sweep_start=start,
|
||||
sweep_end=end,
|
||||
refuel=refuel,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
|
||||
@@ -105,9 +105,11 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
|
||||
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
|
||||
|
||||
refuel = None
|
||||
nav_from_origin = orbit1p
|
||||
|
||||
if self.package.waypoints is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
nav_from_origin = refuel.position
|
||||
|
||||
return TarCapLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
@@ -115,7 +117,7 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
|
||||
self.flight.departure.position, orbit0p, patrol_alt
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
orbit1p, self.flight.arrival.position, patrol_alt
|
||||
nav_from_origin, self.flight.arrival.position, patrol_alt
|
||||
),
|
||||
patrol_start=start,
|
||||
patrol_end=end,
|
||||
|
||||
@@ -17,7 +17,7 @@ class TheaterRefuelingFlightPlan(RefuelingFlightPlan):
|
||||
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
return timedelta(hours=1)
|
||||
return self.flight.coalition.game.settings.desired_tanker_on_station_time
|
||||
|
||||
|
||||
class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
|
||||
|
||||
18
game/ato/flightplans/uizonedisplay.py
Normal file
18
game/ato/flightplans/uizonedisplay.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import abc
|
||||
from dataclasses import dataclass
|
||||
|
||||
from dcs import Point
|
||||
|
||||
from game.utils import Distance
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UiZone:
|
||||
points: list[Point]
|
||||
radius: Distance
|
||||
|
||||
|
||||
class UiZoneDisplay(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def ui_zone(self) -> UiZone:
|
||||
...
|
||||
@@ -14,6 +14,7 @@ from typing import (
|
||||
|
||||
from dcs.mapping import Point, Vector2
|
||||
|
||||
from game.ato.flightplans._common_ctld import generate_random_ctld_point
|
||||
from game.ato.flightwaypoint import AltitudeReference, FlightWaypoint
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.theater import (
|
||||
@@ -23,7 +24,8 @@ from game.theater import (
|
||||
TheaterGroundObject,
|
||||
TheaterUnit,
|
||||
)
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from game.theater.interfaces.CTLD import CTLD
|
||||
from game.utils import Distance, meters, nautical_miles, feet
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
@@ -127,10 +129,9 @@ class WaypointBuilder:
|
||||
position = divert.position
|
||||
altitude_type: AltitudeReference
|
||||
if isinstance(divert, OffMapSpawn):
|
||||
if self.is_helo:
|
||||
altitude = meters(500)
|
||||
else:
|
||||
altitude = self.doctrine.rendezvous_altitude
|
||||
altitude = (
|
||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
)
|
||||
altitude_type = "BARO"
|
||||
else:
|
||||
altitude = meters(0)
|
||||
@@ -225,15 +226,17 @@ class WaypointBuilder:
|
||||
position: Point,
|
||||
objective: MissionTarget,
|
||||
) -> FlightWaypoint:
|
||||
alt = self.doctrine.ingress_altitude
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo:
|
||||
if self.is_helo or self.flight.is_hercules:
|
||||
alt_type = "RADIO"
|
||||
alt = meters(60) if self.is_helo else feet(1000)
|
||||
|
||||
return FlightWaypoint(
|
||||
"INGRESS",
|
||||
ingress_type,
|
||||
position,
|
||||
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
alt,
|
||||
alt_type,
|
||||
description=f"INGRESS on {objective.name}",
|
||||
pretty_name=f"INGRESS on {objective.name}",
|
||||
@@ -304,6 +307,13 @@ class WaypointBuilder:
|
||||
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
|
||||
|
||||
def assault_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
"""A destination waypoint used by air-assault ground troops.
|
||||
|
||||
This waypoint is an implementation detail for CTLD and should not be followed by
|
||||
aircraft.
|
||||
"""
|
||||
# TODO: Add a property that can hide this waypoint from the player's flight
|
||||
# plan.
|
||||
return self._target_area(f"ASSAULT {target.name}", target)
|
||||
|
||||
@staticmethod
|
||||
@@ -415,6 +425,24 @@ class WaypointBuilder:
|
||||
pretty_name="Orbit",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def escort_hold(start: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates custom waypoint for escort flights that need to hold.
|
||||
|
||||
Args:
|
||||
start: Position of the waypoint.
|
||||
altitude: Altitude of the holding pattern.
|
||||
"""
|
||||
|
||||
return FlightWaypoint(
|
||||
"ESCORT HOLD",
|
||||
FlightWaypointType.CUSTOM,
|
||||
start,
|
||||
altitude,
|
||||
description="Anchor and hold at this point",
|
||||
pretty_name="Escort Hold",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a sweep start waypoint.
|
||||
@@ -494,58 +522,54 @@ class WaypointBuilder:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def stopover(stopover: ControlPoint, name: str = "STOPOVER") -> FlightWaypoint:
|
||||
"""Creates a stopover waypoint.
|
||||
|
||||
Args:
|
||||
control_point: Pick up location.
|
||||
def pickup_zone(pick_up: MissionTarget) -> FlightWaypoint:
|
||||
"""Creates a pickup landing zone waypoint
|
||||
This waypoint is used to generate the Trigger Zone used for AirAssault and
|
||||
AirLift using the CTLD plugin (see LogisticsGenerator)
|
||||
"""
|
||||
return FlightWaypoint(
|
||||
name,
|
||||
FlightWaypointType.STOPOVER,
|
||||
stopover.position,
|
||||
meters(0),
|
||||
"RADIO",
|
||||
description=f"Stopover at {stopover}",
|
||||
pretty_name="Stopover location",
|
||||
control_point=stopover,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def pickup(pick_up: MissionTarget) -> FlightWaypoint:
|
||||
"""Creates a cargo pickup waypoint.
|
||||
|
||||
Args:
|
||||
control_point: Pick up location.
|
||||
"""
|
||||
control_point = pick_up if isinstance(pick_up, ControlPoint) else None
|
||||
return FlightWaypoint(
|
||||
"PICKUP",
|
||||
FlightWaypointType.PICKUP,
|
||||
"PICKUPZONE",
|
||||
FlightWaypointType.PICKUP_ZONE,
|
||||
pick_up.position,
|
||||
meters(0),
|
||||
"RADIO",
|
||||
description=f"Pick up cargo from {pick_up.name}",
|
||||
pretty_name="Pick up location",
|
||||
control_point=control_point,
|
||||
pretty_name="Pick-up zone",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def drop_off(drop_off: MissionTarget) -> FlightWaypoint:
|
||||
"""Creates a cargo drop-off waypoint.
|
||||
|
||||
Args:
|
||||
control_point: Drop-off location.
|
||||
def dropoff_zone(drop_off: MissionTarget) -> FlightWaypoint:
|
||||
"""Creates a dropoff landing zone waypoint
|
||||
This waypoint is used to generate the Trigger Zone used for AirAssault and
|
||||
AirLift using the CTLD plugin (see LogisticsGenerator)
|
||||
"""
|
||||
control_point = drop_off if isinstance(drop_off, ControlPoint) else None
|
||||
return FlightWaypoint(
|
||||
"DROP OFF",
|
||||
FlightWaypointType.DROP_OFF,
|
||||
"DROPOFFZONE",
|
||||
FlightWaypointType.DROPOFF_ZONE,
|
||||
drop_off.position,
|
||||
meters(0),
|
||||
"RADIO",
|
||||
description=f"Drop off cargo at {drop_off.name}",
|
||||
pretty_name="Drop off location",
|
||||
pretty_name="Drop-off zone",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def cargo_stop(control_point: ControlPoint) -> FlightWaypoint:
|
||||
"""Creates a cargo stop waypoint.
|
||||
This waypoint is used by AirLift as a landing and stopover waypoint
|
||||
"""
|
||||
if isinstance(control_point, CTLD) and control_point.ctld_zones:
|
||||
pos = generate_random_ctld_point(control_point)
|
||||
else:
|
||||
pos = control_point.position
|
||||
return FlightWaypoint(
|
||||
"CARGOSTOP",
|
||||
FlightWaypointType.CARGO_STOP,
|
||||
pos,
|
||||
meters(0),
|
||||
"RADIO",
|
||||
description=f"Stop for cargo at {control_point.name}",
|
||||
pretty_name="Cargo stop",
|
||||
control_point=control_point,
|
||||
)
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@ class InFlight(FlightState, ABC):
|
||||
FlightWaypointType.INGRESS_OCA_RUNWAY,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
FlightWaypointType.INGRESS_AIR_ASSAULT,
|
||||
}
|
||||
return self.current_waypoint.waypoint_type in contact_types
|
||||
|
||||
|
||||
@@ -40,6 +40,11 @@ class Navigating(InFlight):
|
||||
)
|
||||
|
||||
def estimate_altitude(self) -> tuple[Distance, str]:
|
||||
# This does not behave well when one of the waypoints is AGL and the other is
|
||||
# MSL. We can't really avoid that problem though. We don't know where the ground
|
||||
# is, so conversions between them are impossible, and we do need to use AGL
|
||||
# altitudes for takeoff and landing waypoints (even if we had the runway
|
||||
# elevation, we don't have elevation for FARPs).
|
||||
return (
|
||||
meters(
|
||||
lerp(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from typing import Literal, TYPE_CHECKING
|
||||
@@ -12,8 +11,7 @@ from game.theater.theatergroup import TheaterUnit
|
||||
from game.utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
|
||||
from game.theater import ControlPoint
|
||||
|
||||
AltitudeReference = Literal["BARO", "RADIO"]
|
||||
|
||||
@@ -32,7 +30,7 @@ class FlightWaypoint:
|
||||
# having three names. A short and long form is enough.
|
||||
description: str = ""
|
||||
|
||||
targets: Sequence[MissionTarget | TheaterUnit] = field(default_factory=list)
|
||||
targets: list[TheaterUnit] = field(default_factory=list)
|
||||
obj_name: str = ""
|
||||
pretty_name: str = ""
|
||||
only_for_player: bool = False
|
||||
|
||||
@@ -43,8 +43,9 @@ class FlightWaypointType(IntEnum):
|
||||
DIVERT = 23
|
||||
INGRESS_OCA_RUNWAY = 24
|
||||
INGRESS_OCA_AIRCRAFT = 25
|
||||
PICKUP = 26
|
||||
DROP_OFF = 27
|
||||
PICKUP_ZONE = 26 # Pickup Zone for cargo or troops
|
||||
DROPOFF_ZONE = 27 # Dropoff Zone for cargo or troops
|
||||
BULLSEYE = 28
|
||||
REFUEL = 29 # Should look for nearby tanker to refuel from.
|
||||
STOPOVER = 30 # Stopover landing point using the LandingReFuAr waypoint type
|
||||
CARGO_STOP = 30 # Stopover landing point using the LandingReFuAr waypoint type
|
||||
INGRESS_AIR_ASSAULT = 31
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import datetime
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
from typing import Iterator, Mapping, Optional, TYPE_CHECKING, Type, Dict, Any
|
||||
from typing import Iterator, Optional, TYPE_CHECKING, Type, Dict
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
@@ -19,14 +19,14 @@ class Loadout:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
pylons: Mapping[int, Optional[Weapon]],
|
||||
pylons: Dict[int, Optional[Weapon]],
|
||||
date: Optional[datetime.date],
|
||||
is_custom: bool = False,
|
||||
) -> None:
|
||||
self.name = name
|
||||
# We clear unused pylon entries on initialization, but UI actions can still
|
||||
# cause a pylon to be emptied, so make the optional type explicit.
|
||||
self.pylons: Mapping[int, Optional[Weapon]] = {
|
||||
self.pylons: Dict[int, Optional[Weapon]] = {
|
||||
k: v for k, v in pylons.items() if v is not None
|
||||
}
|
||||
self.date = date
|
||||
@@ -151,10 +151,22 @@ class 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.
|
||||
loadout_names = {t: [f"Liberation {t.value}"] for t in FlightType}
|
||||
loadout_names = {
|
||||
t: [f"Retribution {t.value}", f"Liberation {t.value}"] for t in FlightType
|
||||
}
|
||||
legacy_names = {
|
||||
FlightType.TARCAP: ("CAP HEAVY", "CAP", "Liberation BARCAP"),
|
||||
FlightType.BARCAP: ("CAP HEAVY", "CAP", "Liberation TARCAP"),
|
||||
FlightType.TARCAP: (
|
||||
"CAP HEAVY",
|
||||
"CAP",
|
||||
"Retribution BARCAP",
|
||||
"Liberation BARCAP",
|
||||
),
|
||||
FlightType.BARCAP: (
|
||||
"CAP HEAVY",
|
||||
"CAP",
|
||||
"Retribution TARCAP",
|
||||
"Liberation TARCAP",
|
||||
),
|
||||
FlightType.CAS: ("CAS MAVERICK F", "CAS"),
|
||||
FlightType.STRIKE: ("STRIKE",),
|
||||
FlightType.ANTISHIP: ("ANTISHIP",),
|
||||
|
||||
@@ -13,19 +13,28 @@ from .flightplans.formation import FormationFlightPlan
|
||||
from .flighttype import FlightType
|
||||
from .packagewaypoints import PackageWaypoints
|
||||
from .traveltime import TotEstimator
|
||||
from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
|
||||
from ..radio.radios import RadioFrequency
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
|
||||
|
||||
class Package:
|
||||
class Package(RadioFrequencyContainer):
|
||||
"""A mission package."""
|
||||
|
||||
def __init__(
|
||||
self, target: MissionTarget, db: Database[Flight], auto_asap: bool = False
|
||||
self,
|
||||
target: MissionTarget,
|
||||
db: Database[Flight],
|
||||
auto_asap: bool = False,
|
||||
custom_name: str | None = None,
|
||||
frequency: RadioFrequency | None = None,
|
||||
) -> None:
|
||||
self.target = target
|
||||
self._db = db
|
||||
self.frequency = frequency
|
||||
self.custom_name = custom_name
|
||||
|
||||
# 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
|
||||
@@ -129,6 +138,16 @@ class Package:
|
||||
if not self.flights:
|
||||
self.waypoints = None
|
||||
|
||||
@property
|
||||
def primary_flight(self) -> Optional[Flight]:
|
||||
task = self.primary_task
|
||||
if not task:
|
||||
return None
|
||||
primaries = [x for x in self.flights if x.flight_type == task]
|
||||
if len(primaries) > 0:
|
||||
return primaries[0]
|
||||
return None
|
||||
|
||||
@property
|
||||
def primary_task(self) -> Optional[FlightType]:
|
||||
if not self.flights:
|
||||
@@ -204,3 +223,12 @@ class Package:
|
||||
if flight.departure == airfield:
|
||||
return airfield
|
||||
raise RuntimeError("Could not find any airfield assigned to this package")
|
||||
|
||||
@staticmethod
|
||||
def clone_package(package: Package) -> Package:
|
||||
clone = Package(package.target, package._db, package.auto_asap)
|
||||
clone.time_over_target = package.time_over_target
|
||||
for f in package.flights:
|
||||
cf = Flight.clone_flight(f)
|
||||
clone.add_flight(cf)
|
||||
return clone
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dcs import Point
|
||||
|
||||
from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
||||
from game.flightplan import IpZoneGeometry, JoinZoneGeometry
|
||||
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
|
||||
from game.utils import nautical_miles
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato import Package
|
||||
@@ -18,6 +20,7 @@ if TYPE_CHECKING:
|
||||
class PackageWaypoints:
|
||||
join: Point
|
||||
ingress: Point
|
||||
initial: Point
|
||||
split: Point
|
||||
refuel: Point
|
||||
|
||||
@@ -32,6 +35,11 @@ class PackageWaypoints:
|
||||
coalition,
|
||||
).find_best_ip()
|
||||
|
||||
hdg = package.target.position.heading_between_point(ingress_point)
|
||||
# Generate a waypoint randomly between 7 & 9 NM
|
||||
dist = nautical_miles(random.random() * 2 + 7).meters
|
||||
initial_point = package.target.position.point_from_heading(hdg, dist)
|
||||
|
||||
join_point = JoinZoneGeometry(
|
||||
package.target.position,
|
||||
origin.position,
|
||||
@@ -51,6 +59,7 @@ class PackageWaypoints:
|
||||
return PackageWaypoints(
|
||||
WaypointBuilder.perturb(join_point),
|
||||
ingress_point,
|
||||
initial_point,
|
||||
WaypointBuilder.perturb(join_point),
|
||||
refuel_point,
|
||||
)
|
||||
|
||||
@@ -3,8 +3,8 @@ import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
from dcs.flyingunit import FlyingUnit
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
|
||||
|
||||
def callsign_for_support_unit(group: FlyingGroup[Any]) -> str:
|
||||
@@ -15,7 +15,7 @@ def callsign_for_support_unit(group: FlyingGroup[Any]) -> str:
|
||||
try:
|
||||
return f"Flight {int(raw_callsign)}"
|
||||
except ValueError:
|
||||
return raw_callsign.rstrip("1234567890")
|
||||
return f"{raw_callsign[:-2]} {raw_callsign[-2]}"
|
||||
|
||||
|
||||
def create_group_callsign_from_unit(lead: FlyingUnit) -> str:
|
||||
|
||||
@@ -19,6 +19,7 @@ from game.theater.iadsnetwork.iadsnetwork import IadsNetwork
|
||||
from game.theater.theaterloader import TheaterLoader
|
||||
from game.version import CAMPAIGN_FORMAT_VERSION
|
||||
from .campaignairwingconfig import CampaignAirWingConfig
|
||||
from .campaigngroundconfig import TgoConfig
|
||||
from .mizcampaignloader import MizCampaignLoader
|
||||
|
||||
PERF_FRIENDLY = 0
|
||||
@@ -32,6 +33,7 @@ DEFAULT_BUDGET = 2000
|
||||
class Campaign:
|
||||
name: str
|
||||
menu_thumbnail_dcs_relative_path: Path
|
||||
fallback_icon_path: Path
|
||||
authors: str
|
||||
description: str
|
||||
|
||||
@@ -90,6 +92,7 @@ class Campaign:
|
||||
return cls(
|
||||
data["name"],
|
||||
TheaterLoader(data["theater"].lower()).menu_thumbnail_dcs_relative_path,
|
||||
TheaterLoader(data["theater"].lower()).icon_path,
|
||||
data.get("authors", "???"),
|
||||
data.get("description", ""),
|
||||
(version.major, version.minor),
|
||||
@@ -135,6 +138,13 @@ class Campaign:
|
||||
return CampaignAirWingConfig({})
|
||||
return CampaignAirWingConfig.from_campaign_data(squadron_data, theater)
|
||||
|
||||
def load_ground_forces_config(self) -> TgoConfig:
|
||||
ground_forces = self.data.get("ground_forces", {})
|
||||
if not ground_forces:
|
||||
logging.warning(f"Campaign {self.name} does not define any ground_forces")
|
||||
return TgoConfig({})
|
||||
return TgoConfig.from_campaign_data(ground_forces)
|
||||
|
||||
@property
|
||||
def is_out_of_date(self) -> bool:
|
||||
"""Returns True if this campaign is not up to date with the latest format.
|
||||
@@ -171,7 +181,7 @@ class Campaign:
|
||||
@classmethod
|
||||
def iter_campaign_defs(cls) -> Iterator[Path]:
|
||||
yield from cls.iter_campaigns_in_dir(
|
||||
Path(persistency.base_path()) / "Liberation/Campaigns"
|
||||
Path(persistency.base_path()) / "Retribution/Campaigns"
|
||||
)
|
||||
yield from cls.iter_campaigns_in_dir(Path("resources/campaigns"))
|
||||
|
||||
|
||||
25
game/campaignloader/campaigngroundconfig.py
Normal file
25
game/campaignloader/campaigngroundconfig.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator, Optional
|
||||
|
||||
from game.armedforces.forcegroup import ForceGroup
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TgoConfig:
|
||||
by_original_name: dict[str, ForceGroup]
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return self.by_original_name.__iter__()
|
||||
|
||||
def __getitem__(self, name: str) -> Optional[ForceGroup]:
|
||||
return self.by_original_name.get(name)
|
||||
|
||||
@classmethod
|
||||
def from_campaign_data(cls, data: dict[str, str]) -> TgoConfig:
|
||||
by_original_name: dict[str, ForceGroup] = defaultdict()
|
||||
for tgo_name, force_group in data.items():
|
||||
by_original_name[tgo_name] = ForceGroup.from_preset_group(force_group)
|
||||
return TgoConfig(by_original_name)
|
||||
@@ -1,13 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
from typing import Optional, TYPE_CHECKING, Dict, Union
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.squadrons import Squadron
|
||||
from game.squadrons.squadrondef import SquadronDef
|
||||
from ..ato.flighttype import FlightType
|
||||
from .campaignairwingconfig import CampaignAirWingConfig, SquadronConfig
|
||||
from ..ato.flighttype import FlightType
|
||||
from ..dcs.aircrafttype import AircraftType
|
||||
from ..theater import ControlPoint
|
||||
|
||||
@@ -30,7 +29,6 @@ class DefaultSquadronAssigner:
|
||||
self.coalition.player
|
||||
):
|
||||
for squadron_config in self.config.by_location[control_point]:
|
||||
|
||||
squadron_def = self.override_squadron_defaults(
|
||||
self.find_squadron_for(squadron_config, control_point),
|
||||
squadron_config,
|
||||
@@ -54,7 +52,6 @@ class DefaultSquadronAssigner:
|
||||
def find_squadron_for(
|
||||
self, config: SquadronConfig, control_point: ControlPoint
|
||||
) -> Optional[SquadronDef]:
|
||||
|
||||
for preferred_aircraft in config.aircraft:
|
||||
squadron_def = self.find_preferred_squadron(
|
||||
preferred_aircraft, config.primary, control_point
|
||||
@@ -95,8 +92,9 @@ class DefaultSquadronAssigner:
|
||||
if aircraft not in self.coalition.faction.aircrafts:
|
||||
return None
|
||||
|
||||
lo = self.coalition.faction.liveries_overrides
|
||||
squadron_def = self.find_squadron_for_airframe(aircraft, task, control_point)
|
||||
if squadron_def is not None:
|
||||
if squadron_def is not None and lo.get(aircraft) is None:
|
||||
return squadron_def
|
||||
|
||||
# No premade squadron available for this aircraft that meets the requirements,
|
||||
@@ -154,7 +152,6 @@ class DefaultSquadronAssigner:
|
||||
def override_squadron_defaults(
|
||||
squadron_def: Optional[SquadronDef], config: SquadronConfig
|
||||
) -> Optional[SquadronDef]:
|
||||
|
||||
if squadron_def is None:
|
||||
return None
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ from game.utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater.conflicttheater import ConflictTheater
|
||||
from dcs import Point
|
||||
|
||||
|
||||
class MizCampaignLoader:
|
||||
@@ -107,8 +108,12 @@ class MizCampaignLoader:
|
||||
if self.mission.country(self.RED_COUNTRY.name) is None:
|
||||
self.mission.coalition["red"].add_country(self.RED_COUNTRY)
|
||||
|
||||
def control_point_from_airport(self, airport: Airport) -> ControlPoint:
|
||||
cp = Airfield(airport, self.theater, starts_blue=airport.is_blue())
|
||||
def control_point_from_airport(
|
||||
self, airport: Airport, ctld_zones: List[Tuple[Point, float]]
|
||||
) -> ControlPoint:
|
||||
cp = Airfield(
|
||||
airport, self.theater, starts_blue=airport.is_blue(), ctld_zones=ctld_zones
|
||||
)
|
||||
|
||||
# Use the unlimited aircraft option to determine if an airfield should
|
||||
# be owned by the player when the campaign is "inverted".
|
||||
@@ -245,7 +250,8 @@ class MizCampaignLoader:
|
||||
control_points = {}
|
||||
for airport in self.mission.terrain.airport_list():
|
||||
if airport.is_blue() or airport.is_red():
|
||||
control_point = self.control_point_from_airport(airport)
|
||||
ctld_zones = self.get_ctld_zones(airport.name)
|
||||
control_point = self.control_point_from_airport(airport, ctld_zones)
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
for blue in (False, True):
|
||||
@@ -268,8 +274,13 @@ class MizCampaignLoader:
|
||||
control_point.captured_invert = ship.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for fob in self.fobs(blue):
|
||||
ctld_zones = self.get_ctld_zones(fob.name)
|
||||
control_point = Fob(
|
||||
str(fob.name), fob.position, self.theater, starts_blue=blue
|
||||
str(fob.name),
|
||||
fob.position,
|
||||
self.theater,
|
||||
starts_blue=blue,
|
||||
ctld_zones=ctld_zones,
|
||||
)
|
||||
control_point.captured_invert = fob.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
@@ -460,3 +471,9 @@ class MizCampaignLoader:
|
||||
self.add_preset_locations()
|
||||
self.add_supply_routes()
|
||||
self.add_shipping_lanes()
|
||||
|
||||
def get_ctld_zones(self, prefix: str) -> List[Tuple[Point, float]]:
|
||||
zones = [t for t in self.mission.triggers.zones() if prefix + " CTLD" in t.name]
|
||||
for z in zones:
|
||||
self.mission.triggers.zones().remove(z)
|
||||
return [(z.position, z.radius) for z in zones]
|
||||
|
||||
@@ -4,12 +4,12 @@ import itertools
|
||||
import random
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.ato.ai_flight_planner_db import aircraft_for_task, tasks_for_aircraft
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.squadrons.operatingbases import OperatingBases
|
||||
from game.squadrons.squadrondef import SquadronDef
|
||||
from game.theater import ControlPoint
|
||||
from game.ato.ai_flight_planner_db import aircraft_for_task, tasks_for_aircraft
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.factions.faction import Faction
|
||||
|
||||
@@ -2,8 +2,8 @@ from dataclasses import field, dataclass
|
||||
from enum import Enum, auto
|
||||
from typing import Optional
|
||||
|
||||
from game.theater import MissionTarget
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.theater import MissionTarget
|
||||
|
||||
|
||||
class EscortType(Enum):
|
||||
|
||||
@@ -6,9 +6,9 @@ from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from typing import Iterator, Dict, TYPE_CHECKING
|
||||
|
||||
from game.theater import MissionTarget
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.ato.traveltime import TotEstimator
|
||||
from game.theater import MissionTarget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
|
||||
@@ -5,6 +5,7 @@ import operator
|
||||
from collections.abc import Iterable, Iterator
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from game.ato.closestairfields import ClosestAirfields, ObjectiveDistanceCache
|
||||
from game.theater import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
@@ -20,7 +21,6 @@ from game.theater.theatergroundobject import (
|
||||
IadsBuildingGroundObject,
|
||||
)
|
||||
from game.utils import meters, nautical_miles
|
||||
from game.ato.closestairfields import ClosestAirfields, ObjectiveDistanceCache
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -139,14 +139,6 @@ class ObjectiveFinder:
|
||||
"""Iterates over all active front lines in the theater."""
|
||||
yield from self.game.theater.conflicts()
|
||||
|
||||
def air_assault_targets(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all capturable controlpoints for all active front lines"""
|
||||
if not self.game.settings.plugin_option("ctld"):
|
||||
# Air Assault should only be tasked with CTLD enabled
|
||||
return
|
||||
for front_line in self.front_lines():
|
||||
yield front_line.control_point_hostile_to(self.is_player)
|
||||
|
||||
def vulnerable_control_points(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over friendly CPs that are vulnerable to enemy CPs.
|
||||
|
||||
@@ -178,6 +170,8 @@ class ObjectiveFinder:
|
||||
return self._targets_by_range(airfields)
|
||||
|
||||
def convoys(self) -> Iterator[Convoy]:
|
||||
if self.game.settings.perf_disable_convoys:
|
||||
return
|
||||
for front_line in self.front_lines():
|
||||
yield from self.game.coalition_for(
|
||||
self.is_player
|
||||
|
||||
@@ -7,7 +7,6 @@ from game.commander.tasks.compound.destroyenemygroundunits import (
|
||||
from game.commander.tasks.compound.reduceenemyfrontlinecapacity import (
|
||||
ReduceEnemyFrontLineCapacity,
|
||||
)
|
||||
from game.commander.tasks.primitive.airassault import PlanAirAssault
|
||||
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
@@ -19,7 +18,6 @@ class CaptureBase(CompoundTask[TheaterState]):
|
||||
front_line: FrontLine
|
||||
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
yield [PlanAirAssault(self.enemy_cp(state))]
|
||||
yield [BreakthroughAttack(self.front_line, state.context.coalition.player)]
|
||||
yield [DestroyEnemyGroundUnits(self.front_line)]
|
||||
if self.worth_destroying_ammo_depots(state):
|
||||
|
||||
@@ -4,8 +4,8 @@ from dataclasses import dataclass
|
||||
from game.commander.tasks.compound.attackairinfrastructure import (
|
||||
AttackAirInfrastructure,
|
||||
)
|
||||
from game.commander.tasks.compound.attackbuildings import AttackBuildings
|
||||
from game.commander.tasks.compound.attackbattlepositions import AttackBattlePositions
|
||||
from game.commander.tasks.compound.attackbuildings import AttackBuildings
|
||||
from game.commander.tasks.compound.capturebases import CaptureBases
|
||||
from game.commander.tasks.compound.defendbases import DefendBases
|
||||
from game.commander.tasks.compound.degradeiads import DegradeIads
|
||||
|
||||
@@ -5,6 +5,7 @@ import operator
|
||||
from abc import abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import IntEnum, auto, unique
|
||||
import random
|
||||
from typing import Generic, Iterator, Optional, TYPE_CHECKING, TypeVar, Union
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
@@ -170,7 +171,24 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
|
||||
if not ignore_iads:
|
||||
for iads_threat in self.iter_iads_threats(state):
|
||||
threatened = True
|
||||
# Only consider blue faction flights threatened.
|
||||
# Red might still plan missions into hostile territory,
|
||||
# depending on the aggressiveness setting.
|
||||
if (
|
||||
state.context.coalition.player
|
||||
or random.randint(1, 100)
|
||||
> state.context.coalition.game.settings.opfor_autoplanner_aggressiveness
|
||||
):
|
||||
threatened = True
|
||||
if iads_threat not in state.threatening_air_defenses:
|
||||
state.threatening_air_defenses.append(iads_threat)
|
||||
return not threatened
|
||||
|
||||
def get_flight_size(self) -> int:
|
||||
settings = self.target.coalition.game.settings
|
||||
weights = [
|
||||
settings.fpa_2ship_weight,
|
||||
settings.fpa_3ship_weight,
|
||||
settings.fpa_4ship_weight,
|
||||
]
|
||||
return random.choices([2, 3, 4], weights, k=1)[0]
|
||||
|
||||
@@ -2,10 +2,10 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import MissionTarget
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from random import randint
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import ControlPoint
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanAirAssault(PackagePlanningTask[ControlPoint]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if self.target not in state.air_assault_targets:
|
||||
return False
|
||||
if self.capture_blocked(state):
|
||||
# Do not task if there are enemy battle_positions blocking the capture
|
||||
return False
|
||||
if not self.target_area_preconditions_met(state):
|
||||
# Do not task if air defense is present in the target area
|
||||
return False
|
||||
return super().preconditions_met(state)
|
||||
|
||||
def capture_blocked(self, state: TheaterState) -> bool:
|
||||
battle_positions = state.enemy_battle_positions[self.target]
|
||||
return len(battle_positions.blocking_capture) > 0
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.air_assault_targets.remove(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.AIR_ASSAULT, randint(2, 4))
|
||||
# TODO Validate this.. / is Heli escort possible?
|
||||
self.propose_flight(FlightType.TARCAP, 2)
|
||||
@@ -3,11 +3,11 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from random import randint
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.missionproposals import EscortType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater.theatergroundobject import NavalGroundObject
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -23,5 +23,6 @@ class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
|
||||
state.eliminate_ship(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.ANTISHIP, randint(2, 4))
|
||||
size = self.get_flight_size()
|
||||
self.propose_flight(FlightType.ANTISHIP, size)
|
||||
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)
|
||||
|
||||
@@ -3,10 +3,10 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from random import randint
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.transfers import CargoShip
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -22,5 +22,6 @@ class PlanAntiShipping(PackagePlanningTask[CargoShip]):
|
||||
state.enemy_shipping.remove(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.ANTISHIP, randint(2, 4))
|
||||
size = self.get_flight_size()
|
||||
self.propose_flight(FlightType.ANTISHIP, size)
|
||||
self.propose_common_escorts()
|
||||
|
||||
@@ -2,10 +2,10 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater.theatergroundobject import VehicleGroupGroundObject
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from random import randint
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import ControlPoint
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -22,7 +23,8 @@ class PlanBarcap(PackagePlanningTask[ControlPoint]):
|
||||
state.barcaps_needed[self.target] -= 1
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.BARCAP, randint(2, 4))
|
||||
size = self.get_flight_size()
|
||||
self.propose_flight(FlightType.BARCAP, size)
|
||||
|
||||
@property
|
||||
def purchase_multiplier(self) -> int:
|
||||
|
||||
@@ -3,10 +3,10 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from random import randint
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import FrontLine
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -30,5 +30,6 @@ class PlanCas(PackagePlanningTask[FrontLine]):
|
||||
state.vulnerable_front_lines.remove(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.CAS, randint(2, 4))
|
||||
size = self.get_flight_size()
|
||||
self.propose_flight(FlightType.CAS, size)
|
||||
self.propose_flight(FlightType.TARCAP, 2)
|
||||
|
||||
@@ -2,11 +2,10 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.transfers import Convoy
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -2,11 +2,11 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.missionproposals import EscortType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater.theatergroundobject import IadsGroundObject
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -42,6 +42,5 @@ class PlanDead(PackagePlanningTask[IadsGroundObject]):
|
||||
# needs a decent refactor to the escort planning to do so.
|
||||
if self.target.has_live_radar_sam:
|
||||
self.propose_flight(FlightType.SEAD, 2)
|
||||
else:
|
||||
self.propose_flight(FlightType.SEAD_ESCORT, 2, EscortType.Sead)
|
||||
self.propose_flight(FlightType.SEAD_ESCORT, 2, EscortType.Sead)
|
||||
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)
|
||||
|
||||
@@ -3,10 +3,10 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from random import randint
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import ControlPoint
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -24,7 +24,8 @@ class PlanOcaStrike(PackagePlanningTask[ControlPoint]):
|
||||
state.oca_targets.remove(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.OCA_RUNWAY, randint(2, 4))
|
||||
size = self.get_flight_size()
|
||||
self.propose_flight(FlightType.OCA_RUNWAY, size)
|
||||
if self.aircraft_cold_start:
|
||||
self.propose_flight(FlightType.OCA_AIRCRAFT, 2)
|
||||
self.propose_common_escorts()
|
||||
|
||||
@@ -2,10 +2,10 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import MissionTarget
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -45,7 +45,6 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
context: PersistentContext
|
||||
barcaps_needed: dict[ControlPoint, int]
|
||||
active_front_lines: list[FrontLine]
|
||||
air_assault_targets: list[ControlPoint]
|
||||
front_line_stances: dict[FrontLine, Optional[CombatStance]]
|
||||
vulnerable_front_lines: list[FrontLine]
|
||||
aewc_targets: list[MissionTarget]
|
||||
@@ -110,7 +109,6 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
context=self.context,
|
||||
barcaps_needed=dict(self.barcaps_needed),
|
||||
active_front_lines=list(self.active_front_lines),
|
||||
air_assault_targets=list(self.air_assault_targets),
|
||||
front_line_stances=dict(self.front_line_stances),
|
||||
vulnerable_front_lines=list(self.vulnerable_front_lines),
|
||||
aewc_targets=list(self.aewc_targets),
|
||||
@@ -161,7 +159,6 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
cp: barcap_rounds for cp in finder.vulnerable_control_points()
|
||||
},
|
||||
active_front_lines=list(finder.front_lines()),
|
||||
air_assault_targets=list(finder.air_assault_targets()),
|
||||
front_line_stances={f: None for f in finder.front_lines()},
|
||||
vulnerable_front_lines=list(finder.front_lines()),
|
||||
aewc_targets=[finder.farthest_friendly_control_point()],
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import inspect
|
||||
|
||||
import dcs
|
||||
|
||||
REQUIRED_BUILDINGS = [
|
||||
"ammo",
|
||||
"factory",
|
||||
"fob",
|
||||
"oil",
|
||||
]
|
||||
|
||||
IADS_BUILDINGS = [
|
||||
@@ -15,7 +17,6 @@ IADS_BUILDINGS = [
|
||||
|
||||
DEFAULT_AVAILABLE_BUILDINGS = [
|
||||
"fuel",
|
||||
"oil",
|
||||
"ware",
|
||||
"farp",
|
||||
"derrick",
|
||||
|
||||
@@ -2,7 +2,7 @@ from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
|
||||
from game.data.units import UnitClass
|
||||
from game.utils import Distance, feet, nautical_miles
|
||||
from game.utils import Distance, feet, nautical_miles, Speed, knots
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -77,6 +77,16 @@ class Doctrine:
|
||||
|
||||
ground_unit_procurement_ratios: GroundUnitProcurementRatios
|
||||
|
||||
rtb_speed: Speed
|
||||
|
||||
sead_escort_spacing: Distance
|
||||
|
||||
escort_spacing: Distance
|
||||
|
||||
sead_escort_engagement_range: Distance
|
||||
|
||||
escort_engagement_range: Distance
|
||||
|
||||
|
||||
MODERN_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
@@ -113,6 +123,11 @@ MODERN_DOCTRINE = Doctrine(
|
||||
UnitClass.RECON: 1,
|
||||
}
|
||||
),
|
||||
rtb_speed=knots(500),
|
||||
sead_escort_spacing=feet(1000),
|
||||
escort_spacing=feet(2000),
|
||||
sead_escort_engagement_range=nautical_miles(40),
|
||||
escort_engagement_range=nautical_miles(30),
|
||||
)
|
||||
|
||||
COLDWAR_DOCTRINE = Doctrine(
|
||||
@@ -150,6 +165,11 @@ COLDWAR_DOCTRINE = Doctrine(
|
||||
UnitClass.RECON: 1,
|
||||
}
|
||||
),
|
||||
rtb_speed=knots(450),
|
||||
sead_escort_spacing=feet(500),
|
||||
escort_spacing=feet(1000),
|
||||
sead_escort_engagement_range=nautical_miles(25),
|
||||
escort_engagement_range=nautical_miles(20),
|
||||
)
|
||||
|
||||
WWII_DOCTRINE = Doctrine(
|
||||
@@ -186,4 +206,9 @@ WWII_DOCTRINE = Doctrine(
|
||||
UnitClass.RECON: 1,
|
||||
}
|
||||
),
|
||||
rtb_speed=knots(300),
|
||||
sead_escort_spacing=feet(100),
|
||||
escort_spacing=feet(200),
|
||||
sead_escort_engagement_range=nautical_miles(10),
|
||||
escort_engagement_range=nautical_miles(5),
|
||||
)
|
||||
|
||||
@@ -19,6 +19,18 @@ PydcsWeapon = Any
|
||||
PydcsWeaponAssignment = tuple[int, PydcsWeapon]
|
||||
|
||||
|
||||
def weapons_migrator(name: str) -> str:
|
||||
migration_map = {
|
||||
"AGM-88C HARM - High Speed Anti-Radiation Missile": "AGM-88C HARM",
|
||||
"Kh-25MPU (Updated AS-12 Kegler) - 320kg, ARM, IN & Pas Rdr": "Kh-25MPU",
|
||||
"Kh-31P (AS-17 Krypton) - 600kg, ARM, IN & Pas Rdr": "Kh-31P",
|
||||
"Kh-58U (AS-11 Kilter) - 640kg, ARM, IN & Pas Rdr": "Kh-58U",
|
||||
}
|
||||
while name in migration_map:
|
||||
name = migration_map[name]
|
||||
return name
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Weapon:
|
||||
"""Wrapper for DCS weapons."""
|
||||
@@ -139,7 +151,7 @@ class WeaponGroup:
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# Update any existing models with new data on load.
|
||||
updated = WeaponGroup.named(state["name"])
|
||||
updated = WeaponGroup.named(weapons_migrator(state["name"]))
|
||||
state.update(updated.__dict__)
|
||||
self.__dict__.update(state)
|
||||
|
||||
@@ -161,16 +173,21 @@ class WeaponGroup:
|
||||
|
||||
@classmethod
|
||||
def _each_weapon_group(cls) -> Iterator[WeaponGroup]:
|
||||
names = []
|
||||
links = []
|
||||
for group_file_path in Path("resources/weapons").glob("**/*.yaml"):
|
||||
with group_file_path.open(encoding="utf8") as group_file:
|
||||
data = yaml.safe_load(group_file)
|
||||
name = data["name"]
|
||||
names.append(name)
|
||||
try:
|
||||
weapon_type = WeaponType(data["type"])
|
||||
except KeyError:
|
||||
weapon_type = WeaponType.UNKNOWN
|
||||
year = data.get("year")
|
||||
fallback_name = data.get("fallback")
|
||||
if fallback_name:
|
||||
links.append((name, fallback_name))
|
||||
group = WeaponGroup(name, weapon_type, year, fallback_name)
|
||||
for clsid in data["clsids"]:
|
||||
weapon = Weapon(clsid, group)
|
||||
@@ -178,6 +195,17 @@ class WeaponGroup:
|
||||
group.weapons.append(weapon)
|
||||
yield group
|
||||
|
||||
for name, fb in links:
|
||||
if fb not in names:
|
||||
sn = "Unknown"
|
||||
for n in names:
|
||||
if fb in n:
|
||||
sn = n
|
||||
break
|
||||
logging.error(
|
||||
f"Weapon '{name}' has invalid fallback '{fb}': suggested = {sn}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def register_clean_pylon(cls) -> None:
|
||||
group = WeaponGroup(
|
||||
|
||||
@@ -348,9 +348,11 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
|
||||
@classmethod
|
||||
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
|
||||
data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
|
||||
# Replace slashes with underscores because slashes are not allowed in filenames
|
||||
aircraft_id = aircraft.id.replace("/", "_")
|
||||
data_path = Path("resources/units/aircraft") / f"{aircraft_id}.yaml"
|
||||
if not data_path.exists():
|
||||
logging.warning(f"No data for {aircraft.id}; it will not be available")
|
||||
logging.warning(f"No data for {aircraft_id}; it will not be available")
|
||||
return
|
||||
|
||||
with data_path.open(encoding="utf-8") as data_file:
|
||||
@@ -371,7 +373,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
nautical_miles(50) if aircraft.helicopter else nautical_miles(150)
|
||||
)
|
||||
logging.warning(
|
||||
f"{aircraft.id} does not specify a max_range. Defaulting to "
|
||||
f"{aircraft_id} does not specify a max_range. Defaulting to "
|
||||
f"{mission_range.nautical_miles}NM"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ from typing import Optional, Type
|
||||
from dcs.helicopters import helicopter_map
|
||||
from dcs.planes import plane_map
|
||||
from dcs.ships import ship_map
|
||||
from dcs.statics import fortification_map, groundobject_map, warehouse_map, cargo_map
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.vehicles import vehicle_map
|
||||
from dcs.statics import fortification_map, groundobject_map, warehouse_map, cargo_map
|
||||
|
||||
|
||||
def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
|
||||
|
||||
@@ -338,9 +338,7 @@ class Debriefing:
|
||||
seen = set()
|
||||
captures = []
|
||||
for capture in reversed(self.state_data.base_capture_events):
|
||||
# The ID string in the JSON file will be an airport ID for airport captures
|
||||
# but will be a UUID for all other types, since DCS doesn't know the UUIDs
|
||||
# for the captured FOBs.
|
||||
# The ID string in the JSON file will be the UUID generated from liberation
|
||||
cp_id, new_owner_id_str, _name = capture.split("||")
|
||||
|
||||
# Only the most recent capture event matters.
|
||||
@@ -349,13 +347,8 @@ class Debriefing:
|
||||
seen.add(cp_id)
|
||||
|
||||
try:
|
||||
control_point = self.game.theater.find_control_point_by_airport_id(
|
||||
int(cp_id)
|
||||
)
|
||||
except ValueError:
|
||||
# The CP ID could not be converted to an int, so it's a UUID.
|
||||
control_point = self.game.theater.find_control_point_by_id(UUID(cp_id))
|
||||
except KeyError:
|
||||
except (KeyError, ValueError):
|
||||
# Captured base is not a part of the campaign. This happens when neutral
|
||||
# bases are near the conflict. Nothing to do.
|
||||
continue
|
||||
|
||||
@@ -11,6 +11,7 @@ from dcs.countries import country_dict
|
||||
from dcs.unittype import ShipType, StaticType
|
||||
from dcs.unittype import UnitType as DcsUnitType
|
||||
|
||||
from game.armedforces.forcegroup import ForceGroup
|
||||
from game.data.building_data import (
|
||||
WW2_ALLIES_BUILDINGS,
|
||||
DEFAULT_AVAILABLE_BUILDINGS,
|
||||
@@ -25,13 +26,13 @@ from game.data.doctrine import (
|
||||
COLDWAR_DOCTRINE,
|
||||
WWII_DOCTRINE,
|
||||
)
|
||||
from game.data.units import UnitClass
|
||||
from game.data.groups import GroupRole
|
||||
from game.data.units import UnitClass
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.shipunittype import ShipUnitType
|
||||
from game.armedforces.forcegroup import ForceGroup
|
||||
from game.dcs.unittype import UnitType
|
||||
from pydcs_extensions.f16i_idf.f16i_idf import inject_F16I
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater.start_generator import ModSettings
|
||||
@@ -59,7 +60,7 @@ class Faction:
|
||||
description: str = field(default="")
|
||||
|
||||
# Available aircraft
|
||||
aircrafts: List[AircraftType] = field(default_factory=list)
|
||||
aircraft: List[AircraftType] = field(default_factory=list)
|
||||
|
||||
# Available awacs aircraft
|
||||
awacs: List[AircraftType] = field(default_factory=list)
|
||||
@@ -115,6 +116,9 @@ class Faction:
|
||||
# List of default livery overrides
|
||||
liveries_overrides: Dict[AircraftType, List[str]] = field(default_factory=dict)
|
||||
|
||||
# List of default livery overrides for ground vehicles
|
||||
liveries_overrides_ground_forces: Dict[str, List[str]] = field(default_factory=dict)
|
||||
|
||||
#: Set to True if the faction should force the "Unrestricted satnav" option
|
||||
#: for the mission. This option enables GPS for capable aircraft regardless
|
||||
#: of the time period or operator. For example, the CJTF "countries" don't
|
||||
@@ -124,6 +128,10 @@ class Faction:
|
||||
#: both will use it.
|
||||
unrestricted_satnav: bool = False
|
||||
|
||||
# Store mod settings so mod properties can be injected again on game load,
|
||||
# in case mods like CJS F/A-18E/F/G or IDF F-16I are selected by the player
|
||||
mod_settings: Optional[ModSettings] = field(default=None)
|
||||
|
||||
def has_access_to_dcs_type(self, unit_type: Type[DcsUnitType]) -> bool:
|
||||
# Vehicle and Ship Units
|
||||
if any(unit_type == u.dcs_unit_type for u in self.accessible_units):
|
||||
@@ -170,6 +178,10 @@ class Faction:
|
||||
)
|
||||
return sorted(air_defenses)
|
||||
|
||||
@cached_property
|
||||
def aircrafts(self) -> list[UnitType[Any]]:
|
||||
return list(self.aircraft + self.awacs + self.tankers)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
faction = Faction(locales=json.get("locales"))
|
||||
@@ -198,14 +210,10 @@ class Faction:
|
||||
faction.authors = json.get("authors", "")
|
||||
faction.description = json.get("description", "")
|
||||
|
||||
faction.aircrafts = [AircraftType.named(n) for n in json.get("aircrafts", [])]
|
||||
faction.aircraft = [AircraftType.named(n) for n in json.get("aircrafts", [])]
|
||||
faction.awacs = [AircraftType.named(n) for n in json.get("awacs", [])]
|
||||
faction.tankers = [AircraftType.named(n) for n in json.get("tankers", [])]
|
||||
|
||||
faction.aircrafts = list(
|
||||
set(faction.aircrafts + faction.awacs + faction.tankers)
|
||||
)
|
||||
|
||||
faction.frontline_units = [
|
||||
GroundUnitType.named(n) for n in json.get("frontline_units", [])
|
||||
]
|
||||
@@ -279,6 +287,16 @@ class Faction:
|
||||
aircraft = AircraftType.named(name)
|
||||
faction.liveries_overrides[aircraft] = [s.lower() for s in livery]
|
||||
|
||||
# Load liveries override for ground forces
|
||||
faction.liveries_overrides_ground_forces = {}
|
||||
liveries_overrides_ground_forces = json.get(
|
||||
"liveries_overrides_ground_forces", {}
|
||||
)
|
||||
for vehicle_type, livery in liveries_overrides_ground_forces.items():
|
||||
faction.liveries_overrides_ground_forces[vehicle_type] = [
|
||||
s.lower() for s in livery
|
||||
]
|
||||
|
||||
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
|
||||
|
||||
return faction
|
||||
@@ -294,7 +312,21 @@ class Faction:
|
||||
if unit.unit_class is unit_class:
|
||||
yield unit
|
||||
|
||||
def apply_mod_settings(self, mod_settings: ModSettings) -> None:
|
||||
def apply_mod_settings(self, mod_settings: Optional[ModSettings] = None) -> None:
|
||||
if mod_settings is None:
|
||||
if self.mod_settings is None:
|
||||
# No mod settings were provided and none were saved for this faction
|
||||
# so stop here
|
||||
return
|
||||
elif self.mod_settings is not None:
|
||||
# Saved mod settings were found for this faction,
|
||||
# so load them for use
|
||||
mod_settings = self.mod_settings
|
||||
else:
|
||||
# Update the mod settings of this faction
|
||||
# so the settings can be applied again on load, if needed
|
||||
self.mod_settings = mod_settings
|
||||
|
||||
# aircraft
|
||||
if not mod_settings.a4_skyhawk:
|
||||
self.remove_aircraft("A-4E-C")
|
||||
@@ -303,16 +335,40 @@ class Faction:
|
||||
if not mod_settings.uh_60l:
|
||||
self.remove_aircraft("UH-60L")
|
||||
self.remove_aircraft("KC130J")
|
||||
if not mod_settings.f4bc_phantom:
|
||||
self.remove_aircraft("VSN_F4B")
|
||||
self.remove_aircraft("VSN_F4C")
|
||||
if not mod_settings.f15d_baz:
|
||||
self.remove_aircraft("F-15D")
|
||||
if not mod_settings.f_16_idf:
|
||||
self.remove_aircraft("F-16I")
|
||||
self.remove_aircraft("F_16D_52")
|
||||
self.remove_aircraft("F_16D_50")
|
||||
self.remove_aircraft("F_16D_50_NS")
|
||||
self.remove_aircraft("F_16D_52_NS")
|
||||
self.remove_aircraft("F-16D_Barak_30")
|
||||
self.remove_aircraft("F-16D_Barak_40")
|
||||
else:
|
||||
inject_F16I()
|
||||
if not mod_settings.f22_raptor:
|
||||
self.remove_aircraft("F-22A")
|
||||
if not mod_settings.f84g_thunderjet:
|
||||
self.remove_aircraft("VSN_F84G")
|
||||
if not mod_settings.f100_supersabre:
|
||||
self.remove_aircraft("VSN_F100")
|
||||
if not mod_settings.f104_starfighter:
|
||||
self.remove_aircraft("VSN_F104C")
|
||||
self.remove_aircraft("VSN_F104G")
|
||||
self.remove_aircraft("VSN_F104S")
|
||||
self.remove_aircraft("VSN_F104S_AG")
|
||||
if not mod_settings.f105_thunderchief:
|
||||
self.remove_aircraft("VSN_F105D")
|
||||
self.remove_aircraft("VSN_F105G")
|
||||
if not mod_settings.a6a_intruder:
|
||||
self.remove_aircraft("VSN_A6A")
|
||||
if not mod_settings.jas39_gripen:
|
||||
self.remove_aircraft("JAS39Gripen")
|
||||
self.remove_aircraft("JAS39Gripen_BVR")
|
||||
self.remove_aircraft("JAS39Gripen_AG")
|
||||
if not mod_settings.su30_flanker_h:
|
||||
self.remove_aircraft("Su-30MKA")
|
||||
@@ -321,6 +377,8 @@ class Faction:
|
||||
self.remove_aircraft("Su-30SM")
|
||||
if not mod_settings.su57_felon:
|
||||
self.remove_aircraft("Su-57")
|
||||
if not mod_settings.ov10a_bronco:
|
||||
self.remove_aircraft("Bronco-OV-10A")
|
||||
# frenchpack
|
||||
if not mod_settings.frenchpack:
|
||||
self.remove_vehicle("AMX10RCR")
|
||||
@@ -367,19 +425,67 @@ class Faction:
|
||||
self.remove_preset("SA-20B/S-300PMU-2")
|
||||
self.remove_preset("SA-23/S-300VM")
|
||||
self.remove_preset("SA-17")
|
||||
self.remove_preset("KS-19")
|
||||
self.remove_preset("KS-19_HDS")
|
||||
self.remove_preset("HQ-2")
|
||||
self.remove_preset("SA-2/S-75 V-759/5V23")
|
||||
self.remove_preset("SA-3/S-125 V-601P/5V27")
|
||||
self.remove_vehicle("SAM SA-14 Strela-3 manpad")
|
||||
self.remove_vehicle("SAM SA-24 Igla-S manpad")
|
||||
self.remove_vehicle("Polyana-D4M1 C2 node")
|
||||
if not mod_settings.fa_18efg:
|
||||
self.remove_aircraft("FA-18E")
|
||||
self.remove_aircraft("FA-18F")
|
||||
self.remove_aircraft("EA-18G")
|
||||
# swedish military assets pack
|
||||
if not mod_settings.swedishmilitaryassetspack:
|
||||
self.remove_vehicle("BV410_RBS70")
|
||||
self.remove_vehicle("BV410_RBS90")
|
||||
self.remove_vehicle("LvS_103_Lavett103_Rb103A")
|
||||
self.remove_vehicle("LvS_103_Lavett103_Rb103B")
|
||||
self.remove_vehicle("LvS_103_Lavett103_HX_Rb103A")
|
||||
self.remove_vehicle("LvS_103_Lavett103_HX_Rb103B")
|
||||
self.remove_vehicle("LvS_103_StriE103")
|
||||
self.remove_vehicle("LvS_103_PM103")
|
||||
self.remove_vehicle("LvS_103_PM103_HX")
|
||||
self.remove_vehicle("LvS_103_Elverk103")
|
||||
self.remove_vehicle("LvKv9040")
|
||||
self.remove_vehicle("RBS_70")
|
||||
self.remove_vehicle("RBS_90")
|
||||
self.remove_vehicle("RBS_98")
|
||||
self.remove_vehicle("UndE23")
|
||||
self.remove_vehicle("BV410")
|
||||
self.remove_vehicle("CV9040")
|
||||
self.remove_vehicle("Strv103")
|
||||
self.remove_vehicle("Strv121")
|
||||
self.remove_vehicle("Strv122")
|
||||
self.remove_vehicle("Strv2000")
|
||||
self.remove_vehicle("Volvo740")
|
||||
self.remove_vehicle("RBS_15KA")
|
||||
self.remove_vehicle("AG_90")
|
||||
self.remove_vehicle("SwedishinfantryAK4")
|
||||
self.remove_vehicle("SwedishinfantryAK5")
|
||||
self.remove_vehicle("SwedishinfantryAK5GT")
|
||||
self.remove_vehicle("SwedishinfantryKSP90")
|
||||
self.remove_vehicle("SwedishinfantryKSP58")
|
||||
self.remove_vehicle("SwedishinfantryPskott86")
|
||||
self.remove_vehicle("RBS_57")
|
||||
self.remove_vehicle("RBS_58")
|
||||
self.remove_vehicle("Artillerisystem08")
|
||||
self.remove_vehicle("Grkpbv90")
|
||||
self.remove_ship("HSwMS_Visby")
|
||||
self.remove_ship("Strb90")
|
||||
self.remove_aircraft("HKP15B")
|
||||
|
||||
def remove_aircraft(self, name: str) -> None:
|
||||
for i in self.aircrafts:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.aircrafts.remove(i)
|
||||
|
||||
def remove_aircraft_by_name(self, name: str) -> None:
|
||||
for i in self.aircrafts:
|
||||
if i.name == name:
|
||||
self.aircrafts.remove(i)
|
||||
|
||||
def remove_preset(self, name: str) -> None:
|
||||
for pg in self.preset_groups:
|
||||
if pg.name == name:
|
||||
@@ -390,6 +496,11 @@ class Faction:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.frontline_units.remove(i)
|
||||
|
||||
def remove_ship(self, name: str) -> None:
|
||||
for i in self.naval_units:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.naval_units.remove(i)
|
||||
|
||||
|
||||
def load_ship(name: str) -> Optional[Type[ShipType]]:
|
||||
if (ship := getattr(dcs.ships, name, None)) is not None:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -30,7 +31,7 @@ class FactionLoader:
|
||||
|
||||
@classmethod
|
||||
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
|
||||
user_faction_path = Path(persistency.base_path()) / "Liberation/Factions"
|
||||
user_faction_path = Path(persistency.base_path()) / "Retribution/Factions"
|
||||
files = cls.find_faction_files_in(
|
||||
FACTION_DIRECTORY
|
||||
) + cls.find_faction_files_in(user_faction_path)
|
||||
|
||||
@@ -21,7 +21,8 @@ class RefuelZoneGeometry:
|
||||
|
||||
def find_best_refuel_point(self) -> Point:
|
||||
# Do simple at first.
|
||||
# TODO: Consider threats.
|
||||
# TODO: Avoid threatened areas, minimize backtracking.
|
||||
# https://github.com/dcs-liberation/dcs_liberation/issues/2542
|
||||
distance = 0.75 * self.package_home.distance_to_point(self.join)
|
||||
heading = self.package_home.heading_between_point(self.join)
|
||||
return self.package_home.point_from_heading(heading, distance)
|
||||
|
||||
17
game/game.py
17
game/game.py
@@ -232,6 +232,10 @@ class Game:
|
||||
LuaPluginManager.load_settings(self.settings)
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
self.compute_unculled_zones(GameUpdateEvents())
|
||||
# Apply mod settings again so mod properties get injected again,
|
||||
# in case mods like CJS F/A-18E/F/G or IDF F-16I are selected by the player
|
||||
self.blue.faction.apply_mod_settings()
|
||||
self.red.faction.apply_mod_settings()
|
||||
if not game_still_initializing:
|
||||
# We don't need to push events that happen during load. The UI will fully
|
||||
# reset when we're done.
|
||||
@@ -341,7 +345,6 @@ class Game:
|
||||
with logged_duration("Turn initialization"):
|
||||
self.initialize_turn(events)
|
||||
|
||||
events.begin_new_turn()
|
||||
EventStream.put_nowait(events)
|
||||
|
||||
# Autosave progress
|
||||
@@ -437,6 +440,8 @@ class Game:
|
||||
with logged_duration("Computing culling positions"):
|
||||
self.compute_unculled_zones(events)
|
||||
|
||||
events.begin_new_turn()
|
||||
|
||||
def message(self, title: str, text: str = "") -> None:
|
||||
self.informations.append(Information(title, text, turn=self.turn))
|
||||
|
||||
@@ -523,12 +528,20 @@ class Game:
|
||||
|
||||
packages = itertools.chain(self.blue.ato.packages, self.red.ato.packages)
|
||||
for package in packages:
|
||||
if package.primary_task is FlightType.BARCAP:
|
||||
if package.primary_task in [
|
||||
FlightType.BARCAP,
|
||||
FlightType.TRANSPORT,
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING,
|
||||
]:
|
||||
# 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.
|
||||
|
||||
# Don't create culling exclusion zones around FlightType.TRANSPORT,
|
||||
# FlightType.AEWC & FlightType.REFUELING mission targets.
|
||||
continue
|
||||
zones.append(package.target.position)
|
||||
|
||||
|
||||
@@ -89,7 +89,6 @@ class GroundPlanner:
|
||||
self.reserve: List[CombatGroup] = []
|
||||
|
||||
def plan_groundwar(self) -> None:
|
||||
|
||||
ground_unit_limit = self.cp.frontline_unit_count_limit
|
||||
|
||||
remaining_available_frontline_units = ground_unit_limit
|
||||
@@ -139,7 +138,6 @@ class GroundPlanner:
|
||||
remaining_available_frontline_units -= available
|
||||
|
||||
while available > 0:
|
||||
|
||||
if role == CombatGroupRole.SHORAD:
|
||||
count = 1
|
||||
else:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Iterator, Type, Optional
|
||||
@@ -28,7 +26,6 @@ from game.theater.theatergroundobject import (
|
||||
IadsGroundObject,
|
||||
)
|
||||
from game.theater.theatergroup import TheaterUnit
|
||||
from game.utils import Heading
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.factions.faction import Faction
|
||||
@@ -140,11 +137,14 @@ class TgoLayoutUnitGroup:
|
||||
|
||||
@property
|
||||
def group_size(self) -> int:
|
||||
"""The amount of units to be generated. If unit_count is defined in the layout this will be randomized accordingly. Otherwise this will be the maximum size."""
|
||||
"""
|
||||
The amount of units to be generated. If unit_count is defined in the layout this will be
|
||||
randomized accordingly. Otherwise this will be the maximum size.
|
||||
"""
|
||||
if self.unit_count:
|
||||
if len(self.unit_count) == 1:
|
||||
return self.unit_count[0]
|
||||
return random.choice(range(min(self.unit_count), max(self.unit_count)))
|
||||
return random.choice(range(min(self.unit_count), max(self.unit_count) + 1))
|
||||
return self.max_size
|
||||
|
||||
@property
|
||||
@@ -241,7 +241,6 @@ class AntiAirLayout(TgoLayout):
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
) -> IadsGroundObject:
|
||||
|
||||
if GroupTask.EARLY_WARNING_RADAR in self.tasks:
|
||||
return EwrGroundObject(name, location, control_point)
|
||||
elif any(tasking in self.tasks for tasking in GroupRole.AIR_DEFENSE.tasks):
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import pickle
|
||||
from collections import defaultdict
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from typing import Iterator
|
||||
@@ -31,7 +31,7 @@ from game.profiling import logged_duration
|
||||
from game.version import VERSION
|
||||
|
||||
LAYOUT_DIR = "resources/layouts/"
|
||||
LAYOUT_DUMP = "Liberation/layouts.p"
|
||||
LAYOUT_DUMP = "Retribution/layouts.p"
|
||||
|
||||
LAYOUT_TYPES = {
|
||||
GroupRole.AIR_DEFENSE: AntiAirLayout,
|
||||
@@ -132,7 +132,6 @@ class LayoutLoader:
|
||||
temp_mis.country(country.name).ship_group,
|
||||
temp_mis.country(country.name).static_group,
|
||||
):
|
||||
|
||||
try:
|
||||
g_id, u_id, group_name, group_mapping = mapping.group_for_name(
|
||||
dcs_group.name
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional, Type
|
||||
|
||||
|
||||
@@ -19,4 +19,4 @@ def init_logging(version: str) -> None:
|
||||
with log_config.open() as log_file:
|
||||
logging.config.dictConfig(yaml.safe_load(log_file))
|
||||
|
||||
logging.info(f"DCS Liberation {version}")
|
||||
logging.info(f"DCS Retribution {version}")
|
||||
|
||||
78
game/migrator.py
Normal file
78
game/migrator.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from game.ato.packagewaypoints import PackageWaypoints
|
||||
from game.data.doctrine import MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
def try_set_attr(obj: Any, attr_name: str, val: Any = None) -> None:
|
||||
if not hasattr(obj, attr_name):
|
||||
setattr(obj, attr_name, val)
|
||||
|
||||
|
||||
class Migrator:
|
||||
def __init__(self, game: Game):
|
||||
self.game = game
|
||||
self._migrate_game()
|
||||
|
||||
def _migrate_game(self) -> None:
|
||||
self._update_doctrine()
|
||||
self._update_packagewaypoints()
|
||||
self._update_package_attributes()
|
||||
self._update_control_points()
|
||||
self._update_flights()
|
||||
|
||||
def _update_doctrine(self) -> None:
|
||||
doctrines = [
|
||||
MODERN_DOCTRINE,
|
||||
COLDWAR_DOCTRINE,
|
||||
WWII_DOCTRINE,
|
||||
]
|
||||
for c in self.game.coalitions:
|
||||
if c.faction.doctrine.__dict__ in [d.__dict__ for d in doctrines]:
|
||||
continue
|
||||
found = False
|
||||
for d in doctrines:
|
||||
if c.faction.doctrine.rendezvous_altitude == d.rendezvous_altitude:
|
||||
c.faction.doctrine = d
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
c.faction.doctrine = MODERN_DOCTRINE
|
||||
|
||||
def _update_packagewaypoints(self) -> None:
|
||||
for c in self.game.coalitions:
|
||||
for p in c.ato.packages:
|
||||
if p.flights:
|
||||
try_set_attr(p.waypoints, "initial", PackageWaypoints.create(p, c))
|
||||
|
||||
def _update_package_attributes(self) -> None:
|
||||
for c in self.game.coalitions:
|
||||
for p in c.ato.packages:
|
||||
try_set_attr(p, "custom_name")
|
||||
try_set_attr(p, "frequency")
|
||||
|
||||
def _update_control_points(self) -> None:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
is_carrier = cp.is_carrier
|
||||
is_lha = cp.is_lha
|
||||
is_fob = cp.category == "fob"
|
||||
radio_configurable = is_carrier or is_lha or is_fob
|
||||
if radio_configurable:
|
||||
try_set_attr(cp, "frequency")
|
||||
if is_carrier or is_lha:
|
||||
try_set_attr(cp, "tacan")
|
||||
try_set_attr(cp, "tcn_name")
|
||||
try_set_attr(cp, "icls_channel")
|
||||
try_set_attr(cp, "icls_name")
|
||||
try_set_attr(cp, "link4")
|
||||
|
||||
def _update_flights(self) -> None:
|
||||
for f in self.game.db.flights.objects.values():
|
||||
try_set_attr(f, "frequency")
|
||||
try_set_attr(f, "tacan")
|
||||
try_set_attr(f, "tcn_name")
|
||||
@@ -8,6 +8,7 @@ from dcs.task import (
|
||||
CAP,
|
||||
CAS,
|
||||
EPLRS,
|
||||
Escort,
|
||||
FighterSweep,
|
||||
GroundAttack,
|
||||
Nothing,
|
||||
@@ -19,12 +20,16 @@ from dcs.task import (
|
||||
Refueling,
|
||||
RunwayAttack,
|
||||
Transport,
|
||||
SEAD,
|
||||
SwitchWaypoint,
|
||||
OptJettisonEmptyTanks,
|
||||
)
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
|
||||
from game.ato import Flight, FlightType
|
||||
from game.ato.flightplans.aewc import AewcFlightPlan
|
||||
from game.ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
|
||||
|
||||
class AircraftBehavior:
|
||||
@@ -83,6 +88,7 @@ class AircraftBehavior:
|
||||
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
|
||||
restrict_jettison: Optional[bool] = None,
|
||||
mission_uses_gun: bool = True,
|
||||
rtb_on_bingo: bool = True,
|
||||
) -> None:
|
||||
group.points[0].tasks.clear()
|
||||
group.points[0].tasks.append(OptReactOnThreat(react_on_threat))
|
||||
@@ -104,7 +110,8 @@ class AircraftBehavior:
|
||||
for unit in group.units:
|
||||
unit.gun = 0
|
||||
|
||||
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
|
||||
group.points[0].tasks.append(OptRTBOnBingoFuel(rtb_on_bingo))
|
||||
group.points[0].tasks.append(OptJettisonEmptyTanks())
|
||||
# Do not restrict afterburner.
|
||||
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/7121294-ai-stuck-at-high-aoa-after-making-sharp-turn-if-afterburner-is-restricted
|
||||
|
||||
@@ -172,9 +179,9 @@ class AircraftBehavior:
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
# ASM includes ARMs and TALDs (among other things, but those are the useful
|
||||
# Guided includes ARMs and TALDs (among other things, but those are the useful
|
||||
# weapons for SEAD).
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.Guided,
|
||||
restrict_jettison=True,
|
||||
mission_uses_gun=False,
|
||||
)
|
||||
@@ -265,7 +272,15 @@ class AircraftBehavior:
|
||||
# Escort groups are actually given the CAP task so they can perform the
|
||||
# Search Then Engage task, which we have to use instead of the Escort
|
||||
# task for the reasons explained in JoinPointBuilder.
|
||||
group.task = CAP.name
|
||||
group.task = Escort.name
|
||||
if flight.flight_plan.is_formation(flight.flight_plan):
|
||||
index = flight.flight_plan.get_index_of_wpt_by_type(
|
||||
FlightWaypointType.SPLIT
|
||||
)
|
||||
if index > 0:
|
||||
group.add_trigger_action(SwitchWaypoint(None, index))
|
||||
else:
|
||||
logging.warning(f"Couldn't determine SPLIT for {group.name}")
|
||||
self.configure_behavior(
|
||||
flight, group, roe=OptROE.Values.OpenFire, restrict_jettison=True
|
||||
)
|
||||
@@ -274,25 +289,34 @@ class AircraftBehavior:
|
||||
# CAS is able to perform all the same tasks as SEAD using a superset of the
|
||||
# available aircraft, and F-14s are not able to be SEAD despite having TALDs.
|
||||
# https://forums.eagle.ru/topic/272112-cannot-assign-f-14-to-sead/
|
||||
group.task = CAS.name
|
||||
group.task = SEAD.name
|
||||
index = flight.flight_plan.get_index_of_wpt_by_type(FlightWaypointType.SPLIT)
|
||||
if index > 0 and flight.flight_plan.is_formation(flight.flight_plan):
|
||||
group.add_trigger_action(SwitchWaypoint(None, index))
|
||||
if index < 1:
|
||||
logging.warning(f"Couldn't determine SPLIT for {group.name}")
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
# ASM includes ARMs and TALDs (among other things, but those are the useful
|
||||
# Guided includes ARMs and TALDs (among other things, but those are the useful
|
||||
# weapons for SEAD).
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.Guided,
|
||||
restrict_jettison=True,
|
||||
mission_uses_gun=False,
|
||||
)
|
||||
|
||||
def configure_transport(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||
group.task = Transport.name
|
||||
roe = OptROE.Values.WeaponHold
|
||||
if flight.is_hercules:
|
||||
group.task = GroundAttack.name
|
||||
roe = OptROE.Values.OpenFire
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.WeaponHold,
|
||||
roe=roe,
|
||||
restrict_jettison=True,
|
||||
)
|
||||
|
||||
@@ -304,6 +328,7 @@ class AircraftBehavior:
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.WeaponHold,
|
||||
restrict_jettison=True,
|
||||
rtb_on_bingo=False,
|
||||
)
|
||||
|
||||
def configure_unknown_task(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||
|
||||
@@ -5,9 +5,12 @@ from datetime import datetime
|
||||
from functools import cached_property
|
||||
from typing import Any, Dict, List, TYPE_CHECKING
|
||||
|
||||
from dcs.action import AITaskPush
|
||||
from dcs.condition import FlagIsTrue, GroupDead, Or, FlagIsFalse
|
||||
from dcs.country import Country
|
||||
from dcs.mission import Mission
|
||||
from dcs.terrain.terrain import NoParkingSlotError
|
||||
from dcs.triggers import TriggerOnce, Event
|
||||
from dcs.unitgroup import FlyingGroup, StaticGroup
|
||||
|
||||
from game.ato.airtaaskingorder import AirTaskingOrder
|
||||
@@ -17,8 +20,8 @@ from game.ato.flighttype import FlightType
|
||||
from game.ato.package import Package
|
||||
from game.ato.starttype import StartType
|
||||
from game.factions.faction import Faction
|
||||
from game.missiongenerator.missiondata import MissionData
|
||||
from game.missiongenerator.lasercoderegistry import LaserCodeRegistry
|
||||
from game.missiongenerator.missiondata import MissionData
|
||||
from game.radio.radios import RadioRegistry
|
||||
from game.radio.tacan import TacanRegistry
|
||||
from game.runways import RunwayData
|
||||
@@ -26,6 +29,7 @@ from game.settings import Settings
|
||||
from game.theater.controlpoint import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
Fob,
|
||||
)
|
||||
from game.unitmap import UnitMap
|
||||
from .aircraftpainter import AircraftPainter
|
||||
@@ -101,7 +105,9 @@ class AircraftGenerator:
|
||||
ato: The ATO to spawn aircraft for.
|
||||
dynamic_runways: Runway data for carriers and FARPs.
|
||||
"""
|
||||
for package in ato.packages:
|
||||
self._reserve_frequencies_and_tacan(ato)
|
||||
|
||||
for package in reversed(sorted(ato.packages, key=lambda x: x.time_over_target)):
|
||||
if not package.flights:
|
||||
continue
|
||||
for flight in package.flights:
|
||||
@@ -111,6 +117,25 @@ class AircraftGenerator:
|
||||
flight, country, dynamic_runways
|
||||
)
|
||||
self.unit_map.add_aircraft(group, flight)
|
||||
if (
|
||||
package.primary_flight is not None
|
||||
and package.primary_flight.flight_plan.is_formation(
|
||||
package.primary_flight.flight_plan
|
||||
)
|
||||
):
|
||||
splittrigger = TriggerOnce(Event.NoEvent, f"Split-{id(package)}")
|
||||
splittrigger.add_condition(FlagIsTrue(flag=f"split-{id(package)}"))
|
||||
splittrigger.add_condition(Or())
|
||||
splittrigger.add_condition(FlagIsFalse(flag=f"split-{id(package)}"))
|
||||
splittrigger.add_condition(GroupDead(package.primary_flight.group_id))
|
||||
for flight in package.flights:
|
||||
if flight.flight_type in [
|
||||
FlightType.ESCORT,
|
||||
FlightType.SEAD_ESCORT,
|
||||
]:
|
||||
splittrigger.add_action(AITaskPush(flight.group_id, 1))
|
||||
if len(splittrigger.actions) > 0:
|
||||
self.mission.triggerrules.triggers.append(splittrigger)
|
||||
|
||||
def spawn_unused_aircraft(
|
||||
self, player_country: Country, enemy_country: Country
|
||||
@@ -152,7 +177,7 @@ class AircraftGenerator:
|
||||
flight.state = Completed(flight, self.game.settings)
|
||||
|
||||
group = FlightGroupSpawner(
|
||||
flight, country, self.mission, self.helipads
|
||||
flight, country, self.mission, self.helipads, self.mission_data
|
||||
).create_idle_aircraft()
|
||||
AircraftPainter(flight, group).apply_livery()
|
||||
self.unit_map.add_aircraft(group, flight)
|
||||
@@ -162,7 +187,7 @@ class AircraftGenerator:
|
||||
) -> FlyingGroup[Any]:
|
||||
"""Creates and configures the flight group in the mission."""
|
||||
group = FlightGroupSpawner(
|
||||
flight, country, self.mission, self.helipads
|
||||
flight, country, self.mission, self.helipads, self.mission_data
|
||||
).create_flight_group()
|
||||
self.flights.append(
|
||||
FlightGroupConfigurator(
|
||||
@@ -179,4 +204,27 @@ class AircraftGenerator:
|
||||
self.use_client,
|
||||
).configure()
|
||||
)
|
||||
|
||||
wpt = group.waypoint("LANDING")
|
||||
if flight.is_helo and isinstance(flight.arrival, Fob) and wpt:
|
||||
hpad = self.helipads[flight.arrival].units.pop(0)
|
||||
wpt.helipad_id = hpad.id
|
||||
wpt.link_unit = hpad.id
|
||||
self.helipads[flight.arrival].units.append(hpad)
|
||||
|
||||
return group
|
||||
|
||||
def _reserve_frequencies_and_tacan(self, ato: AirTaskingOrder) -> None:
|
||||
for package in ato.packages:
|
||||
if package.frequency is None:
|
||||
continue
|
||||
if package.frequency not in self.radio_registry.allocated_channels:
|
||||
self.radio_registry.reserve(package.frequency)
|
||||
for f in package.flights:
|
||||
if (
|
||||
f.frequency
|
||||
and f.frequency not in self.radio_registry.allocated_channels
|
||||
):
|
||||
self.radio_registry.reserve(f.frequency)
|
||||
if f.tacan and f.tacan not in self.tacan_registy.allocated_channels:
|
||||
self.tacan_registy.mark_unavailable(f.tacan)
|
||||
|
||||
@@ -12,9 +12,9 @@ from dcs.unitgroup import FlyingGroup
|
||||
from game.ato import Flight, FlightType
|
||||
from game.callsigns import callsign_for_support_unit
|
||||
from game.data.weapons import Pylon, WeaponType as WeaponTypeEnum
|
||||
from game.missiongenerator.missiondata import MissionData, AwacsInfo, TankerInfo
|
||||
from game.missiongenerator.lasercoderegistry import LaserCodeRegistry
|
||||
from game.missiongenerator.logisticsgenerator import LogisticsGenerator
|
||||
from game.missiongenerator.missiondata import MissionData, AwacsInfo, TankerInfo
|
||||
from game.radio.radios import RadioFrequency, RadioRegistry
|
||||
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
|
||||
from game.runways import RunwayData
|
||||
@@ -133,14 +133,20 @@ class FlightGroupConfigurator:
|
||||
laser_codes.append(None)
|
||||
|
||||
def setup_radios(self) -> RadioFrequency:
|
||||
if self.flight.flight_type in {FlightType.AEWC, FlightType.REFUELING}:
|
||||
channel = self.radio_registry.alloc_uhf()
|
||||
self.register_air_support(channel)
|
||||
else:
|
||||
channel = self.flight.unit_type.alloc_flight_radio(self.radio_registry)
|
||||
freq = self.flight.frequency
|
||||
if freq is None and (freq := self.flight.package.frequency) is None:
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
self.flight.package.frequency = freq
|
||||
if freq not in self.radio_registry.allocated_channels:
|
||||
self.radio_registry.reserve(freq)
|
||||
|
||||
self.group.set_frequency(channel.mhz)
|
||||
return channel
|
||||
if self.flight.flight_type in {FlightType.AEWC, FlightType.REFUELING}:
|
||||
self.register_air_support(freq)
|
||||
elif self.flight.frequency is None and self.flight.client_count:
|
||||
freq = self.flight.unit_type.alloc_flight_radio(self.radio_registry)
|
||||
|
||||
self.group.set_frequency(freq.mhz)
|
||||
return freq
|
||||
|
||||
def register_air_support(self, channel: RadioFrequency) -> None:
|
||||
callsign = callsign_for_support_unit(self.group)
|
||||
@@ -151,13 +157,18 @@ class FlightGroupConfigurator:
|
||||
callsign=callsign,
|
||||
freq=channel,
|
||||
depature_location=self.flight.departure.name,
|
||||
end_time=self.flight.flight_plan.mission_departure_time,
|
||||
start_time=self.flight.flight_plan.takeoff_time(),
|
||||
start_time=self.flight.flight_plan.patrol_start_time,
|
||||
end_time=self.flight.flight_plan.patrol_end_time,
|
||||
blue=self.flight.departure.captured,
|
||||
)
|
||||
)
|
||||
elif isinstance(self.flight.flight_plan, TheaterRefuelingFlightPlan):
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir)
|
||||
if self.flight.tacan is None:
|
||||
tacan = self.tacan_registry.alloc_for_band(
|
||||
TacanBand.Y, TacanUsage.AirToAir
|
||||
)
|
||||
else:
|
||||
tacan = self.flight.tacan
|
||||
self.mission_data.tankers.append(
|
||||
TankerInfo(
|
||||
group_name=str(self.group.name),
|
||||
|
||||
@@ -6,7 +6,7 @@ from dcs import Mission
|
||||
from dcs.country import Country
|
||||
from dcs.mapping import Vector2
|
||||
from dcs.mission import StartType as DcsStartType
|
||||
from dcs.planes import Su_33
|
||||
from dcs.planes import F_14A, Su_33
|
||||
from dcs.ships import KUZNECOW
|
||||
from dcs.terrain import Airport, NoParkingSlotError
|
||||
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
|
||||
@@ -15,6 +15,7 @@ from game.ato import Flight
|
||||
from game.ato.flightstate import InFlight
|
||||
from game.ato.starttype import StartType
|
||||
from game.ato.traveltime import GroundSpeed
|
||||
from game.missiongenerator.missiondata import MissionData
|
||||
from game.naming import namegen
|
||||
from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint, OffMapSpawn
|
||||
from game.utils import feet, meters
|
||||
@@ -32,6 +33,8 @@ WARM_START_ALTITUDE = meters(3000)
|
||||
MINIMUM_MID_MISSION_SPAWN_ALTITUDE_MSL = feet(6000)
|
||||
MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL = feet(500)
|
||||
|
||||
STACK_SEPARATION = feet(200)
|
||||
|
||||
RTB_ALTITUDE = meters(800)
|
||||
RTB_DISTANCE = 5000
|
||||
HELI_ALT = 500
|
||||
@@ -44,11 +47,13 @@ class FlightGroupSpawner:
|
||||
country: Country,
|
||||
mission: Mission,
|
||||
helipads: dict[ControlPoint, StaticGroup],
|
||||
mission_data: MissionData,
|
||||
) -> None:
|
||||
self.flight = flight
|
||||
self.country = country
|
||||
self.mission = mission
|
||||
self.helipads = helipads
|
||||
self.mission_data = mission_data
|
||||
|
||||
def create_flight_group(self) -> FlyingGroup[Any]:
|
||||
"""Creates the group for the flight and adds it to the mission.
|
||||
@@ -74,8 +79,12 @@ class FlightGroupSpawner:
|
||||
self.flight.state.is_waiting_for_start
|
||||
or self.flight.state.spawn_type is not StartType.IN_FLIGHT
|
||||
):
|
||||
return self.generate_flight_at_departure()
|
||||
return self.generate_mid_mission()
|
||||
grp = self.generate_flight_at_departure()
|
||||
self.flight.group_id = grp.id
|
||||
return grp
|
||||
grp = self.generate_mid_mission()
|
||||
self.flight.group_id = grp.id
|
||||
return grp
|
||||
|
||||
def create_idle_aircraft(self) -> FlyingGroup[Any]:
|
||||
airport = self.flight.squadron.location.dcs_airport
|
||||
@@ -126,7 +135,6 @@ class FlightGroupSpawner:
|
||||
"No room on runway or parking slots. Starting from the air."
|
||||
)
|
||||
group = self._generate_over_departure(name, cp)
|
||||
group.points[0].alt = 1500
|
||||
return group
|
||||
|
||||
def generate_mid_mission(self) -> FlyingGroup[Any]:
|
||||
@@ -136,6 +144,10 @@ class FlightGroupSpawner:
|
||||
pos = self.flight.state.estimate_position()
|
||||
pos += Vector2(random.randint(100, 1000), random.randint(100, 1000))
|
||||
alt, alt_type = self.flight.state.estimate_altitude()
|
||||
cp = self.flight.squadron.location.id
|
||||
|
||||
if cp not in self.mission_data.cp_stack:
|
||||
self.mission_data.cp_stack[cp] = MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL
|
||||
|
||||
# We don't know where the ground is, so just make sure that any aircraft
|
||||
# spawning at an MSL altitude is spawned at some minimum altitude.
|
||||
@@ -145,8 +157,9 @@ class FlightGroupSpawner:
|
||||
|
||||
# Set a minimum AGL value for 'alt' if needed,
|
||||
# otherwise planes might crash in trees and stuff.
|
||||
if alt_type == "RADIO" and alt < MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL:
|
||||
alt = MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL
|
||||
if alt_type == "RADIO" and alt < self.mission_data.cp_stack[cp]:
|
||||
alt = self.mission_data.cp_stack[cp]
|
||||
self.mission_data.cp_stack[cp] += STACK_SEPARATION
|
||||
|
||||
group = self.mission.flight_group(
|
||||
country=self.country,
|
||||
@@ -194,7 +207,11 @@ class FlightGroupSpawner:
|
||||
elif self.flight.unit_type.helicopter:
|
||||
alt = WARM_START_HELI_ALT
|
||||
else:
|
||||
alt = WARM_START_ALTITUDE
|
||||
if origin.id not in self.mission_data.cp_stack:
|
||||
min_alt = MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL
|
||||
self.mission_data.cp_stack[origin.id] = min_alt
|
||||
alt = self.mission_data.cp_stack[origin.id]
|
||||
self.mission_data.cp_stack[origin.id] += STACK_SEPARATION
|
||||
|
||||
speed = GroundSpeed.for_flight(self.flight, alt)
|
||||
pos = at + Vector2(random.randint(100, 1000), random.randint(100, 1000))
|
||||
@@ -230,16 +247,18 @@ class FlightGroupSpawner:
|
||||
def _generate_at_cp_helipad(self, name: str, cp: ControlPoint) -> FlyingGroup[Any]:
|
||||
try:
|
||||
helipad = self.helipads[cp]
|
||||
except IndexError as ex:
|
||||
except IndexError:
|
||||
raise NoParkingSlotError()
|
||||
|
||||
group = self._generate_at_group(name, helipad)
|
||||
|
||||
if self.start_type is not StartType.COLD:
|
||||
if self.start_type is StartType.WARM:
|
||||
group.points[0].type = "TakeOffParkingHot"
|
||||
hpad = helipad.units[0]
|
||||
for i in range(self.flight.count):
|
||||
group.units[i].position = helipad.units[i].position
|
||||
group.units[i].heading = helipad.units[i].heading
|
||||
group.units[i].position = hpad.position
|
||||
group.units[i].heading = hpad.heading
|
||||
group.units[i].parking_id = str(i + 1)
|
||||
return group
|
||||
|
||||
def dcs_start_type(self) -> DcsStartType:
|
||||
@@ -259,7 +278,9 @@ class FlightGroupSpawner:
|
||||
# Setting Su-33s starting from the non-supercarrier Kuznetsov to take off from
|
||||
# runway to work around a DCS AI issue preventing Su-33s from taking off when
|
||||
# set to "Takeoff from ramp" (#1352)
|
||||
if (
|
||||
# Also setting the F-14A AI variant to start from cats since they are reported
|
||||
# to have severe pathfinding problems when doing ramp starts (#1927)
|
||||
if self.flight.unit_type.dcs_unit_type == F_14A or (
|
||||
self.flight.unit_type.dcs_unit_type == Su_33
|
||||
and group_units[0] is not None
|
||||
and group_units[0].type == KUZNECOW.id
|
||||
|
||||
25
game/missiongenerator/aircraft/waypoints/_helper.py
Normal file
25
game/missiongenerator/aircraft/waypoints/_helper.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from dcs import Mission
|
||||
from dcs.action import SetFlag
|
||||
from dcs.condition import TimeAfter
|
||||
from dcs.task import ControlledTask
|
||||
from dcs.triggers import TriggerOnce, Event
|
||||
|
||||
from game.ato import Package
|
||||
|
||||
|
||||
def create_stop_orbit_trigger(
|
||||
orbit: ControlledTask, package: Package, mission: Mission, elapsed: int
|
||||
) -> None:
|
||||
orbit.stop_if_user_flag(id(package), True)
|
||||
orbits = [
|
||||
x
|
||||
for x in mission.triggerrules.triggers
|
||||
if x.comment == f"StopOrbit{id(package)}"
|
||||
]
|
||||
if not any(orbits):
|
||||
stop_trigger = TriggerOnce(Event.NoEvent, f"StopOrbit{id(package)}")
|
||||
stop_condition = TimeAfter(elapsed)
|
||||
stop_action = SetFlag(id(package))
|
||||
stop_trigger.add_condition(stop_condition)
|
||||
stop_trigger.add_action(stop_action)
|
||||
mission.triggerrules.triggers.append(stop_trigger)
|
||||
@@ -0,0 +1,28 @@
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import Expend, WeaponType, CarpetBombing, OptROE
|
||||
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.utils import feet, knots
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class AirAssaultIngressBuilder(PydcsWaypointBuilder):
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
air_drop = self.group.units[0].unit_type in [Hercules]
|
||||
if air_drop:
|
||||
waypoint.speed = knots(230).meters_per_second
|
||||
waypoint.speed_locked = True
|
||||
waypoint.ETA_locked = False
|
||||
tgt = self.flight.flight_plan.package.target.position
|
||||
for wpt in self.flight.flight_plan.waypoints:
|
||||
if wpt.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC:
|
||||
tgt = wpt.position
|
||||
break
|
||||
bombing = CarpetBombing(
|
||||
tgt,
|
||||
weapon_type=WeaponType.Bombs,
|
||||
expend=Expend.All,
|
||||
carpet_length=feet(9000).meters,
|
||||
)
|
||||
waypoint.add_task(bombing)
|
||||
@@ -1,9 +1,4 @@
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import Land
|
||||
|
||||
from game.utils import feet
|
||||
from dcs.point import PointAction
|
||||
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
@@ -11,16 +6,9 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
class CargoStopBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
# Create a landing task, currently only for Helos!
|
||||
if self.flight.is_helo:
|
||||
# Calculate a landing point with a small buffer to prevent AI from landing
|
||||
# directly at the static ammo depot and exploding
|
||||
landing_point = waypoint.position.random_point_within(15, 5)
|
||||
# Use Land Task with 30s duration for helos
|
||||
waypoint.add_task(Land(landing_point, duration=30))
|
||||
else:
|
||||
# Fixed wing will drop the cargo at the waypoint so we set a lower altitude
|
||||
waypoint.alt = int(feet(10000).meters)
|
||||
waypoint.alt_type = "BARO"
|
||||
waypoint.action = PointAction.FlyOverPoint
|
||||
waypoint.type = "LandingReFuAr"
|
||||
waypoint.action = PointAction.LandingReFuAr
|
||||
waypoint.landing_refuel_rearm_time = 2 # Minutes.
|
||||
if (control_point := self.waypoint.control_point) is not None:
|
||||
waypoint.airdrome_id = control_point.airdrome_id_for_landing
|
||||
return waypoint
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import logging
|
||||
import math
|
||||
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import AttackGroup, OptECMUsing, WeaponType
|
||||
from dcs.task import AttackGroup, OptECMUsing, WeaponType, Expend
|
||||
|
||||
from game.theater import TheaterGroundObject
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
@@ -28,10 +29,24 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
|
||||
continue
|
||||
|
||||
task = AttackGroup(
|
||||
miz_group.id, weapon_type=WeaponType.Auto, group_attack=True
|
||||
miz_group.id,
|
||||
weapon_type=WeaponType.Guided,
|
||||
altitude=round(self.flight.coalition.doctrine.ingress_altitude.meters),
|
||||
)
|
||||
waypoint.tasks.append(task)
|
||||
|
||||
dir = target.position.heading_between_point(waypoint.position)
|
||||
|
||||
task = AttackGroup(
|
||||
miz_group.id,
|
||||
weapon_type=WeaponType.Unguided,
|
||||
expend=Expend.All,
|
||||
direction=math.radians(dir),
|
||||
altitude=round(self.flight.coalition.doctrine.ingress_altitude.meters),
|
||||
)
|
||||
task.params["altitudeEnabled"] = False
|
||||
waypoint.tasks.append(task)
|
||||
|
||||
# Preemptively use ECM to better avoid getting swatted.
|
||||
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfDetectedLockByRadar)
|
||||
waypoint.tasks.append(ecm_option)
|
||||
|
||||
@@ -4,6 +4,7 @@ from dcs.point import MovingPoint
|
||||
from dcs.task import ControlledTask, OptFormation, OrbitAction
|
||||
|
||||
from game.ato.flightplans.loiter import LoiterFlightPlan
|
||||
from ._helper import create_stop_orbit_trigger
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
@@ -22,8 +23,10 @@ class HoldPointBuilder(PydcsWaypointBuilder):
|
||||
return
|
||||
push_time = self.flight.flight_plan.push_time
|
||||
self.waypoint.departure_time = push_time
|
||||
loiter.stop_after_time(
|
||||
int((push_time - self.elapsed_mission_time).total_seconds())
|
||||
)
|
||||
elapsed = int((push_time - self.elapsed_mission_time).total_seconds()) - 60
|
||||
loiter.stop_after_time(elapsed)
|
||||
# What follows is some code to cope with the broken 'stop after time' condition
|
||||
create_stop_orbit_trigger(loiter, self.package, self.mission, elapsed)
|
||||
# end of hotfix
|
||||
waypoint.add_task(loiter)
|
||||
waypoint.add_task(OptFormation.finger_four_close())
|
||||
|
||||
@@ -1,50 +1,60 @@
|
||||
from typing import List, Type
|
||||
import random
|
||||
from typing import List
|
||||
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import (
|
||||
ControlledTask,
|
||||
EngageTargets,
|
||||
EscortTaskAction,
|
||||
OptECMUsing,
|
||||
OptFormation,
|
||||
TargetType,
|
||||
Targets,
|
||||
)
|
||||
|
||||
from game.ato import FlightType
|
||||
from game.utils import nautical_miles
|
||||
from game.theater import NavalControlPoint
|
||||
from game.utils import nautical_miles, feet
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class JoinPointBuilder(PydcsWaypointBuilder):
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
waypoint.tasks.append(OptFormation.finger_four_open())
|
||||
|
||||
doctrine = self.flight.coalition.doctrine
|
||||
|
||||
if self.flight.flight_type == FlightType.ESCORT:
|
||||
self.configure_escort_tasks(
|
||||
waypoint,
|
||||
[
|
||||
Targets.All.Air.Planes.Fighters,
|
||||
Targets.All.Air.Planes.MultiroleFighters,
|
||||
Targets.All.Air.Planes.Fighters.id,
|
||||
Targets.All.Air.Planes.MultiroleFighters.id,
|
||||
],
|
||||
max_dist=doctrine.escort_engagement_range.nautical_miles,
|
||||
vertical_spacing=doctrine.escort_spacing.feet,
|
||||
)
|
||||
|
||||
if self.flight.count < 4:
|
||||
waypoint.tasks.append(OptFormation.line_abreast_open())
|
||||
else:
|
||||
waypoint.tasks.append(OptFormation.spread_four_open())
|
||||
|
||||
elif self.flight.flight_type == FlightType.SEAD_ESCORT:
|
||||
self.configure_escort_tasks(
|
||||
waypoint, [Targets.All.GroundUnits.AirDefence.AAA.SAMRelated]
|
||||
)
|
||||
if isinstance(self.flight.package.target, NavalControlPoint):
|
||||
self.configure_escort_tasks(
|
||||
waypoint,
|
||||
[
|
||||
Targets.All.Naval.id,
|
||||
Targets.All.GroundUnits.AirDefence.AAA.SAMRelated.id,
|
||||
],
|
||||
max_dist=doctrine.sead_escort_engagement_range.nautical_miles,
|
||||
vertical_spacing=doctrine.sead_escort_spacing.feet,
|
||||
)
|
||||
else:
|
||||
self.configure_escort_tasks(
|
||||
waypoint,
|
||||
[Targets.All.GroundUnits.AirDefence.AAA.SAMRelated.id],
|
||||
max_dist=doctrine.sead_escort_engagement_range.nautical_miles,
|
||||
vertical_spacing=doctrine.sead_escort_spacing.feet,
|
||||
)
|
||||
|
||||
# Let the AI use ECM to preemptively defend themselves.
|
||||
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfDetectedLockByRadar)
|
||||
waypoint.tasks.append(ecm_option)
|
||||
|
||||
if self.flight.count < 4:
|
||||
waypoint.tasks.append(OptFormation.line_abreast_open())
|
||||
else:
|
||||
waypoint.tasks.append(OptFormation.spread_four_open())
|
||||
|
||||
elif not self.flight.flight_type.is_air_to_air:
|
||||
# Capture any non A/A type to avoid issues with SPJs that use the primary radar such as the F/A-18C.
|
||||
# You can bully them with STT to not be able to fire radar guided missiles at you,
|
||||
@@ -54,48 +64,31 @@ class JoinPointBuilder(PydcsWaypointBuilder):
|
||||
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfOnlyLockByRadar)
|
||||
waypoint.tasks.append(ecm_option)
|
||||
|
||||
waypoint.tasks.append(OptFormation.finger_four_open())
|
||||
|
||||
@staticmethod
|
||||
def configure_escort_tasks(
|
||||
waypoint: MovingPoint, target_types: List[Type[TargetType]]
|
||||
self,
|
||||
waypoint: MovingPoint,
|
||||
target_types: List[str],
|
||||
max_dist: float = 30.0,
|
||||
vertical_spacing: float = 2000.0,
|
||||
) -> None:
|
||||
# Ideally we would use the escort mission type and escort task to have
|
||||
# the AI automatically but the AI only escorts AI flights while they are
|
||||
# traveling between waypoints. When an AI flight performs an attack
|
||||
# (such as attacking the mission target), AI escorts wander aimlessly
|
||||
# until the escorted group resumes its flight plan.
|
||||
#
|
||||
# As such, we instead use the Search Then Engage task, which is an
|
||||
# enroute task that causes the AI to follow their flight plan and engage
|
||||
# enemies of the set type within a certain distance. The downside to
|
||||
# this approach is that AI escorts are no longer related to the group
|
||||
# they are escorting, aside from the fact that they fly a similar flight
|
||||
# plan at the same time. With Escort, the escorts will follow the
|
||||
# escorted group out of the area. The strike element may or may not fly
|
||||
# directly over the target, and they may or may not require multiple
|
||||
# attack runs. For the escort flight we must just assume a flight plan
|
||||
# for the escort to fly. If the strike flight doesn't need to overfly
|
||||
# the target, the escorts are needlessly going in harms way. If the
|
||||
# strike flight needs multiple passes, the escorts may leave before the
|
||||
# escorted aircraft do.
|
||||
#
|
||||
# Another possible option would be to use Search Then Engage for join ->
|
||||
# ingress and egress -> split, but use a Search Then Engage in Zone task
|
||||
# for the target area that is set to end on a flag flip that occurs when
|
||||
# the strike aircraft finish their attack task.
|
||||
#
|
||||
# https://forums.eagle.ru/topic/251798-options-for-alternate-ai-escort-behavior
|
||||
waypoint.add_task(
|
||||
ControlledTask(
|
||||
EngageTargets(
|
||||
# TODO: From doctrine.
|
||||
max_distance=int(nautical_miles(30).meters),
|
||||
targets=target_types,
|
||||
)
|
||||
rx = (random.random() + 0.1) * 333
|
||||
ry = feet(vertical_spacing).meters
|
||||
rz = (random.random() + 0.1) * 166 * random.choice([-1, 1])
|
||||
pos = {"x": rx, "y": ry, "z": rz}
|
||||
|
||||
group_id = None
|
||||
if self.package.primary_flight is not None:
|
||||
group_id = self.package.primary_flight.group_id
|
||||
|
||||
escort = ControlledTask(
|
||||
EscortTaskAction(
|
||||
group_id=group_id,
|
||||
engagement_max_dist=int(nautical_miles(max_dist).meters),
|
||||
targets=target_types,
|
||||
position=pos,
|
||||
)
|
||||
)
|
||||
|
||||
# We could set this task to end at the split point. pydcs doesn't
|
||||
# currently support that task end condition though, and we don't really
|
||||
# need it.
|
||||
escort.stop_if_user_flag(f"split-{id(self.package)}", True)
|
||||
|
||||
waypoint.tasks.append(escort)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user