mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Compare commits
285 Commits
f16i-mod-s
...
v1.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 |
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:
|
||||
|
||||
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -45,9 +45,19 @@ jobs:
|
||||
with:
|
||||
webhook_url: ${{ secrets.discord_webhook_betas }}
|
||||
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 }}"
|
||||
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: startsWith(github.ref_name, 'test')
|
||||
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'
|
||||
|
||||
@@ -39,7 +39,7 @@ Currently, Retribution supports the following new features:
|
||||
|
||||
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
|
||||
|
||||
168
changelog.md
168
changelog.md
@@ -1,79 +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]** 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 patches 30.09.22 & 09.10.22
|
||||
* **[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.
|
||||
* **[Data]** Added data to support KS-19 & SON-9, including support for "AAA Site" layout.
|
||||
* **[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
|
||||
* **[Mission Generation]** Fixed mission scripting error caused by control points with apostrophes in their names, such as Tha'lah.
|
||||
* **[Mission Generation]** Fixed generation issue that would cause AI helicopters to get stuck after taking off from a FARP.
|
||||
* **[Modding]** Loadouts with invalid weapons (typically new DCS weapons not yet available in Liberation) will be ignored rather than causing an error.
|
||||
* **[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.
|
||||
* **[Squadrons]** Helicopter squadrons can no longer be assigned to FOBs that are not FARPs.
|
||||
* **[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.
|
||||
* **[UI]** Fixed and issue where the liberation main exe was still running after application close.
|
||||
* **[UI]** Disable player slots for non-flyable aircraft.
|
||||
* **[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.
|
||||
|
||||
|
||||
19613
client/package-lock.json
generated
19613
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,3 @@
|
||||
from .game import Game
|
||||
from .version import VERSION
|
||||
from .migrator import Migrator
|
||||
|
||||
@@ -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,6 +119,7 @@ from dcs.planes import (
|
||||
Tu_95MS,
|
||||
WingLoong_I,
|
||||
Yak_40,
|
||||
MB_339A,
|
||||
)
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
@@ -131,16 +135,19 @@ from pydcs_extensions.f16i_idf.f16i_idf import (
|
||||
F_16D_52,
|
||||
F_16D_50_NS,
|
||||
F_16D_52_NS,
|
||||
F_16D_Barak,
|
||||
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
|
||||
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
|
||||
|
||||
@@ -151,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,
|
||||
@@ -169,16 +177,22 @@ CAP_CAPABLE = [
|
||||
MiG_29S,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak,
|
||||
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,
|
||||
@@ -208,7 +222,6 @@ CAP_CAPABLE = [
|
||||
F_86F_Sabre,
|
||||
MiG_15bis,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
VSN_F84G,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
@@ -224,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 = [
|
||||
@@ -236,14 +254,16 @@ CAS_CAPABLE = [
|
||||
Su_25,
|
||||
F_15D,
|
||||
F_15E,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak,
|
||||
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,
|
||||
@@ -251,6 +271,8 @@ CAS_CAPABLE = [
|
||||
JF_17,
|
||||
AV8BNA,
|
||||
A_10A,
|
||||
F_16A_MLU,
|
||||
F_16A,
|
||||
B_1B,
|
||||
A_4E_C,
|
||||
F_14B,
|
||||
@@ -278,6 +300,7 @@ CAS_CAPABLE = [
|
||||
SA342M,
|
||||
SA342L,
|
||||
Ka_50,
|
||||
Ka_50_3,
|
||||
Mi_28N,
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
@@ -298,6 +321,8 @@ CAS_CAPABLE = [
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
MB_339A,
|
||||
Bronco_OV_10A,
|
||||
UH_1H,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
@@ -317,35 +342,38 @@ CAS_CAPABLE = [
|
||||
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_F4B,
|
||||
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,
|
||||
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,
|
||||
@@ -359,15 +387,21 @@ SEAD_CAPABLE = [
|
||||
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,
|
||||
@@ -381,6 +415,7 @@ DEAD_CAPABLE = SEAD_CAPABLE + [
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
Bronco_OV_10A,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
MosquitoFBMkVI,
|
||||
@@ -412,14 +447,18 @@ STRIKE_CAPABLE = [
|
||||
Tornado_GR4,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak,
|
||||
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,
|
||||
@@ -444,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,
|
||||
@@ -462,6 +504,7 @@ STRIKE_CAPABLE = [
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
MB_339A,
|
||||
B_17G,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
@@ -483,7 +526,6 @@ STRIKE_CAPABLE = [
|
||||
VSN_F104C,
|
||||
VSN_F105G,
|
||||
VSN_F105D,
|
||||
VSN_F4B,
|
||||
VSN_F84G,
|
||||
]
|
||||
|
||||
@@ -493,9 +535,12 @@ ANTISHIP_CAPABLE = [
|
||||
Tu_142,
|
||||
Tu_22M3,
|
||||
H_6J,
|
||||
F_16A,
|
||||
FA_18E,
|
||||
FA_18F,
|
||||
FA_18C_hornet,
|
||||
JAS39Gripen_AG,
|
||||
F_16A_MLU,
|
||||
F_16A,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
JF_17,
|
||||
@@ -524,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,
|
||||
@@ -533,14 +586,18 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
AJS37,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak,
|
||||
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,
|
||||
@@ -565,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,
|
||||
@@ -583,6 +635,7 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
MB_339A,
|
||||
B_17G,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
@@ -604,7 +657,6 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
VSN_F104G,
|
||||
VSN_F104C,
|
||||
VSN_F100,
|
||||
VSN_F4B,
|
||||
]
|
||||
|
||||
# For any aircraft that isn't necessarily directly involved in strike
|
||||
@@ -640,7 +692,6 @@ AIR_ASSAULT_CAPABLE = [
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
Hercules,
|
||||
C_47,
|
||||
]
|
||||
|
||||
DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
|
||||
@@ -681,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:
|
||||
@@ -691,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
|
||||
|
||||
|
||||
@@ -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
|
||||
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"Retribution {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", "Retribution BARCAP"),
|
||||
FlightType.BARCAP: ("CAP HEAVY", "CAP", "Retribution 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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -29,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,
|
||||
@@ -53,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
|
||||
@@ -94,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,
|
||||
@@ -153,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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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
|
||||
|
||||
|
||||
@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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from random import randint
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()],
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -60,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)
|
||||
@@ -116,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
|
||||
@@ -175,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"))
|
||||
@@ -203,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", [])
|
||||
]
|
||||
@@ -284,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
|
||||
@@ -322,8 +335,9 @@ class Faction:
|
||||
if not mod_settings.uh_60l:
|
||||
self.remove_aircraft("UH-60L")
|
||||
self.remove_aircraft("KC130J")
|
||||
if not mod_settings.f4b_phantom:
|
||||
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:
|
||||
@@ -332,10 +346,10 @@ class Faction:
|
||||
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()
|
||||
# Remove the stock Viper because that DCS ID is now used by the Sufa
|
||||
self.remove_aircraft_by_name("F-16CM Fighting Falcon (Block 50)")
|
||||
if not mod_settings.f22_raptor:
|
||||
self.remove_aircraft("F-22A")
|
||||
if not mod_settings.f84g_thunderjet:
|
||||
@@ -343,6 +357,7 @@ class Faction:
|
||||
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")
|
||||
@@ -353,6 +368,7 @@ class Faction:
|
||||
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")
|
||||
@@ -361,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")
|
||||
@@ -407,13 +425,56 @@ 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:
|
||||
@@ -435,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:
|
||||
|
||||
@@ -31,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)
|
||||
|
||||
10
game/game.py
10
game/game.py
@@ -528,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:
|
||||
|
||||
@@ -137,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
|
||||
@@ -238,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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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,24 +1,14 @@
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.point import PointAction
|
||||
from dcs.task import Land
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
|
||||
from game.utils import feet
|
||||
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,60 +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.theater import NavalControlPoint
|
||||
from game.utils import nautical_miles
|
||||
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:
|
||||
if isinstance(self.flight.package.target, NavalControlPoint):
|
||||
self.configure_escort_tasks(
|
||||
waypoint,
|
||||
[
|
||||
Targets.All.Naval,
|
||||
Targets.All.GroundUnits.AirDefence.AAA.SAMRelated,
|
||||
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]
|
||||
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,
|
||||
@@ -64,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)
|
||||
|
||||
17
game/missiongenerator/aircraft/waypoints/landingzone.py
Normal file
17
game/missiongenerator/aircraft/waypoints/landingzone.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import Land
|
||||
|
||||
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class LandingZoneBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
# Create a landing task, currently only for Helos!
|
||||
# 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))
|
||||
return waypoint
|
||||
@@ -4,7 +4,7 @@ from datetime import timedelta
|
||||
from typing import Any, Iterable, Union
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.planes import AJS37, F_14B, JF_17
|
||||
from dcs.planes import AJS37, F_14A_135_GR, F_14B, JF_17
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
|
||||
@@ -91,5 +91,5 @@ class PydcsWaypointBuilder:
|
||||
for i, t in enumerate(targets):
|
||||
if self.group.units[0].unit_type == JF_17 and i < 4:
|
||||
self.group.add_nav_target_point(t.position, "PP" + str(i + 1))
|
||||
if self.group.units[0].unit_type == F_14B and i == 0:
|
||||
if self.group.units[0].unit_type in [F_14B, F_14A_135_GR] and i == 0:
|
||||
self.group.add_nav_target_point(t.position, "ST")
|
||||
|
||||
@@ -12,6 +12,7 @@ from dcs.task import (
|
||||
|
||||
from game.ato import FlightType
|
||||
from game.ato.flightplans.patrolling import PatrollingFlightPlan
|
||||
from ._helper import create_stop_orbit_trigger
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
@@ -57,7 +58,11 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
racetrack = ControlledTask(orbit)
|
||||
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
|
||||
loiter_duration = flight_plan.patrol_end_time - self.elapsed_mission_time
|
||||
racetrack.stop_after_time(int(loiter_duration.total_seconds()))
|
||||
elapsed = int(loiter_duration.total_seconds())
|
||||
racetrack.stop_after_time(elapsed)
|
||||
# What follows is some code to cope with the broken 'stop after time' condition
|
||||
create_stop_orbit_trigger(racetrack, self.package, self.mission, elapsed)
|
||||
# end of hotfix
|
||||
waypoint.add_task(racetrack)
|
||||
|
||||
def configure_refueling_actions(self, waypoint: MovingPoint) -> None:
|
||||
@@ -66,11 +71,20 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
if self.flight.unit_type.dcs_unit_type.tacan:
|
||||
tanker_info = self.mission_data.tankers[-1]
|
||||
tacan = tanker_info.tacan
|
||||
tacan_callsign = {
|
||||
"Texaco": "TEX",
|
||||
"Arco": "ARC",
|
||||
"Shell": "SHL",
|
||||
}.get(tanker_info.callsign)
|
||||
if self.flight.tcn_name is None:
|
||||
cs = tanker_info.callsign[:-2]
|
||||
csn = tanker_info.callsign[-1]
|
||||
tacan_callsign = {
|
||||
"Texaco": "TX",
|
||||
"Arco": "AC",
|
||||
"Shell": "SH",
|
||||
}.get(cs)
|
||||
if tacan_callsign:
|
||||
tacan_callsign = tacan_callsign + csn
|
||||
else:
|
||||
tacan_callsign = cs[0:2] + csn
|
||||
else:
|
||||
tacan_callsign = self.flight.tcn_name
|
||||
|
||||
waypoint.add_task(
|
||||
ActivateBeaconCommand(
|
||||
@@ -80,5 +94,6 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
bearing=True,
|
||||
unit_id=self.group.units[0].id,
|
||||
aa=True,
|
||||
tanker=True,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import RefuelingTaskAction
|
||||
from dcs.task import RefuelingTaskAction, ControlledTask
|
||||
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class RefuelPointBuilder(PydcsWaypointBuilder):
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
waypoint.add_task(RefuelingTaskAction())
|
||||
refuel = ControlledTask(RefuelingTaskAction())
|
||||
refuel.start_probability(10)
|
||||
waypoint.add_task(refuel)
|
||||
return super().add_tasks(waypoint)
|
||||
|
||||
@@ -49,9 +49,10 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
|
||||
# All non ARM types like Decoys will use the normal AttackGroup Task
|
||||
attack_task = AttackGroup(
|
||||
miz_group.id,
|
||||
weapon_type=DcsWeaponType.Guided,
|
||||
weapon_type=DcsWeaponType.Decoy,
|
||||
group_attack=True,
|
||||
expend=Expend.All,
|
||||
altitude=waypoint.alt,
|
||||
)
|
||||
waypoint.tasks.append(attack_task)
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import OptECMUsing, OptFormation
|
||||
from dcs.task import OptECMUsing, OptFormation, RunScript
|
||||
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class SplitPointBuilder(PydcsWaypointBuilder):
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
|
||||
if 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,
|
||||
@@ -16,4 +15,12 @@ class SplitPointBuilder(PydcsWaypointBuilder):
|
||||
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfOnlyLockByRadar)
|
||||
waypoint.tasks.append(ecm_option)
|
||||
|
||||
waypoint.tasks.append(OptFormation.finger_four_close())
|
||||
waypoint.tasks.append(OptFormation.finger_four_close())
|
||||
waypoint.speed_locked = True
|
||||
waypoint.speed = self.flight.coalition.doctrine.rtb_speed.meters_per_second
|
||||
waypoint.ETA_locked = False
|
||||
if self.flight is self.package.primary_flight:
|
||||
script = RunScript(
|
||||
f'trigger.action.setUserFlag("split-{id(self.package)}", true)'
|
||||
)
|
||||
waypoint.tasks.append(script)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class StopoverBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
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,44 +1,60 @@
|
||||
import copy
|
||||
|
||||
from dcs.planes import B_17G, B_52H, Tu_22M3
|
||||
from dcs import Point
|
||||
from dcs.planes import B_17G, B_52H, Tu_22M3, B_1B
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import Bombing, OptFormation, WeaponType, Expend
|
||||
from dcs.task import Bombing, Expend, OptFormation, WeaponType, CarpetBombing
|
||||
|
||||
from game.utils import mach
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class StrikeIngressBuilder(PydcsWaypointBuilder):
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
if self.group.units[0].unit_type in [B_17G, B_52H, Tu_22M3]:
|
||||
self.add_bombing_tasks(waypoint)
|
||||
else:
|
||||
self.add_strike_tasks(waypoint)
|
||||
|
||||
bomber = self.group.units[0].unit_type in [B_17G, Tu_22M3]
|
||||
bomber_guided = self.group.units[0].unit_type in [B_1B, B_52H]
|
||||
waypoint.tasks.append(OptFormation.trail_open())
|
||||
if bomber_guided or not bomber:
|
||||
self.add_strike_tasks(waypoint, WeaponType.Guided)
|
||||
|
||||
waypoint.tasks.append(OptFormation.ww2_bomber_element_close())
|
||||
self.add_bombing_tasks(waypoint)
|
||||
|
||||
def add_bombing_tasks(self, waypoint: MovingPoint) -> None:
|
||||
targets = self.waypoint.targets
|
||||
if not targets:
|
||||
return
|
||||
|
||||
center = copy.copy(targets[0].position)
|
||||
center: Point = copy.copy(targets[0].position)
|
||||
for target in targets[1:]:
|
||||
center += target.position
|
||||
center /= len(targets)
|
||||
bombing = Bombing(
|
||||
center, weapon_type=WeaponType.Bombs, expend=Expend.All, group_attack=True
|
||||
avg_spacing = 0.0
|
||||
for t in targets:
|
||||
avg_spacing += center.distance_to_point(t.position)
|
||||
avg_spacing /= len(targets)
|
||||
bombing = CarpetBombing(
|
||||
center,
|
||||
weapon_type=WeaponType.Bombs,
|
||||
expend=Expend.All,
|
||||
carpet_length=avg_spacing,
|
||||
altitude=round(self.flight.coalition.doctrine.ingress_altitude.meters),
|
||||
)
|
||||
waypoint.tasks.append(bombing)
|
||||
|
||||
def add_strike_tasks(self, waypoint: MovingPoint) -> None:
|
||||
def add_strike_tasks(
|
||||
self, waypoint: MovingPoint, weapon_type: WeaponType = WeaponType.Auto
|
||||
) -> None:
|
||||
ga = True if self.flight.count > 1 else False
|
||||
for target in self.waypoint.targets:
|
||||
bombing = Bombing(
|
||||
target.position, weapon_type=WeaponType.Auto, group_attack=True
|
||||
)
|
||||
bombing = Bombing(target.position, weapon_type=weapon_type, group_attack=ga)
|
||||
# If there is only one target, drop all ordnance in one pass.
|
||||
if len(self.waypoint.targets) == 1:
|
||||
bombing.params["expend"] = Expend.All.value
|
||||
waypoint.tasks.append(bombing)
|
||||
|
||||
doctrine = self.flight.coalition.doctrine
|
||||
waypoint.speed = mach(0.85, doctrine.ingress_altitude).meters_per_second
|
||||
|
||||
# Register special waypoints
|
||||
self.register_special_waypoints(self.waypoint.targets)
|
||||
|
||||
@@ -16,12 +16,13 @@ from game.ato import Flight, FlightWaypoint
|
||||
from game.ato.flightstate import InFlight, WaitingForStart
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.ato.starttype import StartType
|
||||
from game.missiongenerator.aircraft.waypoints.stopover import StopoverBuilder
|
||||
from game.missiongenerator.aircraft.waypoints.cargostop import CargoStopBuilder
|
||||
from game.missiongenerator.missiondata import MissionData
|
||||
from game.settings import Settings
|
||||
from game.utils import pairwise
|
||||
from .airassaultingress import AirAssaultIngressBuilder
|
||||
from .baiingress import BaiIngressBuilder
|
||||
from .cargostop import CargoStopBuilder
|
||||
from .landingzone import LandingZoneBuilder
|
||||
from .casingress import CasIngressBuilder
|
||||
from .deadingress import DeadIngressBuilder
|
||||
from .default import DefaultWaypointBuilder
|
||||
@@ -118,7 +119,6 @@ class WaypointGenerator:
|
||||
|
||||
def builder_for_waypoint(self, waypoint: FlightWaypoint) -> PydcsWaypointBuilder:
|
||||
builders = {
|
||||
FlightWaypointType.DROP_OFF: CargoStopBuilder,
|
||||
FlightWaypointType.INGRESS_BAI: BaiIngressBuilder,
|
||||
FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
|
||||
FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder,
|
||||
@@ -133,9 +133,11 @@ class WaypointGenerator:
|
||||
FlightWaypointType.LOITER: HoldPointBuilder,
|
||||
FlightWaypointType.PATROL: RaceTrackEndBuilder,
|
||||
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
|
||||
FlightWaypointType.PICKUP: CargoStopBuilder,
|
||||
FlightWaypointType.PICKUP_ZONE: LandingZoneBuilder,
|
||||
FlightWaypointType.DROPOFF_ZONE: LandingZoneBuilder,
|
||||
FlightWaypointType.REFUEL: RefuelPointBuilder,
|
||||
FlightWaypointType.STOPOVER: StopoverBuilder,
|
||||
FlightWaypointType.CARGO_STOP: CargoStopBuilder,
|
||||
FlightWaypointType.INGRESS_AIR_ASSAULT: AirAssaultIngressBuilder,
|
||||
}
|
||||
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
|
||||
return builder(
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import List, TYPE_CHECKING, Tuple, Type
|
||||
|
||||
from dcs.mission import Mission, StartType
|
||||
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135, PlaneType
|
||||
from dcs.task import (
|
||||
AWACS,
|
||||
ActivateBeaconCommand,
|
||||
MainTask,
|
||||
Refueling,
|
||||
SetImmortalCommand,
|
||||
SetInvisibleCommand,
|
||||
)
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game.ato.ai_flight_planner_db import AEWC_CAPABLE
|
||||
from game.callsigns import callsign_for_support_unit
|
||||
from game.naming import namegen
|
||||
from game.radio.radios import RadioRegistry
|
||||
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
|
||||
from game.utils import Heading
|
||||
from .airconflictdescription import AirConflictDescription
|
||||
from .missiondata import AwacsInfo, MissionData, TankerInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
TANKER_DISTANCE = 15000
|
||||
TANKER_ALT = 4572
|
||||
TANKER_HEADING_OFFSET = 45
|
||||
|
||||
AWACS_DISTANCE = 150000
|
||||
AWACS_ALT = 13000
|
||||
|
||||
|
||||
class AirSupportGenerator:
|
||||
def __init__(
|
||||
self,
|
||||
mission: Mission,
|
||||
conflict: AirConflictDescription,
|
||||
game: Game,
|
||||
radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry,
|
||||
mission_data: MissionData,
|
||||
) -> None:
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
self.radio_registry = radio_registry
|
||||
self.tacan_registry = tacan_registry
|
||||
self.mission_data = mission_data
|
||||
|
||||
@classmethod
|
||||
def support_tasks(cls) -> List[Type[MainTask]]:
|
||||
return [Refueling, AWACS]
|
||||
|
||||
@staticmethod
|
||||
def _get_tanker_params(unit_type: Type[UnitType]) -> Tuple[int, int]:
|
||||
if unit_type is KC130:
|
||||
return TANKER_ALT - 500, 596
|
||||
elif unit_type is KC_135:
|
||||
return TANKER_ALT, 770
|
||||
elif unit_type is KC135MPRS:
|
||||
return TANKER_ALT + 500, 596
|
||||
return TANKER_ALT, 574
|
||||
|
||||
def generate(self) -> None:
|
||||
player_cp = (
|
||||
self.conflict.blue_cp
|
||||
if self.conflict.blue_cp.captured
|
||||
else self.conflict.red_cp
|
||||
)
|
||||
|
||||
country = self.mission.country(self.game.blue.country_name)
|
||||
|
||||
if not self.game.settings.disable_legacy_tanker:
|
||||
fallback_tanker_number = 0
|
||||
|
||||
for i, tanker_unit_type in enumerate(
|
||||
self.game.faction_for(player=True).tankers
|
||||
):
|
||||
unit_type = tanker_unit_type.dcs_unit_type
|
||||
if not issubclass(unit_type, PlaneType):
|
||||
logging.warning(f"Refueling aircraft {unit_type} must be a plane")
|
||||
continue
|
||||
|
||||
# TODO: Make loiter altitude a property of the unit type.
|
||||
alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
tacan = self.tacan_registry.alloc_for_band(
|
||||
TacanBand.Y, TacanUsage.AirToAir
|
||||
)
|
||||
tanker_heading = Heading.from_degrees(
|
||||
self.conflict.red_cp.position.heading_between_point(
|
||||
self.conflict.blue_cp.position
|
||||
)
|
||||
+ TANKER_HEADING_OFFSET * i
|
||||
)
|
||||
tanker_position = player_cp.position.point_from_heading(
|
||||
tanker_heading.degrees, TANKER_DISTANCE
|
||||
)
|
||||
tanker_group = self.mission.refuel_flight(
|
||||
country=country,
|
||||
name=namegen.next_tanker_name(country, tanker_unit_type),
|
||||
airport=None,
|
||||
plane_type=unit_type,
|
||||
position=tanker_position,
|
||||
altitude=alt,
|
||||
race_distance=58000,
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
speed=airspeed,
|
||||
tacanchannel=str(tacan),
|
||||
)
|
||||
tanker_group.set_frequency(freq.mhz)
|
||||
|
||||
callsign = callsign_for_support_unit(tanker_group)
|
||||
tacan_callsign = {
|
||||
"Texaco": "TEX",
|
||||
"Arco": "ARC",
|
||||
"Shell": "SHL",
|
||||
}.get(callsign)
|
||||
if tacan_callsign is None:
|
||||
# The dict above is all the callsigns currently in the game, but
|
||||
# non-Western countries don't use the callsigns and instead just
|
||||
# use numbers. It's possible that none of those nations have
|
||||
# TACAN compatible refueling aircraft, but fallback just in
|
||||
# case.
|
||||
tacan_callsign = f"TK{fallback_tanker_number}"
|
||||
fallback_tanker_number += 1
|
||||
|
||||
if tanker_unit_type != IL_78M:
|
||||
# Override PyDCS tacan channel.
|
||||
tanker_group.points[0].tasks.pop()
|
||||
tanker_group.points[0].tasks.append(
|
||||
ActivateBeaconCommand(
|
||||
tacan.number,
|
||||
tacan.band.value,
|
||||
tacan_callsign,
|
||||
True,
|
||||
tanker_group.units[0].id,
|
||||
True,
|
||||
)
|
||||
)
|
||||
|
||||
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.mission_data.tankers.append(
|
||||
TankerInfo(
|
||||
group_name=str(tanker_group.name),
|
||||
callsign=callsign,
|
||||
variant=tanker_unit_type.name,
|
||||
freq=freq,
|
||||
tacan=tacan,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
blue=True,
|
||||
)
|
||||
)
|
||||
|
||||
if not self.game.settings.disable_legacy_aewc:
|
||||
possible_awacs = [
|
||||
a
|
||||
for a in self.game.faction_for(player=True).aircrafts
|
||||
if a in AEWC_CAPABLE
|
||||
]
|
||||
|
||||
if not possible_awacs:
|
||||
logging.warning("No AWACS for faction")
|
||||
return
|
||||
|
||||
awacs_unit = possible_awacs[0]
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
|
||||
unit_type = awacs_unit.dcs_unit_type
|
||||
if not issubclass(unit_type, PlaneType):
|
||||
logging.warning(f"AWACS aircraft {unit_type} must be a plane")
|
||||
return
|
||||
|
||||
awacs_flight = self.mission.awacs_flight(
|
||||
country=country,
|
||||
name=namegen.next_awacs_name(country),
|
||||
plane_type=unit_type,
|
||||
altitude=AWACS_ALT,
|
||||
airport=None,
|
||||
position=self.conflict.center.random_point_within(
|
||||
AWACS_DISTANCE, AWACS_DISTANCE
|
||||
),
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
)
|
||||
awacs_flight.set_frequency(freq.mhz)
|
||||
|
||||
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.mission_data.awacs.append(
|
||||
AwacsInfo(
|
||||
group_name=str(awacs_flight.name),
|
||||
callsign=callsign_for_support_unit(awacs_flight),
|
||||
freq=freq,
|
||||
depature_location=None,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
blue=True,
|
||||
)
|
||||
)
|
||||
@@ -17,7 +17,7 @@ from game.radio.radios import RadioFrequency
|
||||
from game.runways import RunwayData
|
||||
from game.theater import ControlPoint, FrontLine
|
||||
from .aircraft.flightdata import FlightData
|
||||
from .airsupportgenerator import AwacsInfo, TankerInfo
|
||||
from .missiondata import AwacsInfo, TankerInfo
|
||||
from .flotgenerator import JtacInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -58,6 +58,7 @@ class MissionInfoGenerator:
|
||||
self.awacs: List[AwacsInfo] = []
|
||||
self.comms: List[CommInfo] = []
|
||||
self.flights: List[FlightData] = []
|
||||
self.player_flights: List[FlightData] = []
|
||||
self.jtacs: List[JtacInfo] = []
|
||||
self.tankers: List[TankerInfo] = []
|
||||
self.frontlines: List[FrontLineInfo] = []
|
||||
@@ -87,6 +88,8 @@ class MissionInfoGenerator:
|
||||
flight: Flight information.
|
||||
"""
|
||||
self.flights.append(flight)
|
||||
if len(flight.client_units) > 0:
|
||||
self.player_flights.append(flight)
|
||||
|
||||
def add_jtac(self, jtac: JtacInfo) -> None:
|
||||
"""Adds a JTAC to the mission.
|
||||
|
||||
@@ -10,6 +10,7 @@ from dcs.unit import Vehicle
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.missiongenerator.groundforcepainter import GroundForcePainter
|
||||
from game.transfers import Convoy
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import kph
|
||||
@@ -72,6 +73,7 @@ class ConvoyGenerator:
|
||||
for_player: bool,
|
||||
) -> VehicleGroup:
|
||||
country = self.mission.country(self.game.coalition_for(for_player).country_name)
|
||||
faction = self.game.faction_for(for_player)
|
||||
|
||||
unit_types = list(units.items())
|
||||
main_unit_type, main_unit_count = unit_types[0]
|
||||
@@ -97,6 +99,7 @@ class ConvoyGenerator:
|
||||
v.position.x = position.x
|
||||
v.position.y = next(y)
|
||||
v.heading = 0
|
||||
GroundForcePainter(faction, v).apply_livery()
|
||||
group.add_unit(v)
|
||||
|
||||
return group
|
||||
|
||||
@@ -59,7 +59,6 @@ class DrawingsGenerator:
|
||||
if destination in seen:
|
||||
continue
|
||||
else:
|
||||
|
||||
# Determine path color
|
||||
if cp.captured and destination.captured:
|
||||
color = BLUE_PATH_COLOR
|
||||
|
||||
@@ -17,6 +17,7 @@ class EnvironmentGenerator:
|
||||
def set_atmospheric(self, atmospheric: AtmosphericConditions) -> None:
|
||||
self.mission.weather.qnh = atmospheric.qnh.mm_hg
|
||||
self.mission.weather.season_temperature = atmospheric.temperature_celsius
|
||||
self.mission.weather.turbulence_at_ground = int(atmospheric.turbulence_per_10cm)
|
||||
|
||||
def set_clouds(self, clouds: Optional[Clouds]) -> None:
|
||||
if clouds is None:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
from datetime import timedelta
|
||||
from typing import List, Optional, TYPE_CHECKING, Tuple
|
||||
|
||||
from dcs import Mission
|
||||
@@ -14,7 +16,6 @@ from dcs.task import (
|
||||
AFAC,
|
||||
AttackGroup,
|
||||
ControlledTask,
|
||||
EPLRS,
|
||||
FAC,
|
||||
FireAtPoint,
|
||||
GoToWaypoint,
|
||||
@@ -43,8 +44,10 @@ from game.theater.controlpoint import ControlPoint
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import Heading
|
||||
from .frontlineconflictdescription import FrontLineConflictDescription
|
||||
from .groundforcepainter import GroundForcePainter
|
||||
from .lasercoderegistry import LaserCodeRegistry
|
||||
from .missiondata import JtacInfo, MissionData
|
||||
from ..ato import FlightType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -201,7 +204,6 @@ class FlotGenerator:
|
||||
side: Country,
|
||||
forward_heading: Heading,
|
||||
) -> None:
|
||||
|
||||
infantry_position = self.conflict.find_ground_position(
|
||||
group.points[0].position.random_point_within(250, 50),
|
||||
500,
|
||||
@@ -221,7 +223,7 @@ class FlotGenerator:
|
||||
u = random.choices(
|
||||
manpads, weights=[m.spawn_weight for m in manpads]
|
||||
)[0]
|
||||
self.mission.vehicle_group(
|
||||
vg = self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, u),
|
||||
u.dcs_unit_type,
|
||||
@@ -230,6 +232,8 @@ class FlotGenerator:
|
||||
heading=forward_heading.degrees,
|
||||
move_formation=PointAction.OffRoad,
|
||||
)
|
||||
vehicle = vg.units[0]
|
||||
GroundForcePainter(faction, vehicle).apply_livery()
|
||||
return
|
||||
|
||||
possible_infantry_units = set(faction.infantry_with_class(UnitClass.INFANTRY))
|
||||
@@ -246,7 +250,7 @@ class FlotGenerator:
|
||||
weights=[u.spawn_weight for u in infantry_choices],
|
||||
k=INFANTRY_GROUP_SIZE,
|
||||
)
|
||||
self.mission.vehicle_group(
|
||||
vg = self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, units[0]),
|
||||
units[0].dcs_unit_type,
|
||||
@@ -255,10 +259,12 @@ class FlotGenerator:
|
||||
heading=forward_heading.degrees,
|
||||
move_formation=PointAction.OffRoad,
|
||||
)
|
||||
vehicle = vg.units[0]
|
||||
GroundForcePainter(faction, vehicle).apply_livery()
|
||||
|
||||
for unit in units[1:]:
|
||||
position = infantry_position.random_point_within(55, 5)
|
||||
self.mission.vehicle_group(
|
||||
vg = self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, unit),
|
||||
unit.dcs_unit_type,
|
||||
@@ -267,9 +273,32 @@ class FlotGenerator:
|
||||
heading=forward_heading.degrees,
|
||||
move_formation=PointAction.OffRoad,
|
||||
)
|
||||
vehicle = vg.units[0]
|
||||
GroundForcePainter(faction, vehicle).apply_livery()
|
||||
|
||||
def _earliest_tot_on_flot(self, player: bool) -> timedelta:
|
||||
tots = [
|
||||
x.time_over_target
|
||||
for x in self.game.ato_for(player).packages
|
||||
if x.primary_task == FlightType.CAS
|
||||
]
|
||||
return (
|
||||
timedelta(seconds=random.randint(300, 1800))
|
||||
if len(tots) == 0
|
||||
else min(
|
||||
[
|
||||
x.time_over_target
|
||||
for x in self.game.ato_for(player).packages
|
||||
if x.primary_task == FlightType.CAS
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def _set_reform_waypoint(
|
||||
self, dcs_group: VehicleGroup, forward_heading: Heading
|
||||
self,
|
||||
dcs_group: VehicleGroup,
|
||||
forward_heading: Heading,
|
||||
hold_duration: timedelta = timedelta(),
|
||||
) -> None:
|
||||
"""Setting a waypoint close to the spawn position allows the group to reform gracefully
|
||||
rather than spin
|
||||
@@ -277,7 +306,10 @@ class FlotGenerator:
|
||||
reform_point = dcs_group.position.point_from_heading(
|
||||
forward_heading.degrees, 50
|
||||
)
|
||||
dcs_group.add_waypoint(reform_point)
|
||||
rp = dcs_group.add_waypoint(reform_point)
|
||||
hold = ControlledTask(Hold())
|
||||
hold.stop_after_duration(hold_duration.seconds)
|
||||
rp.add_task(hold)
|
||||
|
||||
def _plan_artillery_action(
|
||||
self,
|
||||
@@ -311,11 +343,10 @@ class FlotGenerator:
|
||||
|
||||
# Artillery will fall back when under attack
|
||||
if stance != CombatStance.RETREAT:
|
||||
|
||||
# Hold position
|
||||
dcs_group.points[1].tasks.append(Hold())
|
||||
retreat = self.find_retreat_point(
|
||||
dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 3)
|
||||
dcs_group, forward_heading, int(RETREAT_DISTANCE / 3)
|
||||
)
|
||||
dcs_group.add_waypoint(
|
||||
dcs_group.position.point_from_heading(forward_heading.degrees, 1),
|
||||
@@ -362,7 +393,10 @@ class FlotGenerator:
|
||||
Handles adding the DCS tasks for tank and IFV groups for all combat stances.
|
||||
Returns True if tasking was added, returns False if the stance was not a combat stance.
|
||||
"""
|
||||
self._set_reform_waypoint(dcs_group, forward_heading)
|
||||
duration = timedelta()
|
||||
if stance != CombatStance.RETREAT:
|
||||
duration = self._earliest_tot_on_flot(not to_cp.coalition.player)
|
||||
self._set_reform_waypoint(dcs_group, forward_heading, duration)
|
||||
if stance == CombatStance.AGGRESSIVE:
|
||||
# Attack nearest enemy if any
|
||||
# Then move forward OR Attack enemy base if it is not too far away
|
||||
@@ -449,7 +483,10 @@ class FlotGenerator:
|
||||
Handles adding the DCS tasks for APC and ATGM groups for all combat stances.
|
||||
Returns True if tasking was added, returns False if the stance was not a combat stance.
|
||||
"""
|
||||
self._set_reform_waypoint(dcs_group, forward_heading)
|
||||
duration = timedelta()
|
||||
if stance != CombatStance.RETREAT:
|
||||
duration = self._earliest_tot_on_flot(not to_cp.coalition.player)
|
||||
self._set_reform_waypoint(dcs_group, forward_heading, duration)
|
||||
if stance in [
|
||||
CombatStance.AGGRESSIVE,
|
||||
CombatStance.BREAKTHROUGH,
|
||||
@@ -483,14 +520,10 @@ class FlotGenerator:
|
||||
from_cp: ControlPoint,
|
||||
to_cp: ControlPoint,
|
||||
) -> None:
|
||||
|
||||
if not self.game.settings.perf_moving_units:
|
||||
return
|
||||
|
||||
for dcs_group, group in ally_groups:
|
||||
if group.unit_type.eplrs_capable:
|
||||
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
|
||||
|
||||
if group.role == CombatGroupRole.ARTILLERY:
|
||||
if self.game.settings.perf_artillery:
|
||||
target = self.get_artillery_target_in_range(
|
||||
@@ -511,7 +544,7 @@ class FlotGenerator:
|
||||
|
||||
if stance == CombatStance.RETREAT:
|
||||
# In retreat mode, the units will fall back
|
||||
# If the ally base is close enough, the units will even regroup there
|
||||
# If the allied base is close enough, the units will even regroup there
|
||||
if (
|
||||
from_cp.position.distance_to_point(dcs_group.points[0].position)
|
||||
<= RETREAT_DISTANCE
|
||||
@@ -529,7 +562,7 @@ class FlotGenerator:
|
||||
self, dcs_group: VehicleGroup, forward_heading: Heading
|
||||
) -> None:
|
||||
"""
|
||||
This add a trigger to manage units fleeing whenever their group is hit hard, or being engaged by CAS
|
||||
This adds a trigger to manage units fleeing whenever their group is hit hard, or being engaged by CAS
|
||||
"""
|
||||
|
||||
if len(dcs_group.units) == 1:
|
||||
@@ -546,7 +579,7 @@ class FlotGenerator:
|
||||
# We add a new retreat waypoint
|
||||
dcs_group.add_waypoint(
|
||||
self.find_retreat_point(
|
||||
dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)
|
||||
dcs_group, forward_heading, int(RETREAT_DISTANCE / 8)
|
||||
),
|
||||
self.wpt_pointaction,
|
||||
)
|
||||
@@ -676,7 +709,7 @@ class FlotGenerator:
|
||||
@staticmethod
|
||||
def get_artilery_group_distance_from_frontline(group: CombatGroup) -> int:
|
||||
"""
|
||||
For artilery group, decide the distance from frontline with the range of the unit
|
||||
For artillery group, decide the distance from frontline with the range of the unit
|
||||
"""
|
||||
rg = group.unit_type.dcs_unit_type.threat_range - 7500
|
||||
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
||||
@@ -769,6 +802,8 @@ class FlotGenerator:
|
||||
heading: Heading,
|
||||
) -> VehicleGroup:
|
||||
cp = self.conflict.front_line.control_point_friendly_to(player)
|
||||
faction = self.game.faction_for(player)
|
||||
|
||||
group = self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_unit_name(side, unit_type),
|
||||
@@ -783,5 +818,6 @@ class FlotGenerator:
|
||||
for c in range(count):
|
||||
vehicle: Vehicle = group.units[c]
|
||||
vehicle.player_can_drive = True
|
||||
GroundForcePainter(faction, vehicle).apply_livery()
|
||||
|
||||
return group
|
||||
|
||||
82
game/missiongenerator/groundforcepainter.py
Normal file
82
game/missiongenerator/groundforcepainter.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from typing import Any, Optional
|
||||
|
||||
from dcs.unit import Ship
|
||||
from dcs.unitgroup import Vehicle
|
||||
|
||||
from game.factions.faction import Faction
|
||||
|
||||
|
||||
class GroundForcePainter:
|
||||
def __init__(self, faction: Faction, vehicle: Vehicle) -> None:
|
||||
self.faction = faction
|
||||
self.vehicle = vehicle
|
||||
|
||||
def livery_from_faction(self) -> Optional[str]:
|
||||
faction = self.faction
|
||||
try:
|
||||
if (
|
||||
choices := faction.liveries_overrides_ground_forces.get(
|
||||
self.vehicle.type
|
||||
)
|
||||
) is not None:
|
||||
return random.choice(choices)
|
||||
except AttributeError:
|
||||
logging.warning(
|
||||
f"Faction {self.faction.name} is missing livery for ground unit {self.vehicle.type}"
|
||||
)
|
||||
return None
|
||||
logging.warning(
|
||||
f"Faction {self.faction.name} is missing livery for ground unit {self.vehicle.type}"
|
||||
)
|
||||
return None
|
||||
|
||||
def determine_livery(self) -> Optional[str]:
|
||||
if (livery := self.livery_from_faction()) is not None:
|
||||
return livery
|
||||
return None
|
||||
|
||||
def apply_livery(self) -> None:
|
||||
livery = self.determine_livery()
|
||||
if livery is None:
|
||||
return
|
||||
self.vehicle.livery_id = livery
|
||||
|
||||
|
||||
class NavalForcePainter:
|
||||
def __init__(self, faction: Faction, vessel: Ship) -> None:
|
||||
self.faction = faction
|
||||
self.vessel = vessel
|
||||
|
||||
def livery_from_faction(self) -> Optional[str]:
|
||||
faction = self.faction
|
||||
try:
|
||||
if (
|
||||
choices := faction.liveries_overrides_ground_forces.get(
|
||||
self.vessel.type
|
||||
)
|
||||
) is not None:
|
||||
return random.choice(choices)
|
||||
except AttributeError:
|
||||
logging.warning(
|
||||
f"Faction {self.faction.name} is missing livery for naval unit {self.vessel.type}"
|
||||
)
|
||||
return None
|
||||
logging.warning(
|
||||
f"Faction {self.faction.name} is missing livery for naval unit {self.vessel.type}"
|
||||
)
|
||||
return None
|
||||
|
||||
def determine_livery(self) -> Optional[str]:
|
||||
if (livery := self.livery_from_faction()) is not None:
|
||||
return livery
|
||||
return None
|
||||
|
||||
def apply_livery(self) -> None:
|
||||
livery = self.determine_livery()
|
||||
if livery is None:
|
||||
return
|
||||
self.vessel.livery_id = livery
|
||||
@@ -43,10 +43,10 @@ from game.radio.radios import RadioFrequency
|
||||
from game.runways import RunwayData
|
||||
from game.theater import TheaterGroundObject, TheaterUnit
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.utils import Distance, UnitSystem, meters, mps, pounds
|
||||
from game.utils import Distance, UnitSystem, meters, mps, pounds, knots, feet
|
||||
from game.weather import Weather
|
||||
from .aircraft.flightdata import FlightData
|
||||
from .airsupportgenerator import AwacsInfo, TankerInfo
|
||||
from .missiondata import AwacsInfo, TankerInfo
|
||||
from .briefinggenerator import CommInfo, JtacInfo, MissionInfoGenerator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -191,7 +191,6 @@ class NumberedWaypoint:
|
||||
|
||||
|
||||
class FlightPlanBuilder:
|
||||
|
||||
WAYPOINT_DESC_MAX_LEN = 25
|
||||
|
||||
def __init__(self, start_time: datetime.datetime, units: UnitSystem) -> None:
|
||||
@@ -336,7 +335,7 @@ class BriefingPage(KneeboardPage):
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
|
||||
if self.flight.custom_name is not None:
|
||||
if self.flight.custom_name:
|
||||
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
|
||||
else:
|
||||
custom_name_title = ""
|
||||
@@ -400,6 +399,22 @@ class BriefingPage(KneeboardPage):
|
||||
f"Temperature: {round(self.weather.atmospheric.temperature_celsius)} °C at sea level"
|
||||
)
|
||||
writer.text(f"QNH: {qnh_in_hg} inHg / {qnh_mm_hg} mmHg / {qnh_hpa} hPa")
|
||||
writer.text(
|
||||
f"Turbulence: {round(self.weather.atmospheric.turbulence_per_10cm)} per 10cm at ground level."
|
||||
)
|
||||
writer.text(
|
||||
f"Wind: {self.weather.wind.at_0m.direction}°"
|
||||
f" / {round(mps(self.weather.wind.at_0m.speed).knots)}kts (0ft)"
|
||||
f" ; {self.weather.wind.at_2000m.direction}°"
|
||||
f" / {round(mps(self.weather.wind.at_2000m.speed).knots)}kts (~6500ft)"
|
||||
f" ; {self.weather.wind.at_8000m.direction}°"
|
||||
f" / {round(mps(self.weather.wind.at_8000m.speed).knots)}kts (~26000ft)"
|
||||
)
|
||||
c = self.weather.clouds
|
||||
writer.text(
|
||||
f'Cloud base: {f"{int(round(meters(c.base).feet, -2))}ft" if c else "CAVOK"}'
|
||||
f'{f", {c.preset.ui_name[:-2]}" if c and c.preset else ""}'
|
||||
)
|
||||
|
||||
fl = self.flight
|
||||
|
||||
@@ -453,7 +468,7 @@ class BriefingPage(KneeboardPage):
|
||||
ils = ""
|
||||
return [
|
||||
row_title,
|
||||
"\n".join(textwrap.wrap(runway.airfield_name, width=24)),
|
||||
"\n".join(textwrap.wrap(runway.airfield_name, width=17)),
|
||||
atc,
|
||||
tacan,
|
||||
ils,
|
||||
@@ -479,6 +494,7 @@ class SupportPage(KneeboardPage):
|
||||
def __init__(
|
||||
self,
|
||||
flight: FlightData,
|
||||
package_flights: List[FlightData],
|
||||
comms: List[CommInfo],
|
||||
awacs: List[AwacsInfo],
|
||||
tankers: List[TankerInfo],
|
||||
@@ -487,6 +503,7 @@ class SupportPage(KneeboardPage):
|
||||
dark_kneeboard: bool,
|
||||
) -> None:
|
||||
self.flight = flight
|
||||
self.package_flights = package_flights
|
||||
self.comms = list(comms)
|
||||
self.awacs = awacs
|
||||
self.tankers = tankers
|
||||
@@ -497,60 +514,92 @@ class SupportPage(KneeboardPage):
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
|
||||
if self.flight.custom_name is not None:
|
||||
if self.flight.custom_name:
|
||||
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
|
||||
else:
|
||||
custom_name_title = ""
|
||||
writer.title(f"{self.flight.callsign} Support Info{custom_name_title}")
|
||||
|
||||
# Package Section
|
||||
package = self.flight.package
|
||||
custom = f' "{package.custom_name}"' if package.custom_name else ""
|
||||
writer.heading(f"{package.package_description} Package{custom}")
|
||||
freq = self.format_frequency(package.frequency).replace("\n", " - ")
|
||||
writer.text(f" FREQ: {freq}", font=writer.table_font)
|
||||
tot = self._format_time(package.time_over_target)
|
||||
writer.text(f" TOT: {tot}", font=writer.table_font)
|
||||
comm_ladder = []
|
||||
for comm in self.comms:
|
||||
comm_ladder.append(
|
||||
[
|
||||
comm.name,
|
||||
"",
|
||||
"",
|
||||
str(len(self.flight.units)),
|
||||
self.format_frequency(comm.freq),
|
||||
]
|
||||
)
|
||||
for f in self.package_flights:
|
||||
comm_ladder.append(
|
||||
[
|
||||
f.callsign,
|
||||
str(f.flight_type),
|
||||
KneeboardPageWriter.wrap_line(str(f.aircraft_type), 23),
|
||||
str(len(f.units)),
|
||||
self.format_frequency(f.intra_flight_channel),
|
||||
]
|
||||
)
|
||||
|
||||
writer.table(comm_ladder, headers=["Callsign", "Task", "Type", "#A/C", "FREQ"])
|
||||
|
||||
# AEW&C
|
||||
writer.heading("AEW&C")
|
||||
aewc_ladder = []
|
||||
|
||||
for single_aewc in self.awacs:
|
||||
|
||||
if single_aewc.depature_location is None:
|
||||
dep = "-"
|
||||
arr = "-"
|
||||
tot = "-"
|
||||
tos = "-"
|
||||
else:
|
||||
dep = self._format_time(single_aewc.start_time)
|
||||
arr = self._format_time(single_aewc.end_time)
|
||||
tot = self._format_time(single_aewc.start_time)
|
||||
tos = self._format_duration(
|
||||
single_aewc.end_time - single_aewc.start_time
|
||||
)
|
||||
|
||||
aewc_ladder.append(
|
||||
[
|
||||
str(single_aewc.callsign),
|
||||
self.format_frequency(single_aewc.freq),
|
||||
str(single_aewc.depature_location),
|
||||
str(dep),
|
||||
str(arr),
|
||||
"TOT: " + tot + "\n" + "TOS: " + tos,
|
||||
]
|
||||
)
|
||||
|
||||
writer.table(
|
||||
aewc_ladder,
|
||||
headers=["Callsign", "FREQ", "Depature", "ETD", "ETA"],
|
||||
headers=["Callsign", "FREQ", "Departure", "TOT / TOS"],
|
||||
)
|
||||
|
||||
# Package Section
|
||||
writer.heading("Comm ladder")
|
||||
comm_ladder = []
|
||||
for comm in self.comms:
|
||||
comm_ladder.append(
|
||||
[comm.name, "", "", "", self.format_frequency(comm.freq)]
|
||||
)
|
||||
|
||||
writer.heading("Tankers:")
|
||||
for tanker in self.tankers:
|
||||
tot = self._format_time(tanker.start_time)
|
||||
tos = self._format_duration(tanker.end_time - tanker.start_time)
|
||||
comm_ladder.append(
|
||||
[
|
||||
tanker.callsign,
|
||||
"Tanker",
|
||||
tanker.variant,
|
||||
KneeboardPageWriter.wrap_line(tanker.variant, 21),
|
||||
str(tanker.tacan),
|
||||
self.format_frequency(tanker.freq),
|
||||
"TOT: " + tot + "\n" + "TOS: " + tos,
|
||||
]
|
||||
)
|
||||
|
||||
writer.table(comm_ladder, headers=["Callsign", "Task", "Type", "TACAN", "FREQ"])
|
||||
writer.table(
|
||||
comm_ladder,
|
||||
headers=["Callsign", "Task", "Type", "TACAN", "FREQ", "TOT / TOS"],
|
||||
)
|
||||
|
||||
writer.heading("JTAC")
|
||||
jtacs = []
|
||||
@@ -570,7 +619,9 @@ class SupportPage(KneeboardPage):
|
||||
|
||||
writer.write(path)
|
||||
|
||||
def format_frequency(self, frequency: RadioFrequency) -> str:
|
||||
def format_frequency(self, frequency: Optional[RadioFrequency]) -> str:
|
||||
if frequency is None:
|
||||
return ""
|
||||
channel = self.flight.channel_for(frequency)
|
||||
if channel is None:
|
||||
return str(frequency)
|
||||
@@ -586,6 +637,13 @@ class SupportPage(KneeboardPage):
|
||||
local_time = self.start_time + time
|
||||
return f"{local_time.strftime('%H:%M:%S')}{'Z' if local_time.tzinfo is not None else ''}"
|
||||
|
||||
@staticmethod
|
||||
def _format_duration(time: Optional[datetime.timedelta]) -> str:
|
||||
if time is None:
|
||||
return ""
|
||||
time -= datetime.timedelta(microseconds=time.microseconds)
|
||||
return f"{time}"
|
||||
|
||||
|
||||
class SeadTaskPage(KneeboardPage):
|
||||
"""A kneeboard page containing SEAD/DEAD target information."""
|
||||
@@ -608,7 +666,7 @@ class SeadTaskPage(KneeboardPage):
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
|
||||
if self.flight.custom_name is not None:
|
||||
if self.flight.custom_name:
|
||||
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
|
||||
else:
|
||||
custom_name_title = ""
|
||||
@@ -636,6 +694,8 @@ class SeadTaskPage(KneeboardPage):
|
||||
class StrikeTaskPage(KneeboardPage):
|
||||
"""A kneeboard page containing strike target information."""
|
||||
|
||||
WAYPOINT_DESC_MAX_LEN = 35
|
||||
|
||||
def __init__(self, flight: FlightData, dark_kneeboard: bool) -> None:
|
||||
self.flight = flight
|
||||
self.dark_kneeboard = dark_kneeboard
|
||||
@@ -648,24 +708,28 @@ class StrikeTaskPage(KneeboardPage):
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
|
||||
if self.flight.custom_name is not None:
|
||||
if self.flight.custom_name:
|
||||
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
|
||||
else:
|
||||
custom_name_title = ""
|
||||
writer.title(f"{self.flight.callsign} Strike Task Info{custom_name_title}")
|
||||
|
||||
writer.table(
|
||||
[self.target_info_row(t) for t in self.targets],
|
||||
headers=["Steerpoint", "Description", "Location"],
|
||||
[self.target_info_row(t, writer) for t in self.targets],
|
||||
headers=["STPT", "Description", "Location"],
|
||||
)
|
||||
|
||||
writer.write(path)
|
||||
|
||||
@staticmethod
|
||||
def target_info_row(target: NumberedWaypoint) -> list[str]:
|
||||
def target_info_row(
|
||||
target: NumberedWaypoint, writer: KneeboardPageWriter
|
||||
) -> list[str]:
|
||||
return [
|
||||
str(target.number),
|
||||
target.waypoint.pretty_name,
|
||||
writer.wrap_line(
|
||||
target.waypoint.pretty_name, StrikeTaskPage.WAYPOINT_DESC_MAX_LEN
|
||||
),
|
||||
target.waypoint.position.latlng().format_dms(include_decimal_seconds=True),
|
||||
]
|
||||
|
||||
@@ -724,8 +788,13 @@ class KneeboardGenerator(MissionInfoGenerator):
|
||||
for flight in self.flights:
|
||||
if not flight.client_units:
|
||||
continue
|
||||
package_flights = [
|
||||
f
|
||||
for f in self.flights
|
||||
if f.package is flight.package and f is not flight
|
||||
]
|
||||
all_flights[flight.aircraft_type].extend(
|
||||
self.generate_flight_kneeboard(flight)
|
||||
self.generate_flight_kneeboard(flight, package_flights)
|
||||
)
|
||||
return all_flights
|
||||
|
||||
@@ -736,7 +805,9 @@ class KneeboardGenerator(MissionInfoGenerator):
|
||||
return StrikeTaskPage(flight, self.dark_kneeboard)
|
||||
return None
|
||||
|
||||
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
|
||||
def generate_flight_kneeboard(
|
||||
self, flight: FlightData, package_flights: List[FlightData]
|
||||
) -> List[KneeboardPage]:
|
||||
"""Returns a list of kneeboard pages for the given flight."""
|
||||
|
||||
if flight.aircraft_type.utc_kneeboard:
|
||||
@@ -756,6 +827,7 @@ class KneeboardGenerator(MissionInfoGenerator):
|
||||
),
|
||||
SupportPage(
|
||||
flight,
|
||||
package_flights,
|
||||
self.comms,
|
||||
self.awacs,
|
||||
self.tankers,
|
||||
|
||||
@@ -46,7 +46,7 @@ class LogisticsGenerator:
|
||||
target_zone = f"{self.group.name}TARGET_ZONE"
|
||||
self.mission.triggers.add_triggerzone(
|
||||
self.flight.flight_plan.layout.target.position,
|
||||
self.flight.flight_plan.engagement_distance.meters,
|
||||
self.flight.flight_plan.ctld_target_zone_radius.meters,
|
||||
False,
|
||||
target_zone,
|
||||
)
|
||||
@@ -57,8 +57,8 @@ class LogisticsGenerator:
|
||||
if (
|
||||
waypoint.waypoint_type
|
||||
not in [
|
||||
FlightWaypointType.PICKUP,
|
||||
FlightWaypointType.DROP_OFF,
|
||||
FlightWaypointType.PICKUP_ZONE,
|
||||
FlightWaypointType.DROPOFF_ZONE,
|
||||
]
|
||||
or waypoint.only_for_player
|
||||
and not self.flight.client_count
|
||||
@@ -69,7 +69,7 @@ class LogisticsGenerator:
|
||||
self.mission.triggers.add_triggerzone(
|
||||
waypoint.position, ZONE_RADIUS, False, zone_name
|
||||
)
|
||||
if waypoint.waypoint_type == FlightWaypointType.PICKUP:
|
||||
if waypoint.waypoint_type == FlightWaypointType.PICKUP_ZONE:
|
||||
pickup_point = waypoint.position
|
||||
logistics_info.pickup_zone = zone_name
|
||||
else:
|
||||
|
||||
@@ -40,7 +40,7 @@ class LuaGenerator:
|
||||
self.inject_plugins()
|
||||
|
||||
def generate_plugin_data(self) -> None:
|
||||
lua_data = LuaData("dcsLiberation")
|
||||
lua_data = LuaData("dcsRetribution")
|
||||
|
||||
install_path = lua_data.add_item("installPath")
|
||||
install_path.set_value(os.path.abspath("."))
|
||||
@@ -201,7 +201,7 @@ class LuaGenerator:
|
||||
for role, connections in node.connections.items():
|
||||
iads_element.add_data_array(role, connections)
|
||||
|
||||
trigger = TriggerStart(comment="Set DCS Liberation data")
|
||||
trigger = TriggerStart(comment="Set DCS Retribution data")
|
||||
trigger.add_action(DoScript(String(lua_data.create_operations_lua())))
|
||||
self.mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
@@ -380,7 +380,7 @@ class LuaData(LuaItem):
|
||||
"""crates the liberation lua script for the dcs mission"""
|
||||
lua_prefix = """
|
||||
-- setting configuration table
|
||||
env.info("DCSLiberation|: setting configuration table")
|
||||
env.info("DCSRetribution|: setting configuration table")
|
||||
"""
|
||||
|
||||
return lua_prefix + self.serialize()
|
||||
|
||||
@@ -11,6 +11,8 @@ from game.runways import RunwayData
|
||||
if TYPE_CHECKING:
|
||||
from game.radio.radios import RadioFrequency
|
||||
from game.radio.tacan import TacanChannel
|
||||
from game.utils import Distance
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -31,8 +33,8 @@ class AwacsInfo(GroupInfo):
|
||||
"""AWACS information for the kneeboard."""
|
||||
|
||||
depature_location: Optional[str]
|
||||
start_time: Optional[timedelta]
|
||||
end_time: Optional[timedelta]
|
||||
start_time: timedelta
|
||||
end_time: timedelta
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -41,8 +43,8 @@ class TankerInfo(GroupInfo):
|
||||
|
||||
variant: str
|
||||
tacan: TacanChannel
|
||||
start_time: Optional[timedelta]
|
||||
end_time: Optional[timedelta]
|
||||
start_time: timedelta
|
||||
end_time: timedelta
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -94,3 +96,4 @@ class MissionData:
|
||||
tankers: list[TankerInfo] = field(default_factory=list)
|
||||
jtacs: list[JtacInfo] = field(default_factory=list)
|
||||
logistics: list[LogisticsInfo] = field(default_factory=list)
|
||||
cp_stack: dict[UUID, Distance] = field(default_factory=dict)
|
||||
|
||||
@@ -17,13 +17,11 @@ from game.missiongenerator.aircraft.aircraftgenerator import (
|
||||
AircraftGenerator,
|
||||
)
|
||||
from game.naming import namegen
|
||||
from game.radio.radios import RadioFrequency, RadioRegistry
|
||||
from game.radio.radios import RadioFrequency, RadioRegistry, MHz
|
||||
from game.radio.tacan import TacanRegistry
|
||||
from game.theater import Airfield
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.unitmap import UnitMap
|
||||
from .airconflictdescription import AirConflictDescription
|
||||
from .airsupportgenerator import AirSupportGenerator
|
||||
from .briefinggenerator import BriefingGenerator, MissionInfoGenerator
|
||||
from .cargoshipgenerator import CargoShipGenerator
|
||||
from .convoygenerator import ConvoyGenerator
|
||||
@@ -39,14 +37,12 @@ from .missiondata import MissionData
|
||||
from .tgogenerator import TgoGenerator
|
||||
from .triggergenerator import TriggerGenerator
|
||||
from .visualsgenerator import VisualsGenerator
|
||||
from ..radio.TacanContainer import TacanContainer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
COMBINED_ARMS_SLOTS = 1
|
||||
|
||||
|
||||
def country_id_from_name(name: str) -> int:
|
||||
for k, v in country_dict.items():
|
||||
if v.name == name:
|
||||
@@ -70,7 +66,11 @@ class MissionGenerator:
|
||||
self.generation_started = False
|
||||
|
||||
with open("resources/default_options.lua", "r", encoding="utf-8") as f:
|
||||
self.mission.options.load_from_dict(dcs.lua.loads(f.read())["options"])
|
||||
options = dcs.lua.loads(f.read())["options"]
|
||||
ext_view = game.settings.external_views_allowed
|
||||
options["miscellaneous"]["f11_free_camera"] = ext_view
|
||||
options["difficulty"]["spectatorExternalViews"] = ext_view
|
||||
self.mission.options.load_from_dict(options)
|
||||
|
||||
def generate_miz(self, output: Path) -> UnitMap:
|
||||
if self.generation_started:
|
||||
@@ -159,6 +159,9 @@ class MissionGenerator:
|
||||
unique_map_frequencies: set[RadioFrequency] = set()
|
||||
self.initialize_tacan_registry(unique_map_frequencies)
|
||||
self.initialize_radio_registry(unique_map_frequencies)
|
||||
# Allocate UHF/VHF Guard Freq first!
|
||||
unique_map_frequencies.add(MHz(243))
|
||||
unique_map_frequencies.add(MHz(121, 500))
|
||||
for frequency in unique_map_frequencies:
|
||||
self.radio_registry.reserve(frequency)
|
||||
|
||||
@@ -176,6 +179,9 @@ class MissionGenerator:
|
||||
logging.warning(f"TACAN beacon has no channel: {beacon.callsign}")
|
||||
else:
|
||||
self.tacan_registry.mark_unavailable(beacon.tacan_channel)
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if isinstance(cp, TacanContainer) and cp.tacan is not None:
|
||||
self.tacan_registry.mark_unavailable(cp.tacan)
|
||||
|
||||
def initialize_radio_registry(
|
||||
self, unique_map_frequencies: set[RadioFrequency]
|
||||
@@ -220,17 +226,6 @@ class MissionGenerator:
|
||||
def generate_air_units(self, tgo_generator: TgoGenerator) -> None:
|
||||
"""Generate the air units for the Operation"""
|
||||
|
||||
# Air Support (Tanker & Awacs)
|
||||
air_support_generator = AirSupportGenerator(
|
||||
self.mission,
|
||||
AirConflictDescription.for_theater(self.game.theater),
|
||||
self.game,
|
||||
self.radio_registry,
|
||||
self.tacan_registry,
|
||||
self.mission_data,
|
||||
)
|
||||
air_support_generator.generate()
|
||||
|
||||
# Generate Aircraft Activity on the map
|
||||
aircraft_generator = AircraftGenerator(
|
||||
self.mission,
|
||||
@@ -241,7 +236,7 @@ class MissionGenerator:
|
||||
self.tacan_registry,
|
||||
self.laser_code_registry,
|
||||
self.unit_map,
|
||||
mission_data=air_support_generator.mission_data,
|
||||
mission_data=self.mission_data,
|
||||
helipads=tgo_generator.helipads,
|
||||
)
|
||||
|
||||
@@ -257,19 +252,18 @@ class MissionGenerator:
|
||||
self.game.red.ato,
|
||||
tgo_generator.runways,
|
||||
)
|
||||
aircraft_generator.spawn_unused_aircraft(
|
||||
self.mission.country(self.game.blue.country_name),
|
||||
self.mission.country(self.game.red.country_name),
|
||||
)
|
||||
if not self.game.settings.perf_disable_idle_aircraft:
|
||||
aircraft_generator.spawn_unused_aircraft(
|
||||
self.mission.country(self.game.blue.country_name),
|
||||
self.mission.country(self.game.red.country_name),
|
||||
)
|
||||
|
||||
self.mission_data.flights = aircraft_generator.flights
|
||||
|
||||
for flight in aircraft_generator.flights:
|
||||
if not flight.client_units:
|
||||
continue
|
||||
flight.aircraft_type.assign_channels_for_flight(
|
||||
flight, air_support_generator.mission_data
|
||||
)
|
||||
|
||||
self.mission_data.flights = aircraft_generator.flights
|
||||
flight.aircraft_type.assign_channels_for_flight(flight, self.mission_data)
|
||||
|
||||
def generate_destroyed_units(self) -> None:
|
||||
"""Add destroyed units to the Mission"""
|
||||
@@ -330,6 +324,11 @@ class MissionGenerator:
|
||||
gen.generate()
|
||||
|
||||
def setup_combined_arms(self) -> None:
|
||||
self.mission.groundControl.pilot_can_control_vehicles = COMBINED_ARMS_SLOTS > 0
|
||||
self.mission.groundControl.blue_tactical_commander = COMBINED_ARMS_SLOTS
|
||||
self.mission.groundControl.blue_observer = 1
|
||||
settings = self.game.settings
|
||||
commanders = settings.tactical_commander_count
|
||||
self.mission.groundControl.pilot_can_control_vehicles = commanders > 0
|
||||
|
||||
self.mission.groundControl.blue_game_masters = settings.game_masters_count
|
||||
self.mission.groundControl.blue_tactical_commander = commanders
|
||||
self.mission.groundControl.blue_jtac = settings.jtac_count
|
||||
self.mission.groundControl.blue_observer = settings.observer_count
|
||||
|
||||
@@ -22,6 +22,8 @@ from dcs.ships import (
|
||||
CVN_73,
|
||||
CVN_75,
|
||||
Stennis,
|
||||
Forrestal,
|
||||
LHA_Tarawa,
|
||||
)
|
||||
from dcs.statics import Fortification
|
||||
from dcs.task import (
|
||||
@@ -35,16 +37,26 @@ from dcs.task import (
|
||||
)
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import Event, TriggerOnce, TriggerStart, TriggerZone
|
||||
from dcs.unit import Unit, InvisibleFARP
|
||||
from dcs.unit import Unit, InvisibleFARP, BaseFARP
|
||||
from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup
|
||||
from dcs.unittype import ShipType, VehicleType
|
||||
from dcs.vehicles import vehicle_map
|
||||
|
||||
from game.missiongenerator.groundforcepainter import (
|
||||
NavalForcePainter,
|
||||
GroundForcePainter,
|
||||
)
|
||||
from game.missiongenerator.missiondata import CarrierInfo, MissionData
|
||||
from game.radio.RadioFrequencyContainer import RadioFrequencyContainer
|
||||
from game.radio.radios import RadioFrequency, RadioRegistry
|
||||
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
|
||||
from game.runways import RunwayData
|
||||
from game.theater import ControlPoint, TheaterGroundObject, TheaterUnit
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
TheaterGroundObject,
|
||||
TheaterUnit,
|
||||
NavalControlPoint,
|
||||
)
|
||||
from game.theater.theatergroundobject import (
|
||||
CarrierGroundObject,
|
||||
GenericCarrierGroundObject,
|
||||
@@ -122,6 +134,7 @@ class GroundObjectGenerator:
|
||||
vehicle_group: Optional[VehicleGroup] = None
|
||||
for unit in units:
|
||||
assert issubclass(unit.type, VehicleType)
|
||||
faction = unit.ground_object.control_point.coalition.faction
|
||||
if vehicle_group is None:
|
||||
vehicle_group = self.m.vehicle_group(
|
||||
self.country,
|
||||
@@ -134,11 +147,13 @@ class GroundObjectGenerator:
|
||||
self.enable_eplrs(vehicle_group, unit.type)
|
||||
vehicle_group.units[0].name = unit.unit_name
|
||||
self.set_alarm_state(vehicle_group)
|
||||
GroundForcePainter(faction, vehicle_group.units[0]).apply_livery()
|
||||
else:
|
||||
vehicle_unit = self.m.vehicle(unit.unit_name, unit.type)
|
||||
vehicle_unit.player_can_drive = True
|
||||
vehicle_unit.position = unit.position
|
||||
vehicle_unit.heading = unit.position.heading.degrees
|
||||
GroundForcePainter(faction, vehicle_unit).apply_livery()
|
||||
vehicle_group.add_unit(vehicle_unit)
|
||||
self._register_theater_unit(unit, vehicle_group.units[-1])
|
||||
if vehicle_group is None:
|
||||
@@ -154,6 +169,7 @@ class GroundObjectGenerator:
|
||||
ship_group: Optional[ShipGroup] = None
|
||||
for unit in units:
|
||||
assert issubclass(unit.type, ShipType)
|
||||
faction = unit.ground_object.control_point.coalition.faction
|
||||
if ship_group is None:
|
||||
ship_group = self.m.ship_group(
|
||||
self.country,
|
||||
@@ -166,12 +182,14 @@ class GroundObjectGenerator:
|
||||
ship_group.set_frequency(frequency.hertz)
|
||||
ship_group.units[0].name = unit.unit_name
|
||||
self.set_alarm_state(ship_group)
|
||||
NavalForcePainter(faction, ship_group.units[0]).apply_livery()
|
||||
else:
|
||||
ship_unit = self.m.ship(unit.unit_name, unit.type)
|
||||
if frequency:
|
||||
ship_unit.set_frequency(frequency.hertz)
|
||||
ship_unit.position = unit.position
|
||||
ship_unit.heading = unit.position.heading.degrees
|
||||
NavalForcePainter(faction, ship_unit).apply_livery()
|
||||
ship_group.add_unit(ship_unit)
|
||||
self._register_theater_unit(unit, ship_group.units[-1])
|
||||
if ship_group is None:
|
||||
@@ -341,7 +359,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
def __init__(
|
||||
self,
|
||||
ground_object: GenericCarrierGroundObject,
|
||||
control_point: ControlPoint,
|
||||
control_point: NavalControlPoint,
|
||||
country: Country,
|
||||
game: Game,
|
||||
mission: Mission,
|
||||
@@ -362,9 +380,12 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
self.mission_data = mission_data
|
||||
|
||||
def generate(self) -> None:
|
||||
|
||||
# This can also be refactored as the general generation was updated
|
||||
atc = self.radio_registry.alloc_uhf()
|
||||
if self.control_point.frequency is not None:
|
||||
atc = self.control_point.frequency
|
||||
if atc not in self.radio_registry.allocated_channels:
|
||||
self.radio_registry.reserve(atc)
|
||||
else:
|
||||
atc = self.radio_registry.alloc_uhf()
|
||||
|
||||
for g_id, group in enumerate(self.ground_object.groups):
|
||||
if not group.units:
|
||||
@@ -399,15 +420,33 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
f"Error generating carrier group for {self.control_point.name}"
|
||||
)
|
||||
ship_group.units[0].type = carrier_type.id
|
||||
tacan = self.tacan_registry.alloc_for_band(
|
||||
TacanBand.X, TacanUsage.TransmitReceive
|
||||
)
|
||||
tacan_callsign = self.tacan_callsign()
|
||||
icls = next(self.icls_alloc)
|
||||
if self.control_point.tacan is None:
|
||||
tacan = self.tacan_registry.alloc_for_band(
|
||||
TacanBand.X, TacanUsage.TransmitReceive
|
||||
)
|
||||
else:
|
||||
tacan = self.control_point.tacan
|
||||
if self.control_point.tcn_name is None:
|
||||
tacan_callsign = self.tacan_callsign()
|
||||
else:
|
||||
tacan_callsign = self.control_point.tcn_name
|
||||
link4 = None
|
||||
if carrier_type in [Stennis, CVN_71, CVN_72, CVN_73, CVN_75]:
|
||||
link4 = self.radio_registry.alloc_uhf()
|
||||
self.activate_beacons(ship_group, tacan, tacan_callsign, icls, link4)
|
||||
link4carriers = [Stennis, CVN_71, CVN_72, CVN_73, CVN_75, Forrestal]
|
||||
if carrier_type in link4carriers:
|
||||
if self.control_point.link4 is None:
|
||||
link4 = self.radio_registry.alloc_uhf()
|
||||
else:
|
||||
link4 = self.control_point.link4
|
||||
icls = None
|
||||
icls_name = self.control_point.icls_name
|
||||
if carrier_type in link4carriers or carrier_type == LHA_Tarawa:
|
||||
if self.control_point.icls_channel is None:
|
||||
icls = next(self.icls_alloc)
|
||||
else:
|
||||
icls = self.control_point.icls_channel
|
||||
self.activate_beacons(
|
||||
ship_group, tacan, tacan_callsign, icls, icls_name, link4
|
||||
)
|
||||
self.add_runway_data(
|
||||
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
|
||||
)
|
||||
@@ -451,7 +490,8 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
group: ShipGroup,
|
||||
tacan: TacanChannel,
|
||||
callsign: str,
|
||||
icls: int,
|
||||
icls: Optional[int] = None,
|
||||
icls_name: Optional[str] = None,
|
||||
link4: Optional[RadioFrequency] = None,
|
||||
) -> None:
|
||||
group.points[0].tasks.append(
|
||||
@@ -463,12 +503,14 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
aa=False,
|
||||
)
|
||||
)
|
||||
group.points[0].tasks.append(
|
||||
ActivateICLSCommand(icls, unit_id=group.units[0].id)
|
||||
)
|
||||
if icls is not None:
|
||||
icls_name = "" if icls_name is None else icls_name
|
||||
group.points[0].tasks.append(
|
||||
ActivateICLSCommand(icls, group.units[0].id, icls_name)
|
||||
)
|
||||
if link4 is not None:
|
||||
group.points[0].tasks.append(
|
||||
ActivateLink4Command(int(link4.mhz), group.units[0].id)
|
||||
ActivateLink4Command(link4.hertz, group.units[0].id)
|
||||
)
|
||||
group.points[0].tasks.append(ActivateACLSCommand(unit_id=group.units[0].id))
|
||||
|
||||
@@ -478,9 +520,8 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
atc: RadioFrequency,
|
||||
tacan: TacanChannel,
|
||||
callsign: str,
|
||||
icls: int,
|
||||
icls: Optional[int],
|
||||
) -> None:
|
||||
# TODO: Make unit name usable.
|
||||
# This relies on one control point mapping exactly
|
||||
# to one LHA, carrier, or other usable "runway".
|
||||
# This isn't wholly true, since the DD escorts of
|
||||
@@ -488,10 +529,10 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
# they aren't exposed as such to the game. Should
|
||||
# clean this up so that's possible. We can't use the
|
||||
# unit name since it's an arbitrary ID.
|
||||
self.runways[self.control_point.name] = RunwayData(
|
||||
self.runways[self.control_point.full_name] = RunwayData(
|
||||
self.control_point.name,
|
||||
brc,
|
||||
"N/A",
|
||||
f"{brc.degrees:03}",
|
||||
atc=atc,
|
||||
tacan=tacan,
|
||||
tacan_callsign=callsign,
|
||||
@@ -580,6 +621,13 @@ class HelipadGenerator:
|
||||
self.helipads.add_unit(
|
||||
InvisibleFARP(self.m.terrain, self.m.next_unit_id(), name_i)
|
||||
)
|
||||
|
||||
# Set FREQ
|
||||
if isinstance(self.cp, RadioFrequencyContainer) and self.cp.frequency:
|
||||
for hp in self.helipads.units:
|
||||
if isinstance(hp, BaseFARP):
|
||||
hp.heliport_frequency = self.cp.frequency.mhz
|
||||
|
||||
pad = self.helipads.units[-1]
|
||||
pad.position = helipad
|
||||
pad.heading = heading
|
||||
@@ -651,7 +699,9 @@ class TgoGenerator:
|
||||
|
||||
for ground_object in cp.ground_objects:
|
||||
generator: GroundObjectGenerator
|
||||
if isinstance(ground_object, CarrierGroundObject):
|
||||
if isinstance(ground_object, CarrierGroundObject) and isinstance(
|
||||
cp, NavalControlPoint
|
||||
):
|
||||
generator = CarrierGenerator(
|
||||
ground_object,
|
||||
cp,
|
||||
@@ -665,7 +715,9 @@ class TgoGenerator:
|
||||
self.unit_map,
|
||||
self.mission_data,
|
||||
)
|
||||
elif isinstance(ground_object, LhaGroundObject):
|
||||
elif isinstance(ground_object, LhaGroundObject) and isinstance(
|
||||
cp, NavalControlPoint
|
||||
):
|
||||
generator = LhaGenerator(
|
||||
ground_object,
|
||||
cp,
|
||||
|
||||
@@ -44,7 +44,7 @@ class Silence(Option):
|
||||
|
||||
|
||||
class TriggerGenerator:
|
||||
capture_zone_types = (Fob,)
|
||||
capture_zone_types = (Fob, Airfield)
|
||||
capture_zone_flag = 600
|
||||
|
||||
def __init__(self, mission: Mission, game: Game) -> None:
|
||||
@@ -159,7 +159,9 @@ class TriggerGenerator:
|
||||
flag = self.get_capture_zone_flag()
|
||||
capture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
|
||||
capture_trigger.add_condition(
|
||||
AllOfCoalitionOutsideZone(defending_coalition, trigger_zone.id)
|
||||
AllOfCoalitionOutsideZone(
|
||||
defending_coalition, trigger_zone.id, unit_type="GROUND"
|
||||
)
|
||||
)
|
||||
capture_trigger.add_condition(
|
||||
PartOfCoalitionInZone(
|
||||
@@ -176,7 +178,9 @@ class TriggerGenerator:
|
||||
|
||||
recapture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
|
||||
recapture_trigger.add_condition(
|
||||
AllOfCoalitionOutsideZone(attacking_coalition, trigger_zone.id)
|
||||
AllOfCoalitionOutsideZone(
|
||||
attacking_coalition, trigger_zone.id, unit_type="GROUND"
|
||||
)
|
||||
)
|
||||
recapture_trigger.add_condition(
|
||||
PartOfCoalitionInZone(
|
||||
|
||||
@@ -2,8 +2,9 @@ from typing import Type
|
||||
|
||||
from dcs.helicopters import HelicopterType, helicopter_map
|
||||
from dcs.planes import PlaneType, plane_map
|
||||
from dcs.unittype import VehicleType
|
||||
from dcs.unittype import VehicleType, ShipType
|
||||
from dcs.vehicles import vehicle_map
|
||||
from dcs.ships import ship_map
|
||||
|
||||
|
||||
def helicoptermod(helicopter: Type[HelicopterType]) -> Type[HelicopterType]:
|
||||
@@ -19,3 +20,8 @@ def planemod(plane: Type[PlaneType]) -> Type[PlaneType]:
|
||||
def vehiclemod(vehicle: Type[VehicleType]) -> Type[VehicleType]:
|
||||
vehicle_map[vehicle.id] = vehicle
|
||||
return vehicle
|
||||
|
||||
|
||||
def shipmod(ship: Type[ShipType]) -> Type[ShipType]:
|
||||
ship_map[ship.id] = ship
|
||||
return ship
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user