mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Compare commits
136 Commits
f16i-mod-s
...
hotfix/1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a32d13201 | ||
|
|
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 |
6
.github/ISSUE_TEMPLATE/campaign_update.md
vendored
6
.github/ISSUE_TEMPLATE/campaign_update.md
vendored
@@ -8,13 +8,13 @@ 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"]
|
||||
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
|
||||
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.
|
||||
2
.github/ISSUE_TEMPLATE/mod_support.md
vendored
2
.github/ISSUE_TEMPLATE/mod_support.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
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
|
||||
assignees: ''
|
||||
|
||||
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -45,8 +45,7 @@ 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
|
||||
|
||||
@@ -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
|
||||
|
||||
92
changelog.md
92
changelog.md
@@ -1,78 +1,122 @@
|
||||
# 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
|
||||
|
||||
## Fixes
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
36
client/package-lock.json
generated
36
client/package-lock.json
generated
@@ -6424,9 +6424,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-loader/node_modules/loader-utils": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
|
||||
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
|
||||
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"big.js": "^5.2.2",
|
||||
@@ -13959,9 +13959,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/loader-utils": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
|
||||
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
|
||||
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"big.js": "^5.2.2",
|
||||
@@ -17234,9 +17234,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-dev-utils/node_modules/loader-utils": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz",
|
||||
"integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
|
||||
"integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 12.13.0"
|
||||
@@ -25872,9 +25872,9 @@
|
||||
}
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
|
||||
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
|
||||
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
@@ -31521,9 +31521,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
|
||||
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
|
||||
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
@@ -33869,9 +33869,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz",
|
||||
"integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
|
||||
"integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -126,6 +130,14 @@ from pydcs_extensions.f100.f100 import VSN_F100
|
||||
from pydcs_extensions.f104.f104 import VSN_F104C, VSN_F104G, VSN_F104S, VSN_F104S_AG
|
||||
from pydcs_extensions.f105.f105 import VSN_F105D, VSN_F105G
|
||||
from pydcs_extensions.f15d.f15d import F_15D
|
||||
from pydcs_extensions.f16i_idf.f16i_idf import (
|
||||
F_16D_50,
|
||||
F_16D_52,
|
||||
F_16D_50_NS,
|
||||
F_16D_52_NS,
|
||||
F_16D_Barak,
|
||||
F_16I,
|
||||
)
|
||||
from pydcs_extensions.f22a.f22a import F_22A
|
||||
from pydcs_extensions.f4b.f4b import VSN_F4B
|
||||
from pydcs_extensions.f84g.f84g import VSN_F84G
|
||||
@@ -133,6 +145,7 @@ from pydcs_extensions.hercules.hercules import Hercules
|
||||
from pydcs_extensions.jas39.jas39 import JAS39Gripen, 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
|
||||
|
||||
@@ -143,13 +156,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,
|
||||
@@ -160,9 +174,16 @@ CAP_CAPABLE = [
|
||||
Su_27,
|
||||
MiG_29S,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak,
|
||||
F_16D_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
FA_18C_hornet,
|
||||
JF_17,
|
||||
JAS39Gripen,
|
||||
F_16A_MLU,
|
||||
F_16A,
|
||||
F_4E,
|
||||
VSN_F4B,
|
||||
@@ -194,7 +215,6 @@ CAP_CAPABLE = [
|
||||
F_86F_Sabre,
|
||||
MiG_15bis,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
VSN_F84G,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
@@ -210,6 +230,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 = [
|
||||
@@ -222,8 +247,13 @@ CAS_CAPABLE = [
|
||||
Su_25,
|
||||
F_15D,
|
||||
F_15E,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak,
|
||||
F_16D_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
FA_18C_hornet,
|
||||
Tornado_GR4,
|
||||
Tornado_IDS,
|
||||
@@ -231,6 +261,8 @@ CAS_CAPABLE = [
|
||||
JF_17,
|
||||
AV8BNA,
|
||||
A_10A,
|
||||
F_16A_MLU,
|
||||
F_16A,
|
||||
B_1B,
|
||||
A_4E_C,
|
||||
F_14B,
|
||||
@@ -258,6 +290,7 @@ CAS_CAPABLE = [
|
||||
SA342M,
|
||||
SA342L,
|
||||
Ka_50,
|
||||
Ka_50_3,
|
||||
Mi_28N,
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
@@ -278,6 +311,8 @@ CAS_CAPABLE = [
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
MB_339A,
|
||||
Bronco_OV_10A,
|
||||
UH_1H,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
@@ -309,17 +344,21 @@ CAS_CAPABLE = [
|
||||
|
||||
|
||||
# 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_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
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,
|
||||
@@ -333,15 +372,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,
|
||||
@@ -355,6 +400,7 @@ DEAD_CAPABLE = SEAD_CAPABLE + [
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
Bronco_OV_10A,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
MosquitoFBMkVI,
|
||||
@@ -385,9 +431,16 @@ STRIKE_CAPABLE = [
|
||||
AJS37,
|
||||
Tornado_GR4,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak,
|
||||
F_16D_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
FA_18C_hornet,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
F_16A_MLU,
|
||||
F_16A,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
@@ -414,6 +467,7 @@ STRIKE_CAPABLE = [
|
||||
A_10C,
|
||||
S_3B,
|
||||
A_4E_C,
|
||||
Bronco_OV_10A,
|
||||
M_2000C,
|
||||
Mirage_F1B,
|
||||
Mirage_F1BE,
|
||||
@@ -430,6 +484,7 @@ STRIKE_CAPABLE = [
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
MB_339A,
|
||||
B_17G,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
@@ -461,9 +516,10 @@ ANTISHIP_CAPABLE = [
|
||||
Tu_142,
|
||||
Tu_22M3,
|
||||
H_6J,
|
||||
F_16A,
|
||||
FA_18C_hornet,
|
||||
JAS39Gripen_AG,
|
||||
F_16A_MLU,
|
||||
F_16A,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
JF_17,
|
||||
@@ -492,6 +548,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,
|
||||
@@ -500,9 +564,16 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
F_15E,
|
||||
AJS37,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak,
|
||||
F_16D_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
FA_18C_hornet,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
F_16A_MLU,
|
||||
F_16A,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
@@ -529,15 +600,8 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
A_10C,
|
||||
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,
|
||||
@@ -545,6 +609,7 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
MB_339A,
|
||||
B_17G,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
@@ -643,7 +708,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:
|
||||
@@ -653,7 +718,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:
|
||||
|
||||
@@ -63,6 +63,7 @@ class Flight(SidcDescribable):
|
||||
self.start_type = start_type
|
||||
self.use_custom_loadout = False
|
||||
self.custom_name = custom_name
|
||||
self.group_id: int = 0
|
||||
|
||||
# Only used by transport missions.
|
||||
self.cargo = cargo
|
||||
@@ -90,6 +91,12 @@ class Flight(SidcDescribable):
|
||||
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
|
||||
|
||||
@@ -4,28 +4,39 @@ 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 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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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
|
||||
# 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 from self.nav_to_ingress
|
||||
yield self.ingress
|
||||
yield self.drop_off
|
||||
yield self.target
|
||||
yield from self.nav_to_home
|
||||
@@ -35,7 +46,7 @@ class AirAssaultLayout(AirliftLayout):
|
||||
yield self.bullseye
|
||||
|
||||
|
||||
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
|
||||
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
@@ -53,35 +64,48 @@ 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:
|
||||
raise PlanningError("Air assault is only usable by helicopters")
|
||||
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.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:
|
||||
# TODO The calculation of the Pickup LZ is currently randomized. This
|
||||
# leads to the problem that we can not gurantee that the LZ is clear of
|
||||
# obstacles. This has to be improved in the future so that the Mission can
|
||||
# be autoplanned. In the current state the User has to check the created
|
||||
# Waypoints for the Pickup and Dropoff LZs are free of obstacles.
|
||||
# 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),
|
||||
@@ -90,6 +114,8 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
pickup_position = pickup.position
|
||||
assault_area = builder.assault_area(self.package.target)
|
||||
heading = self.package.target.position.heading_between_point(pickup_position)
|
||||
|
||||
# TODO we can not gurantee a safe LZ for DropOff. See comment above.
|
||||
drop_off_zone = MissionTarget(
|
||||
"Dropoff zone",
|
||||
self.package.target.position.point_from_heading(heading, 1200),
|
||||
@@ -97,21 +123,19 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
|
||||
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=builder.dropoff_zone(drop_off_zone),
|
||||
target=assault_area,
|
||||
nav_to_home=builder.nav_path(
|
||||
drop_off_zone.position,
|
||||
|
||||
@@ -19,10 +19,20 @@ if TYPE_CHECKING:
|
||||
@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 +40,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 +61,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 +94,31 @@ 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", cargo.origin.position.random_point_within(1000, 200)
|
||||
)
|
||||
)
|
||||
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",
|
||||
cargo.next_stop.position.random_point_within(1000, 200),
|
||||
)
|
||||
)
|
||||
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 +127,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,14 +139,14 @@ 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(),
|
||||
)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -8,6 +8,8 @@ from .formationattack import (
|
||||
FormationAttackLayout,
|
||||
)
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from .. import FlightType
|
||||
from ...utils import Distance
|
||||
|
||||
|
||||
class EscortFlightPlan(FormationAttackFlightPlan):
|
||||
@@ -24,12 +26,17 @@ 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 = None
|
||||
if self.package.primary_task == FlightType.STRIKE:
|
||||
initial = builder.escort_hold(
|
||||
self.package.waypoints.initial, Distance.from_feet(20000)
|
||||
)
|
||||
|
||||
return FormationAttackLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
@@ -39,11 +46,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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
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:
|
||||
...
|
||||
@@ -304,6 +304,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 +422,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 +519,50 @@ 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
|
||||
"""
|
||||
return FlightWaypoint(
|
||||
"CARGOSTOP",
|
||||
FlightWaypointType.CARGO_STOP,
|
||||
control_point.position,
|
||||
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
|
||||
|
||||
@@ -151,10 +151,22 @@ class Loadout:
|
||||
# names, so those have been included here too. The priority goes from first to
|
||||
# last - the first element in the tuple will be tried first, then the second,
|
||||
# etc.
|
||||
loadout_names = {t: [f"Liberation {t.value}"] for t in FlightType}
|
||||
loadout_names = {
|
||||
t: [f"Retribution {t.value}", f"Liberation {t.value}"] for t in FlightType
|
||||
}
|
||||
legacy_names = {
|
||||
FlightType.TARCAP: ("CAP HEAVY", "CAP", "Liberation BARCAP"),
|
||||
FlightType.BARCAP: ("CAP HEAVY", "CAP", "Liberation TARCAP"),
|
||||
FlightType.TARCAP: (
|
||||
"CAP HEAVY",
|
||||
"CAP",
|
||||
"Retribution BARCAP",
|
||||
"Liberation BARCAP",
|
||||
),
|
||||
FlightType.BARCAP: (
|
||||
"CAP HEAVY",
|
||||
"CAP",
|
||||
"Retribution TARCAP",
|
||||
"Liberation TARCAP",
|
||||
),
|
||||
FlightType.CAS: ("CAS MAVERICK F", "CAS"),
|
||||
FlightType.STRIKE: ("STRIKE",),
|
||||
FlightType.ANTISHIP: ("ANTISHIP",),
|
||||
|
||||
@@ -129,6 +129,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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,6 +32,7 @@ from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.shipunittype import ShipUnitType
|
||||
from game.dcs.unittype import UnitType
|
||||
from pydcs_extensions.f16i_idf.f16i_idf import inject_F16I
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater.start_generator import ModSettings
|
||||
@@ -115,6 +116,9 @@ class Faction:
|
||||
# List of default livery overrides
|
||||
liveries_overrides: Dict[AircraftType, List[str]] = field(default_factory=dict)
|
||||
|
||||
# List of default livery overrides for ground vehicles
|
||||
liveries_overrides_ground_forces: Dict[str, List[str]] = field(default_factory=dict)
|
||||
|
||||
#: Set to True if the faction should force the "Unrestricted satnav" option
|
||||
#: for the mission. This option enables GPS for capable aircraft regardless
|
||||
#: of the time period or operator. For example, the CJTF "countries" don't
|
||||
@@ -124,6 +128,10 @@ class Faction:
|
||||
#: both will use it.
|
||||
unrestricted_satnav: bool = False
|
||||
|
||||
# Store mod settings so mod properties can be injected again on game load,
|
||||
# in case mods like CJS F/A-18E/F/G or IDF F-16I are selected by the player
|
||||
mod_settings: Optional[ModSettings] = field(default=None)
|
||||
|
||||
def has_access_to_dcs_type(self, unit_type: Type[DcsUnitType]) -> bool:
|
||||
# Vehicle and Ship Units
|
||||
if any(unit_type == u.dcs_unit_type for u in self.accessible_units):
|
||||
@@ -279,6 +287,16 @@ class Faction:
|
||||
aircraft = AircraftType.named(name)
|
||||
faction.liveries_overrides[aircraft] = [s.lower() for s in livery]
|
||||
|
||||
# Load liveries override for ground forces
|
||||
faction.liveries_overrides_ground_forces = {}
|
||||
liveries_overrides_ground_forces = json.get(
|
||||
"liveries_overrides_ground_forces", {}
|
||||
)
|
||||
for vehicle_type, livery in liveries_overrides_ground_forces.items():
|
||||
faction.liveries_overrides_ground_forces[vehicle_type] = [
|
||||
s.lower() for s in livery
|
||||
]
|
||||
|
||||
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
|
||||
|
||||
return faction
|
||||
@@ -294,7 +312,21 @@ class Faction:
|
||||
if unit.unit_class is unit_class:
|
||||
yield unit
|
||||
|
||||
def apply_mod_settings(self, mod_settings: ModSettings) -> None:
|
||||
def apply_mod_settings(self, mod_settings: Optional[ModSettings] = None) -> None:
|
||||
if mod_settings is None:
|
||||
if self.mod_settings is None:
|
||||
# No mod settings were provided and none were saved for this faction
|
||||
# so stop here
|
||||
return
|
||||
elif self.mod_settings is not None:
|
||||
# Saved mod settings were found for this faction,
|
||||
# so load them for use
|
||||
mod_settings = self.mod_settings
|
||||
else:
|
||||
# Update the mod settings of this faction
|
||||
# so the settings can be applied again on load, if needed
|
||||
self.mod_settings = mod_settings
|
||||
|
||||
# aircraft
|
||||
if not mod_settings.a4_skyhawk:
|
||||
self.remove_aircraft("A-4E-C")
|
||||
@@ -307,6 +339,16 @@ class Faction:
|
||||
self.remove_aircraft("VSN_F4B")
|
||||
if not mod_settings.f15d_baz:
|
||||
self.remove_aircraft("F-15D")
|
||||
if not mod_settings.f_16_idf:
|
||||
self.remove_aircraft("F-16I")
|
||||
self.remove_aircraft("F_16D_52")
|
||||
self.remove_aircraft("F_16D_50")
|
||||
self.remove_aircraft("F_16D_50_NS")
|
||||
self.remove_aircraft("F_16D_52_NS")
|
||||
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:
|
||||
@@ -314,6 +356,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")
|
||||
@@ -332,6 +375,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")
|
||||
@@ -391,6 +436,11 @@ class Faction:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.aircrafts.remove(i)
|
||||
|
||||
def remove_aircraft_by_name(self, name: str) -> None:
|
||||
for i in self.aircrafts:
|
||||
if i.name == name:
|
||||
self.aircrafts.remove(i)
|
||||
|
||||
def remove_preset(self, name: str) -> None:
|
||||
for pg in self.preset_groups:
|
||||
if pg.name == name:
|
||||
|
||||
@@ -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)
|
||||
|
||||
14
game/game.py
14
game/game.py
@@ -232,6 +232,10 @@ class Game:
|
||||
LuaPluginManager.load_settings(self.settings)
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
self.compute_unculled_zones(GameUpdateEvents())
|
||||
# Apply mod settings again so mod properties get injected again,
|
||||
# in case mods like CJS F/A-18E/F/G or IDF F-16I are selected by the player
|
||||
self.blue.faction.apply_mod_settings()
|
||||
self.red.faction.apply_mod_settings()
|
||||
if not game_still_initializing:
|
||||
# We don't need to push events that happen during load. The UI will fully
|
||||
# reset when we're done.
|
||||
@@ -524,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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
44
game/migrator.py
Normal file
44
game/migrator.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.ato.packagewaypoints import PackageWaypoints
|
||||
from game.data.doctrine import MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
|
||||
from game.utils import knots, feet, nautical_miles
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
class Migrator:
|
||||
def __init__(self, game: Game):
|
||||
self.game = game
|
||||
self._migrate_game()
|
||||
|
||||
def _migrate_game(self) -> None:
|
||||
self._update_doctrine()
|
||||
self._update_packagewaypoints()
|
||||
|
||||
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 not hasattr(p.waypoints, "initial"):
|
||||
p.waypoints = PackageWaypoints.create(p, c)
|
||||
@@ -8,6 +8,7 @@ from dcs.task import (
|
||||
CAP,
|
||||
CAS,
|
||||
EPLRS,
|
||||
Escort,
|
||||
FighterSweep,
|
||||
GroundAttack,
|
||||
Nothing,
|
||||
@@ -19,6 +20,9 @@ from dcs.task import (
|
||||
Refueling,
|
||||
RunwayAttack,
|
||||
Transport,
|
||||
SEAD,
|
||||
SwitchWaypoint,
|
||||
OptJettisonEmptyTanks,
|
||||
)
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
|
||||
@@ -83,6 +87,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 +109,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
|
||||
|
||||
@@ -265,7 +271,9 @@ 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.package.primary_task == FlightType.STRIKE:
|
||||
group.add_trigger_action(SwitchWaypoint(None, 5))
|
||||
self.configure_behavior(
|
||||
flight, group, roe=OptROE.Values.OpenFire, restrict_jettison=True
|
||||
)
|
||||
@@ -274,7 +282,9 @@ 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
|
||||
if flight.package.primary_task == FlightType.STRIKE:
|
||||
group.add_trigger_action(SwitchWaypoint(None, 5))
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
@@ -304,6 +314,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
|
||||
@@ -111,6 +114,23 @@ class AircraftGenerator:
|
||||
flight, country, dynamic_runways
|
||||
)
|
||||
self.unit_map.add_aircraft(group, flight)
|
||||
if (
|
||||
package.primary_task == FlightType.STRIKE
|
||||
and package.primary_flight is not None
|
||||
):
|
||||
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
|
||||
@@ -135,6 +155,9 @@ class AircraftGenerator:
|
||||
def _spawn_unused_for(
|
||||
self, squadron: Squadron, country: Country, faction: Faction
|
||||
) -> None:
|
||||
if self.game.settings.perf_disable_idle_aircraft:
|
||||
return
|
||||
|
||||
assert isinstance(squadron.location, Airfield)
|
||||
for _ in range(squadron.untasked_aircraft):
|
||||
# Creating a flight even those this isn't a fragged mission lets us
|
||||
@@ -152,7 +175,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 +185,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(
|
||||
|
||||
@@ -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
|
||||
@@ -136,6 +145,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 +158,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,
|
||||
@@ -259,7 +273,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)
|
||||
@@ -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
|
||||
|
||||
@@ -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,32 @@ 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) * 50 * 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import OptECMUsing, OptFormation
|
||||
from dcs.task import OptECMUsing, OptFormation, RunScript
|
||||
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
@@ -16,4 +16,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,15 +1,22 @@
|
||||
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
|
||||
|
||||
from game.utils import mach, Distance
|
||||
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]:
|
||||
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]
|
||||
if bomber_guided:
|
||||
self.add_strike_tasks(waypoint, WeaponType.Guided)
|
||||
self.add_bombing_tasks(waypoint)
|
||||
elif bomber:
|
||||
self.add_bombing_tasks(waypoint)
|
||||
else:
|
||||
self.add_strike_tasks(waypoint)
|
||||
@@ -21,24 +28,28 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
|
||||
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)
|
||||
ga = True if self.flight.count > 1 else False
|
||||
bombing = Bombing(
|
||||
center, weapon_type=WeaponType.Bombs, expend=Expend.All, group_attack=True
|
||||
center, weapon_type=WeaponType.Bombs, expend=Expend.All, group_attack=ga
|
||||
)
|
||||
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)
|
||||
|
||||
waypoint.speed = mach(0.85, Distance.from_feet(20000)).meters_per_second
|
||||
|
||||
# Register special waypoints
|
||||
self.register_special_waypoints(self.waypoint.targets)
|
||||
|
||||
@@ -16,12 +16,12 @@ 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 .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 +118,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 +132,10 @@ 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,
|
||||
}
|
||||
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
|
||||
return builder(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
from typing import List, Optional, TYPE_CHECKING, Tuple
|
||||
@@ -43,6 +44,7 @@ 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
|
||||
|
||||
@@ -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,6 +273,8 @@ class FlotGenerator:
|
||||
heading=forward_heading.degrees,
|
||||
move_formation=PointAction.OffRoad,
|
||||
)
|
||||
vehicle = vg.units[0]
|
||||
GroundForcePainter(faction, vehicle).apply_livery()
|
||||
|
||||
def _set_reform_waypoint(
|
||||
self, dcs_group: VehicleGroup, forward_heading: Heading
|
||||
@@ -769,6 +777,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 +793,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
|
||||
@@ -400,6 +400,9 @@ 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."
|
||||
)
|
||||
|
||||
fl = self.flight
|
||||
|
||||
@@ -636,6 +639,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
|
||||
@@ -655,17 +660,21 @@ class StrikeTaskPage(KneeboardPage):
|
||||
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),
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -44,7 +44,7 @@ if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
COMBINED_ARMS_SLOTS = 1
|
||||
COMBINED_ARMS_SLOTS = 3
|
||||
|
||||
|
||||
def country_id_from_name(name: str) -> int:
|
||||
@@ -330,6 +330,12 @@ class MissionGenerator:
|
||||
gen.generate()
|
||||
|
||||
def setup_combined_arms(self) -> None:
|
||||
observers = 3
|
||||
self.mission.groundControl.pilot_can_control_vehicles = COMBINED_ARMS_SLOTS > 0
|
||||
self.mission.groundControl.blue_game_masters = 1
|
||||
self.mission.groundControl.blue_tactical_commander = COMBINED_ARMS_SLOTS
|
||||
self.mission.groundControl.blue_observer = 1
|
||||
self.mission.groundControl.blue_observer = observers
|
||||
self.mission.groundControl.red_game_masters = 1
|
||||
self.mission.groundControl.red_tactical_commander = COMBINED_ARMS_SLOTS
|
||||
self.mission.groundControl.red_observer = observers
|
||||
self.mission.groundControl.neutrals_observer = observers
|
||||
|
||||
@@ -40,6 +40,10 @@ 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.radios import RadioFrequency, RadioRegistry
|
||||
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
|
||||
@@ -122,6 +126,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 +139,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 +161,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 +174,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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -162,7 +162,7 @@ class LuaPlugin(PluginSettings):
|
||||
for option in self.options:
|
||||
enabled = str(option.enabled).lower()
|
||||
name = option.identifier
|
||||
option_decls.append(f" dcsLiberation.plugins.{name} = {enabled}")
|
||||
option_decls.append(f" dcsRetribution.plugins.{name} = {enabled}")
|
||||
|
||||
joined_options = "\n".join(option_decls)
|
||||
|
||||
@@ -170,11 +170,11 @@ class LuaPlugin(PluginSettings):
|
||||
f"""\
|
||||
-- {self.identifier} plugin configuration.
|
||||
|
||||
if dcsLiberation then
|
||||
if not dcsLiberation.plugins then
|
||||
dcsLiberation.plugins = {{}}
|
||||
if dcsRetribution then
|
||||
if not dcsRetribution.plugins then
|
||||
dcsRetribution.plugins = {{}}
|
||||
end
|
||||
dcsLiberation.plugins.{self.identifier} = {{}}
|
||||
dcsRetribution.plugins.{self.identifier} = {{}}
|
||||
{joined_options}
|
||||
end
|
||||
|
||||
|
||||
@@ -256,6 +256,52 @@ RADIOS: List[Radio] = [
|
||||
RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),
|
||||
),
|
||||
),
|
||||
# MB-339A
|
||||
Radio(
|
||||
"AN/ARC-150(V) 2",
|
||||
(
|
||||
RadioRange(
|
||||
MHz(225),
|
||||
MHz(400),
|
||||
MHz(1),
|
||||
Modulation.AM,
|
||||
frozenset((MHz(243),)),
|
||||
),
|
||||
),
|
||||
),
|
||||
Radio(
|
||||
"SRT-651/N",
|
||||
(
|
||||
RadioRange(
|
||||
MHz(30),
|
||||
MHz(88),
|
||||
MHz(1),
|
||||
Modulation.FM,
|
||||
frozenset((MHz(40, 500),)),
|
||||
),
|
||||
RadioRange(
|
||||
MHz(108),
|
||||
MHz(156),
|
||||
MHz(1),
|
||||
Modulation.AM,
|
||||
frozenset((MHz(121, 500),)),
|
||||
),
|
||||
RadioRange(
|
||||
MHz(156),
|
||||
MHz(174),
|
||||
MHz(1),
|
||||
Modulation.FM,
|
||||
frozenset((MHz(156, 800),)),
|
||||
),
|
||||
RadioRange(
|
||||
MHz(225),
|
||||
MHz(400),
|
||||
MHz(1),
|
||||
Modulation.AM, # Actually AM/FM, but we can't represent that.
|
||||
frozenset((MHz(243),)),
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,7 @@ from fastapi import APIRouter, Depends
|
||||
from shapely.geometry import LineString, Point as ShapelyPoint
|
||||
|
||||
from game import Game
|
||||
from game.ato.flightplans.airassault import AirAssaultFlightPlan
|
||||
from game.ato.flightplans.cas import CasFlightPlan
|
||||
from game.ato.flightplans.patrolling import PatrollingFlightPlan
|
||||
from game.ato.flightplans.uizonedisplay import UiZoneDisplay
|
||||
from game.server import GameContext
|
||||
from game.server.flights.models import FlightJs
|
||||
from game.server.leaflet import LeafletPoly, ShapelyUtil
|
||||
@@ -40,23 +38,12 @@ def commit_boundary(
|
||||
flight_id: UUID, game: Game = Depends(GameContext.require)
|
||||
) -> LeafletPoly:
|
||||
flight = game.db.flights.get(flight_id)
|
||||
if isinstance(flight.flight_plan, CasFlightPlan) or isinstance(
|
||||
flight.flight_plan, AirAssaultFlightPlan
|
||||
):
|
||||
# Special Commit boundary for CAS and AirAssault
|
||||
center = flight.flight_plan.layout.target.position
|
||||
commit_center = ShapelyPoint(center.x, center.y)
|
||||
elif isinstance(flight.flight_plan, PatrollingFlightPlan):
|
||||
# Commit boundary for standard patrolling flight plan
|
||||
start = flight.flight_plan.layout.patrol_start
|
||||
end = flight.flight_plan.layout.patrol_end
|
||||
commit_center = LineString(
|
||||
[
|
||||
ShapelyPoint(start.x, start.y),
|
||||
ShapelyPoint(end.x, end.y),
|
||||
]
|
||||
)
|
||||
else:
|
||||
if not isinstance(flight.flight_plan, UiZoneDisplay):
|
||||
return []
|
||||
bubble = commit_center.buffer(flight.flight_plan.engagement_distance.meters)
|
||||
zone = flight.flight_plan.ui_zone()
|
||||
if len(zone.points) == 1:
|
||||
center = ShapelyPoint(zone.points[0].x, zone.points[0].y)
|
||||
else:
|
||||
center = LineString([ShapelyPoint(p.x, p.y) for p in zone.points])
|
||||
bubble = center.buffer(zone.radius.meters)
|
||||
return ShapelyUtil.poly_to_leaflet(bubble, game.theater)
|
||||
|
||||
@@ -14,6 +14,7 @@ from .minutesoption import minutes_option
|
||||
from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY
|
||||
from .skilloption import skill_option
|
||||
from ..ato.starttype import StartType
|
||||
from ..weather import NightMissions
|
||||
|
||||
|
||||
@unique
|
||||
@@ -56,19 +57,19 @@ class Settings:
|
||||
"Player coalition skill",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=AI_DIFFICULTY_SECTION,
|
||||
default="Good",
|
||||
default="High",
|
||||
)
|
||||
enemy_skill: str = skill_option(
|
||||
"Enemy coalition skill",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=AI_DIFFICULTY_SECTION,
|
||||
default="Average",
|
||||
default="High",
|
||||
)
|
||||
enemy_vehicle_skill: str = skill_option(
|
||||
"Enemy AA and vehicles skill",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=AI_DIFFICULTY_SECTION,
|
||||
default="Average",
|
||||
default="High",
|
||||
)
|
||||
player_income_multiplier: float = bounded_float_option(
|
||||
"Player income multiplier",
|
||||
@@ -105,11 +106,16 @@ class Settings:
|
||||
section=MISSION_DIFFICULTY_SECTION,
|
||||
default=True,
|
||||
)
|
||||
night_disabled: bool = boolean_option(
|
||||
"No night missions",
|
||||
night_day_missions: NightMissions = choices_option(
|
||||
"Night/day mission options",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=MISSION_DIFFICULTY_SECTION,
|
||||
default=False,
|
||||
choices={
|
||||
"Generate night and day missions": NightMissions.DayAndNight,
|
||||
"Only generate day missions": NightMissions.OnlyDay,
|
||||
"Only generate night missions": NightMissions.OnlyNight,
|
||||
},
|
||||
default=NightMissions.DayAndNight,
|
||||
)
|
||||
# Mission Restrictions
|
||||
labels: str = choices_option(
|
||||
@@ -205,10 +211,10 @@ class Settings:
|
||||
)
|
||||
#: Feature flag for squadron limits.
|
||||
enable_squadron_pilot_limits: bool = boolean_option(
|
||||
"Enable per-squadron pilot limits (WIP)",
|
||||
"Enable per-squadron pilot limits",
|
||||
CAMPAIGN_MANAGEMENT_PAGE,
|
||||
PILOTS_AND_SQUADRONS_SECTION,
|
||||
default=False,
|
||||
default=True,
|
||||
detail=(
|
||||
"If set, squadrons will be limited to a maximum number of pilots and dead "
|
||||
"pilots will replenish at a fixed rate, each defined with the settings"
|
||||
@@ -225,7 +231,7 @@ class Settings:
|
||||
CAMPAIGN_MANAGEMENT_PAGE,
|
||||
PILOTS_AND_SQUADRONS_SECTION,
|
||||
default=12,
|
||||
min=12,
|
||||
min=6,
|
||||
max=72,
|
||||
detail=(
|
||||
"Sets the maximum number of pilots a squadron may have active. "
|
||||
@@ -476,6 +482,12 @@ class Settings:
|
||||
section=PERFORMANCE_SECTION,
|
||||
default=True,
|
||||
)
|
||||
perf_disable_idle_aircraft: bool = boolean_option(
|
||||
"Disable idle aircraft at airfields",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=PERFORMANCE_SECTION,
|
||||
default=False,
|
||||
)
|
||||
# Performance culling
|
||||
perf_culling: bool = boolean_option(
|
||||
"Culling of distant units enabled",
|
||||
|
||||
@@ -37,6 +37,7 @@ from dcs.ships import (
|
||||
LHA_Tarawa,
|
||||
Stennis,
|
||||
Type_071,
|
||||
Hms_invincible,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, ParkingSlot
|
||||
from dcs.unitgroup import ShipGroup, StaticGroup
|
||||
@@ -1203,6 +1204,7 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
LHA_Tarawa,
|
||||
KUZNECOW,
|
||||
Type_071,
|
||||
Hms_invincible,
|
||||
]:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -53,7 +53,7 @@ class SkynetNode:
|
||||
# Statics will be placed as dead unit
|
||||
return group.units[0].unit_name
|
||||
# If no alive unit is available and not static raise error
|
||||
raise IadsNetworkException("Group has no skynet usable units")
|
||||
raise IadsNetworkException(f"{group.name} has no skynet usable units")
|
||||
else:
|
||||
# Use the GroupName for SAMs, SAMAsEWR and PDs
|
||||
return group.group_name
|
||||
@@ -121,6 +121,10 @@ class IadsNetwork:
|
||||
# Skip culled ground objects
|
||||
continue
|
||||
|
||||
all_dead = not any([x.alive for x in node.group.units])
|
||||
if all_dead:
|
||||
continue
|
||||
|
||||
# SkynetNode.from_group(node.group) may raise an exception
|
||||
# (originating from SkynetNode.dcs_name_for_group)
|
||||
# but if it does, we want to know because it's supposed to be impossible afaict
|
||||
@@ -194,8 +198,11 @@ class IadsNetwork:
|
||||
# Not participating
|
||||
return
|
||||
events.update_iads_node(node)
|
||||
if self.advanced_iads and not self.iads_config:
|
||||
self._make_advanced_connections_by_range(node)
|
||||
if self.advanced_iads:
|
||||
if self.iads_config:
|
||||
self._add_connections_from_config(node)
|
||||
else:
|
||||
self._make_advanced_connections_by_range(node)
|
||||
|
||||
def node_for_group(self, group: IadsGroundGroup) -> IadsNetworkNode:
|
||||
"""Get existing node from the iads network or create a new node"""
|
||||
@@ -217,7 +224,7 @@ class IadsNetwork:
|
||||
node: Optional[IadsNetworkNode] = None
|
||||
for group in tgo.groups:
|
||||
# TODO Cleanup
|
||||
if isinstance(group, IadsGroundGroup):
|
||||
if isinstance(group, IadsGroundGroup) and group.alive_units > 0:
|
||||
# The first IadsGroundGroup is always the primary Group
|
||||
if not node and group.iads_role.participate:
|
||||
# Primary Node
|
||||
@@ -255,17 +262,17 @@ class IadsNetwork:
|
||||
|
||||
def initialize_network_from_config(self) -> None:
|
||||
"""Initialize the IADS Network from a configuration"""
|
||||
for element_name, connections in self.iads_config.items():
|
||||
for primary_node in self.iads_config.keys():
|
||||
warning_msg = (
|
||||
f"IADS: No ground object found for {element_name}."
|
||||
f"IADS: No ground object found for {primary_node}."
|
||||
f" This can be normal behaviour."
|
||||
)
|
||||
if element_name in self.ground_objects:
|
||||
node = self.node_for_tgo(self.ground_objects[element_name])
|
||||
if primary_node in self.ground_objects:
|
||||
node = self.node_for_tgo(self.ground_objects[primary_node])
|
||||
else:
|
||||
node = None
|
||||
warning_msg = (
|
||||
f"IADS: No ground object found for connection {element_name}"
|
||||
f"IADS: No ground object found for connection {primary_node}"
|
||||
)
|
||||
|
||||
if node is None:
|
||||
@@ -274,16 +281,20 @@ class IadsNetwork:
|
||||
# available. Therefore the TGO will not get populated at all
|
||||
logging.warning(warning_msg)
|
||||
continue
|
||||
self._add_connections_from_config(node)
|
||||
|
||||
# Find all connected ground_objects
|
||||
for node_name in connections:
|
||||
try:
|
||||
node.add_connection_for_tgo(self.ground_objects[node_name])
|
||||
except KeyError:
|
||||
logging.error(
|
||||
f"IADS: No ground object found for connection {node_name}"
|
||||
)
|
||||
continue
|
||||
def _add_connections_from_config(self, node: IadsNetworkNode) -> None:
|
||||
"""Add all connections for the given primary node based on the iads_config"""
|
||||
primary_node = node.group.ground_object.original_name
|
||||
connections = self.iads_config[primary_node]
|
||||
for secondary_node in connections:
|
||||
try:
|
||||
node.add_connection_for_tgo(self.ground_objects[secondary_node])
|
||||
except KeyError:
|
||||
logging.error(
|
||||
f"IADS: No ground object found for connection {secondary_node}"
|
||||
)
|
||||
continue
|
||||
|
||||
def initialize_network_from_range(self) -> None:
|
||||
"""Initialize the IADS Network by range"""
|
||||
|
||||
@@ -45,4 +45,9 @@ class SeasonalConditions:
|
||||
winter_avg_temperature: float
|
||||
temperature_day_night_difference: float
|
||||
|
||||
high_avg_yearly_turbulence_per_10cm: float
|
||||
low_avg_yearly_turbulence_per_10cm: float
|
||||
solar_noon_turbulence_per_10cm: float
|
||||
midnight_turbulence_per_10cm: float
|
||||
|
||||
weather_type_chances: dict[Season, WeatherTypeChances]
|
||||
|
||||
@@ -55,6 +55,7 @@ class ModSettings:
|
||||
a6a_intruder: bool = False
|
||||
f4b_phantom: bool = False
|
||||
f15d_baz: bool = False
|
||||
f_16_idf: bool = False
|
||||
f22_raptor: bool = False
|
||||
f84g_thunderjet: bool = False
|
||||
f100_supersabre: bool = False
|
||||
@@ -67,6 +68,7 @@ class ModSettings:
|
||||
su57_felon: bool = False
|
||||
frenchpack: bool = False
|
||||
high_digit_sams: bool = False
|
||||
ov10a_bronco: bool = False
|
||||
|
||||
|
||||
class GameGenerator:
|
||||
|
||||
@@ -57,6 +57,23 @@ class SeasonData:
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TurbulenceData:
|
||||
high_avg_yearly_turbulence_per_10cm: float | None
|
||||
low_avg_yearly_turbulence_per_10cm: float | None
|
||||
solar_noon_turbulence_per_10cm: float | None
|
||||
midnight_turbulence_per_10cm: float | None
|
||||
|
||||
@staticmethod
|
||||
def from_yaml(data: dict[str, Any]) -> TurbulenceData:
|
||||
return TurbulenceData(
|
||||
data.get("high_avg_yearly_turbulence_per_10cm"),
|
||||
data.get("low_avg_yearly_turbulence_per_10cm"),
|
||||
data.get("solar_noon_turbulence_per_10cm"),
|
||||
data.get("midnight_turbulence_per_10cm"),
|
||||
)
|
||||
|
||||
|
||||
class TheaterLoader:
|
||||
THEATER_RESOURCE_DIR = Path("resources/theaters")
|
||||
|
||||
@@ -113,6 +130,7 @@ class TheaterLoader:
|
||||
spring = SeasonData.from_yaml(climate_data["seasons"]["spring"])
|
||||
summer = SeasonData.from_yaml(climate_data["seasons"]["summer"])
|
||||
fall = SeasonData.from_yaml(climate_data["seasons"]["fall"])
|
||||
turbulence = TurbulenceData.from_yaml(climate_data["turbulence"])
|
||||
if summer.average_pressure is None:
|
||||
raise RuntimeError(
|
||||
f"{self.descriptor_path} does not define a summer average pressure"
|
||||
@@ -129,12 +147,32 @@ class TheaterLoader:
|
||||
raise RuntimeError(
|
||||
f"{self.descriptor_path} does not define a winter average temperature"
|
||||
)
|
||||
if turbulence.high_avg_yearly_turbulence_per_10cm is None:
|
||||
raise RuntimeError(
|
||||
f"{self.descriptor_path} does not define a yearly average high turbulence"
|
||||
)
|
||||
if turbulence.low_avg_yearly_turbulence_per_10cm is None:
|
||||
raise RuntimeError(
|
||||
f"{self.descriptor_path} does not define a yearly average low turbulence"
|
||||
)
|
||||
if turbulence.solar_noon_turbulence_per_10cm is None:
|
||||
raise RuntimeError(
|
||||
f"{self.descriptor_path} does not define a solar noon turbulence"
|
||||
)
|
||||
if turbulence.midnight_turbulence_per_10cm is None:
|
||||
raise RuntimeError(
|
||||
f"{self.descriptor_path} does not define a midnight turbulence"
|
||||
)
|
||||
return SeasonalConditions(
|
||||
summer.average_pressure,
|
||||
winter.average_pressure,
|
||||
summer.average_temperature,
|
||||
winter.average_temperature,
|
||||
climate_data["day_night_temperature_difference"],
|
||||
turbulence.high_avg_yearly_turbulence_per_10cm,
|
||||
turbulence.low_avg_yearly_turbulence_per_10cm,
|
||||
turbulence.solar_noon_turbulence_per_10cm,
|
||||
turbulence.midnight_turbulence_per_10cm,
|
||||
{
|
||||
Season.Winter: winter.weather,
|
||||
Season.Spring: spring.weather,
|
||||
|
||||
@@ -161,10 +161,15 @@ VERSION = _build_version_string()
|
||||
#:
|
||||
#: Version 10.3
|
||||
#: * Campaign files can optionally include a start time in their recommended_start_date
|
||||
#: field. For example, `recommended_start_data: 2022-08-31 13:30:00` will have the
|
||||
#: field. For example, `recommended_start_date: 2022-08-31 13:30:00` will have the
|
||||
#: first turn start at 13:30. If omitted, or if only a date is given, the mission will
|
||||
#: start at a random hour in the middle of the day as before.
|
||||
#:
|
||||
#: Version 10.4
|
||||
#: * Support for the Falklands.
|
||||
CAMPAIGN_FORMAT_VERSION = (10, 4)
|
||||
#:
|
||||
#: Version 10.5
|
||||
#: * Support for scenery objectives defined by quad zones.
|
||||
#: * Campaign designers can now define `max_frontline_length` (in km) setting:
|
||||
#: `max_frontline_length: 25`
|
||||
CAMPAIGN_FORMAT_VERSION = (10, 5)
|
||||
|
||||
169
game/weather.py
169
game/weather.py
@@ -2,8 +2,10 @@ from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.cloud_presets import Clouds as PydcsClouds
|
||||
@@ -11,7 +13,15 @@ from dcs.weather import CloudPreset, Weather as PydcsWeather, Wind
|
||||
|
||||
from game.theater.seasonalconditions import determine_season
|
||||
from game.timeofday import TimeOfDay
|
||||
from game.utils import Distance, Heading, Pressure, inches_hg, interpolate, meters
|
||||
from game.utils import (
|
||||
Distance,
|
||||
Heading,
|
||||
Pressure,
|
||||
inches_hg,
|
||||
interpolate,
|
||||
knots,
|
||||
meters,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.settings import Settings
|
||||
@@ -19,6 +29,12 @@ if TYPE_CHECKING:
|
||||
from game.theater.seasonalconditions import SeasonalConditions
|
||||
|
||||
|
||||
class NightMissions(Enum):
|
||||
DayAndNight = "nightmissions_nightandday"
|
||||
OnlyDay = "nightmissions_onlyday"
|
||||
OnlyNight = "nightmissions_onlynight"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AtmosphericConditions:
|
||||
#: Pressure at sea level.
|
||||
@@ -27,6 +43,9 @@ class AtmosphericConditions:
|
||||
#: Temperature at sea level in Celcius.
|
||||
temperature_celsius: float
|
||||
|
||||
#: Turbulence per 10 cm.
|
||||
turbulence_per_10cm: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WindConditions:
|
||||
@@ -99,18 +118,38 @@ class Weather:
|
||||
day,
|
||||
)
|
||||
|
||||
seasonal_turbulence = self.interpolate_seasonal_turbulence(
|
||||
seasonal_conditions.high_avg_yearly_turbulence_per_10cm,
|
||||
seasonal_conditions.low_avg_yearly_turbulence_per_10cm,
|
||||
day,
|
||||
)
|
||||
|
||||
day_turbulence = seasonal_conditions.solar_noon_turbulence_per_10cm
|
||||
night_turbulence = seasonal_conditions.midnight_turbulence_per_10cm
|
||||
time_of_day_turbulence = self.interpolate_solar_activity(
|
||||
time_of_day, day_turbulence, night_turbulence
|
||||
)
|
||||
|
||||
random_turbulence = random.normalvariate(mu=0, sigma=0.5)
|
||||
|
||||
turbulence = abs(
|
||||
seasonal_turbulence + time_of_day_turbulence + random_turbulence
|
||||
)
|
||||
|
||||
if time_of_day == TimeOfDay.Day:
|
||||
temperature += seasonal_conditions.temperature_day_night_difference / 2
|
||||
if time_of_day == TimeOfDay.Night:
|
||||
temperature -= seasonal_conditions.temperature_day_night_difference / 2
|
||||
pressure += self.pressure_adjustment
|
||||
temperature += self.temperature_adjustment
|
||||
turbulence += self.turbulence_adjustment
|
||||
logging.debug(
|
||||
"Weather: Before random: temp {} press {}".format(temperature, pressure)
|
||||
)
|
||||
conditions = AtmosphericConditions(
|
||||
qnh=self.random_pressure(pressure),
|
||||
temperature_celsius=self.random_temperature(temperature),
|
||||
turbulence_per_10cm=turbulence,
|
||||
)
|
||||
logging.debug(
|
||||
"Weather: After random: temp {} press {}".format(
|
||||
@@ -127,6 +166,10 @@ class Weather:
|
||||
def temperature_adjustment(self) -> float:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def turbulence_adjustment(self) -> float:
|
||||
raise NotImplementedError
|
||||
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -147,24 +190,62 @@ class Weather:
|
||||
wind_direction_2000m = wind_direction + Heading.random(-90, 90)
|
||||
wind_direction_8000m = wind_direction + Heading.random(-90, 90)
|
||||
at_0m_factor = 1
|
||||
at_2000m_factor = 2
|
||||
at_8000m_factor = 3
|
||||
at_2000m_factor = 3 + random.choice([0, 0, 0, 0, 0, 1, 1])
|
||||
|
||||
high_alt_variation = random.choice(
|
||||
[
|
||||
-3,
|
||||
-3,
|
||||
-2,
|
||||
-2,
|
||||
-2,
|
||||
-2,
|
||||
-2,
|
||||
-2,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
]
|
||||
)
|
||||
at_8000m_factor = at_2000m_factor + 5 + high_alt_variation
|
||||
|
||||
base_wind = random.randint(minimum, maximum)
|
||||
|
||||
# DCS is limited to 97 knots wind speed.
|
||||
max_supported_wind_speed = knots(97).meters_per_second
|
||||
|
||||
return WindConditions(
|
||||
# Always some wind to make the smoke move a bit.
|
||||
at_0m=Wind(wind_direction.degrees, max(1, base_wind * at_0m_factor)),
|
||||
at_2000m=Wind(wind_direction_2000m.degrees, base_wind * at_2000m_factor),
|
||||
at_8000m=Wind(wind_direction_8000m.degrees, base_wind * at_8000m_factor),
|
||||
at_2000m=Wind(
|
||||
wind_direction_2000m.degrees,
|
||||
min(max_supported_wind_speed, base_wind * at_2000m_factor),
|
||||
),
|
||||
at_8000m=Wind(
|
||||
wind_direction_8000m.degrees,
|
||||
min(max_supported_wind_speed, base_wind * at_8000m_factor),
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def random_cloud_base() -> int:
|
||||
return random.randint(2000, 3000)
|
||||
return random.randint(1000, 5000)
|
||||
|
||||
@staticmethod
|
||||
def random_cloud_thickness() -> int:
|
||||
return random.randint(100, 400)
|
||||
# values lower than 400m can generate clear skies in some cases
|
||||
return random.randint(400, 2000)
|
||||
|
||||
@staticmethod
|
||||
def random_pressure(average_pressure: float) -> Pressure:
|
||||
@@ -197,6 +278,42 @@ class Weather:
|
||||
winter_factor = distance_from_peak_summer / day_of_year_peak_summer
|
||||
return interpolate(summer_value, winter_value, winter_factor, clamp=True)
|
||||
|
||||
@staticmethod
|
||||
def interpolate_seasonal_turbulence(
|
||||
high_value: float, low_value: float, day: datetime.date
|
||||
) -> float:
|
||||
day_of_year = day.timetuple().tm_yday
|
||||
day_of_year_peak_summer = 183
|
||||
distance_from_peak_summer = -day_of_year_peak_summer + day_of_year
|
||||
|
||||
amplitude = 0.5 * (high_value - low_value)
|
||||
offset = amplitude + low_value
|
||||
|
||||
# A high peak in summer and winter, between high_value and low_value.
|
||||
return (
|
||||
amplitude * math.cos(4 * math.pi * distance_from_peak_summer / 365.25)
|
||||
+ offset
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def interpolate_solar_activity(
|
||||
time_of_day: TimeOfDay, high: float, low: float
|
||||
) -> float:
|
||||
|
||||
scale: float = 0
|
||||
|
||||
match time_of_day:
|
||||
case TimeOfDay.Dawn:
|
||||
scale = 0.4
|
||||
case TimeOfDay.Day:
|
||||
scale = 1
|
||||
case TimeOfDay.Dusk:
|
||||
scale = 0.6
|
||||
case TimeOfDay.Night:
|
||||
scale = 0
|
||||
|
||||
return interpolate(value1=low, value2=high, factor=scale, clamp=True)
|
||||
|
||||
|
||||
class ClearSkies(Weather):
|
||||
@property
|
||||
@@ -207,6 +324,10 @@ class ClearSkies(Weather):
|
||||
def temperature_adjustment(self) -> float:
|
||||
return 3.0
|
||||
|
||||
@property
|
||||
def turbulence_adjustment(self) -> float:
|
||||
return 0.0
|
||||
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return None
|
||||
|
||||
@@ -226,6 +347,10 @@ class Cloudy(Weather):
|
||||
def temperature_adjustment(self) -> float:
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def turbulence_adjustment(self) -> float:
|
||||
return 0.75
|
||||
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds.random_preset(rain=False)
|
||||
|
||||
@@ -234,7 +359,7 @@ class Cloudy(Weather):
|
||||
return None
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(1, 4)
|
||||
return self.random_wind(1, 5)
|
||||
|
||||
|
||||
class Raining(Weather):
|
||||
@@ -246,6 +371,10 @@ class Raining(Weather):
|
||||
def temperature_adjustment(self) -> float:
|
||||
return -3.0
|
||||
|
||||
@property
|
||||
def turbulence_adjustment(self) -> float:
|
||||
return 1.5
|
||||
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds.random_preset(rain=True)
|
||||
|
||||
@@ -254,7 +383,7 @@ class Raining(Weather):
|
||||
return None
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(1, 6)
|
||||
return self.random_wind(2, 6)
|
||||
|
||||
|
||||
class Thunderstorm(Weather):
|
||||
@@ -266,6 +395,10 @@ class Thunderstorm(Weather):
|
||||
def temperature_adjustment(self) -> float:
|
||||
return -3.0
|
||||
|
||||
@property
|
||||
def turbulence_adjustment(self) -> float:
|
||||
return 3.0
|
||||
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds(
|
||||
base=self.random_cloud_base(),
|
||||
@@ -275,7 +408,7 @@ class Thunderstorm(Weather):
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(1, 8)
|
||||
return self.random_wind(2, 8)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -298,7 +431,7 @@ class Conditions:
|
||||
_start_time = datetime.datetime.combine(day, forced_time)
|
||||
else:
|
||||
_start_time = cls.generate_start_time(
|
||||
theater, day, time_of_day, settings.night_disabled
|
||||
theater, day, time_of_day, settings.night_day_missions
|
||||
)
|
||||
|
||||
return cls(
|
||||
@@ -313,11 +446,11 @@ class Conditions:
|
||||
theater: ConflictTheater,
|
||||
day: datetime.date,
|
||||
time_of_day: TimeOfDay,
|
||||
night_disabled: bool,
|
||||
night_day_missions: NightMissions,
|
||||
) -> datetime.datetime:
|
||||
if night_disabled:
|
||||
from game.theater import DaytimeMap
|
||||
from game.theater import DaytimeMap
|
||||
|
||||
if night_day_missions == NightMissions.OnlyDay:
|
||||
logging.info("Skip Night mission due to user settings")
|
||||
time_range = DaytimeMap(
|
||||
dawn=(datetime.time(hour=8), datetime.time(hour=9)),
|
||||
@@ -325,6 +458,14 @@ class Conditions:
|
||||
dusk=(datetime.time(hour=12), datetime.time(hour=14)),
|
||||
night=(datetime.time(hour=14), datetime.time(hour=17)),
|
||||
).range_of(time_of_day)
|
||||
elif night_day_missions == NightMissions.OnlyNight:
|
||||
logging.info("Skip Day mission due to user settings")
|
||||
time_range = DaytimeMap(
|
||||
dawn=(datetime.time(hour=0), datetime.time(hour=3)),
|
||||
day=(datetime.time(hour=3), datetime.time(hour=6)),
|
||||
dusk=(datetime.time(hour=21), datetime.time(hour=22)),
|
||||
night=(datetime.time(hour=22), datetime.time(hour=23)),
|
||||
).range_of(time_of_day)
|
||||
else:
|
||||
time_range = theater.daytime_map.range_of(time_of_day)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from .jas39 import *
|
||||
from .su30 import *
|
||||
from .su57 import *
|
||||
from .uh60l import *
|
||||
from .ov10a import *
|
||||
|
||||
|
||||
def load_mods() -> None:
|
||||
|
||||
@@ -88,39 +88,49 @@ class WeaponsA4EC:
|
||||
"name": "BDU-33 *6 (MER)",
|
||||
"weight": 167.6,
|
||||
}
|
||||
CBU_1_A = {"clsid": "{CBU-1/A}", "name": "CBU-1/A", "weight": 458.921706}
|
||||
BLU_3B_x_19 = {"clsid": "{BLU_3B_GROUP}", "name": "BLU-3B x 19", "weight": 0.793786}
|
||||
BLU_4B_x_27 = {
|
||||
"clsid": "{BLU_4B_GROUP}",
|
||||
"name": "BLU-4B x 27",
|
||||
"weight": 0.5443104,
|
||||
}
|
||||
CBU_1_A = {"clsid": "{CBU-1/A}", "name": "CBU-1/A", "weight": 337.2910112}
|
||||
CBU_1_A_2 = {"clsid": "{CBU-1/A*2}", "name": "CBU-1/A*2", "weight": 616.5222464}
|
||||
CBU_1_A__2 = {
|
||||
"clsid": "{CBU-1/A_TER_2_L}",
|
||||
"name": "CBU-1/A *2",
|
||||
"weight": 713.473056,
|
||||
"weight": 722.1820224,
|
||||
}
|
||||
CBU_1_A__2_ = {
|
||||
"clsid": "{CBU-1/A_TER_2_R}",
|
||||
"name": "CBU-1/A *2",
|
||||
"weight": 713.473056,
|
||||
"weight": 722.1820224,
|
||||
}
|
||||
CBU_2B_A = {"clsid": "{CBU-2B/A}", "name": "CBU-2B/A", "weight": 379.543106}
|
||||
CBU_2B_A = {"clsid": "{CBU-2B/A}", "name": "CBU-2B/A", "weight": 344.616522}
|
||||
CBU_2B_A_2 = {"clsid": "{CBU-2B/A*2}", "name": "CBU-2B/A*2", "weight": 631.173268}
|
||||
CBU_2B_A__2 = {
|
||||
"clsid": "{CBU-2B/A_TER_2_L}",
|
||||
"name": "CBU-2B/A *2",
|
||||
"weight": 806.686212,
|
||||
"weight": 827.324648,
|
||||
}
|
||||
CBU_2B_A__2_ = {
|
||||
"clsid": "{CBU-2B/A_TER_2_R}",
|
||||
"name": "CBU-2B/A *2",
|
||||
"weight": 806.686212,
|
||||
"weight": 827.324648,
|
||||
}
|
||||
CBU_2_A = {"clsid": "{CBU-2/A}", "name": "CBU-2/A", "weight": 343.822736}
|
||||
CBU_2_A = {"clsid": "{CBU-2/A}", "name": "CBU-2/A", "weight": 344.616522}
|
||||
CBU_2_A_2 = {"clsid": "{CBU-2/A*2}", "name": "CBU-2/A*2", "weight": 631.173268}
|
||||
CBU_2_A__2 = {
|
||||
"clsid": "{CBU-2/A_TER_2_L}",
|
||||
"name": "CBU-2/A *2",
|
||||
"weight": 735.245472,
|
||||
"weight": 736.833044,
|
||||
}
|
||||
CBU_2_A__2_ = {
|
||||
"clsid": "{CBU-2/A_TER_2_R}",
|
||||
"name": "CBU-2/A *2",
|
||||
"weight": 735.245472,
|
||||
"weight": 736.833044,
|
||||
}
|
||||
CBU_DUMMY = {"clsid": "{CBU_DUMMY}", "name": "CBU_DUMMY", "weight": 58.059776}
|
||||
Fuel_Tank_150_gallons = {
|
||||
"clsid": "{DFT-150gal}",
|
||||
"name": "Fuel Tank 150 gallons",
|
||||
|
||||
@@ -297,6 +297,10 @@ class VSN_F100(PlaneType):
|
||||
7,
|
||||
Weapons.LAU_7_with_AIM_9P_Sidewinder_IR_AAM,
|
||||
)
|
||||
LAU_118a_with_AGM_45B_Shrike_ARM__Imp_ = (
|
||||
7,
|
||||
Weapons.LAU_118a_with_AGM_45B_Shrike_ARM__Imp_,
|
||||
)
|
||||
Fuel_tank_500_Liter = (7, WeaponsF100.Fuel_tank_500_Liter)
|
||||
|
||||
# ERRR <CLEAN>
|
||||
|
||||
4752
pydcs_extensions/f16i_idf/f16i_idf.py
Normal file
4752
pydcs_extensions/f16i_idf/f16i_idf.py
Normal file
File diff suppressed because it is too large
Load Diff
174
pydcs_extensions/ov10a/ov10a.py
Normal file
174
pydcs_extensions/ov10a/ov10a.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from typing import Any, Dict, Set
|
||||
|
||||
from dcs import task
|
||||
from dcs.liveries_scanner import Liveries
|
||||
from dcs.planes import PlaneType
|
||||
from dcs.weapons_data import Weapons
|
||||
|
||||
from game.modsupport import planemod
|
||||
from pydcs_extensions.weapon_injector import inject_weapons
|
||||
|
||||
|
||||
class WeaponsOV10A:
|
||||
OV10_SMOKE = {"clsid": "{OV10_SMOKE}", "name": "OV10_SMOKE", "weight": 1}
|
||||
ParaTrooper = {"clsid": "{PARA}", "name": "ParaTrooper", "weight": 80}
|
||||
Fuel_Tank_150_gallons_ = {
|
||||
"clsid": "{150gal}",
|
||||
"name": "Fuel Tank 150 gallons",
|
||||
"weight": 499.5592,
|
||||
}
|
||||
|
||||
|
||||
inject_weapons(WeaponsOV10A)
|
||||
|
||||
|
||||
@planemod
|
||||
class Bronco_OV_10A(PlaneType):
|
||||
id = "Bronco-OV-10A"
|
||||
flyable = True
|
||||
height = 4.62
|
||||
width = 12.9
|
||||
length = 12.76
|
||||
fuel_max = 940
|
||||
max_speed = 684
|
||||
eplrs = True
|
||||
category = "Interceptor" # {78EFB7A2-FD52-4b57-A6A6-3BF0E1D6555F}
|
||||
radio_frequency = 127.5
|
||||
|
||||
panel_radio = {
|
||||
1: {
|
||||
"channels": {6: 41, 2: 31, 8: 50, 3: 32, 1: 30, 4: 33, 5: 40, 7: 42},
|
||||
},
|
||||
}
|
||||
|
||||
livery_name = "BRONCO-OV-10A" # from type
|
||||
Liveries = Liveries()[livery_name]
|
||||
|
||||
class Pylon1:
|
||||
LAU_7_with_AIM_9P_Sidewinder_IR_AAM = (
|
||||
1,
|
||||
Weapons.LAU_7_with_AIM_9P_Sidewinder_IR_AAM,
|
||||
)
|
||||
|
||||
# ERRR {MK-81}
|
||||
|
||||
class Pylon2:
|
||||
Mk_82___500lb_GP_Bomb_LD = (2, Weapons.Mk_82___500lb_GP_Bomb_LD)
|
||||
Mk_82_Snakeye___500lb_GP_Bomb_HD = (2, Weapons.Mk_82_Snakeye___500lb_GP_Bomb_HD)
|
||||
Mk_83___1000lb_GP_Bomb_LD = (2, Weapons.Mk_83___1000lb_GP_Bomb_LD)
|
||||
M117___750lb_GP_Bomb_LD = (2, Weapons.M117___750lb_GP_Bomb_LD)
|
||||
LAU3_WP156 = (2, Weapons.LAU3_WP156)
|
||||
LAU3_WP1B = (2, Weapons.LAU3_WP1B)
|
||||
LAU3_WP61 = (2, Weapons.LAU3_WP61)
|
||||
LAU3_HE5 = (2, Weapons.LAU3_HE5)
|
||||
LAU3_HE151 = (2, Weapons.LAU3_HE151)
|
||||
M260_HYDRA = (2, Weapons.M260_HYDRA)
|
||||
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
2,
|
||||
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
2,
|
||||
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
|
||||
# ERRR {MK-81}
|
||||
|
||||
class Pylon3:
|
||||
Mk_82___500lb_GP_Bomb_LD = (3, Weapons.Mk_82___500lb_GP_Bomb_LD)
|
||||
Mk_82_Snakeye___500lb_GP_Bomb_HD = (3, Weapons.Mk_82_Snakeye___500lb_GP_Bomb_HD)
|
||||
Mk_83___1000lb_GP_Bomb_LD = (3, Weapons.Mk_83___1000lb_GP_Bomb_LD)
|
||||
M117___750lb_GP_Bomb_LD = (3, Weapons.M117___750lb_GP_Bomb_LD)
|
||||
LAU3_WP156 = (3, Weapons.LAU3_WP156)
|
||||
LAU3_WP1B = (3, Weapons.LAU3_WP1B)
|
||||
LAU3_WP61 = (3, Weapons.LAU3_WP61)
|
||||
LAU3_HE5 = (3, Weapons.LAU3_HE5)
|
||||
LAU3_HE151 = (3, Weapons.LAU3_HE151)
|
||||
M260_HYDRA = (3, Weapons.M260_HYDRA)
|
||||
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
3,
|
||||
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
3,
|
||||
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
|
||||
class Pylon4:
|
||||
Fuel_Tank_150_gallons_ = (4, Weapons.Fuel_Tank_150_gallons_)
|
||||
# ERRR {MK-81}
|
||||
Mk_82___500lb_GP_Bomb_LD = (4, Weapons.Mk_82___500lb_GP_Bomb_LD)
|
||||
Mk_82_Snakeye___500lb_GP_Bomb_HD = (4, Weapons.Mk_82_Snakeye___500lb_GP_Bomb_HD)
|
||||
Mk_83___1000lb_GP_Bomb_LD = (4, Weapons.Mk_83___1000lb_GP_Bomb_LD)
|
||||
M117___750lb_GP_Bomb_LD = (4, Weapons.M117___750lb_GP_Bomb_LD)
|
||||
|
||||
# ERRR {MK-81}
|
||||
|
||||
class Pylon5:
|
||||
Mk_82___500lb_GP_Bomb_LD = (5, Weapons.Mk_82___500lb_GP_Bomb_LD)
|
||||
Mk_82_Snakeye___500lb_GP_Bomb_HD = (5, Weapons.Mk_82_Snakeye___500lb_GP_Bomb_HD)
|
||||
Mk_83___1000lb_GP_Bomb_LD = (5, Weapons.Mk_83___1000lb_GP_Bomb_LD)
|
||||
M117___750lb_GP_Bomb_LD = (5, Weapons.M117___750lb_GP_Bomb_LD)
|
||||
LAU3_WP156 = (5, Weapons.LAU3_WP156)
|
||||
LAU3_WP1B = (5, Weapons.LAU3_WP1B)
|
||||
LAU3_WP61 = (5, Weapons.LAU3_WP61)
|
||||
LAU3_HE5 = (5, Weapons.LAU3_HE5)
|
||||
LAU3_HE151 = (5, Weapons.LAU3_HE151)
|
||||
M260_HYDRA = (5, Weapons.M260_HYDRA)
|
||||
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
5,
|
||||
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
5,
|
||||
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
|
||||
# ERRR {MK-81}
|
||||
|
||||
class Pylon6:
|
||||
Mk_82___500lb_GP_Bomb_LD = (6, Weapons.Mk_82___500lb_GP_Bomb_LD)
|
||||
Mk_82_Snakeye___500lb_GP_Bomb_HD = (6, Weapons.Mk_82_Snakeye___500lb_GP_Bomb_HD)
|
||||
Mk_83___1000lb_GP_Bomb_LD = (6, Weapons.Mk_83___1000lb_GP_Bomb_LD)
|
||||
M117___750lb_GP_Bomb_LD = (6, Weapons.M117___750lb_GP_Bomb_LD)
|
||||
LAU3_WP156 = (6, Weapons.LAU3_WP156)
|
||||
LAU3_WP1B = (6, Weapons.LAU3_WP1B)
|
||||
LAU3_WP61 = (6, Weapons.LAU3_WP61)
|
||||
LAU3_HE5 = (6, Weapons.LAU3_HE5)
|
||||
LAU3_HE151 = (6, Weapons.LAU3_HE151)
|
||||
M260_HYDRA = (6, Weapons.M260_HYDRA)
|
||||
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
6,
|
||||
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
6,
|
||||
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
|
||||
class Pylon7:
|
||||
LAU_7_with_AIM_9P_Sidewinder_IR_AAM = (
|
||||
7,
|
||||
Weapons.LAU_7_with_AIM_9P_Sidewinder_IR_AAM,
|
||||
)
|
||||
|
||||
class Pylon8:
|
||||
ParaTrooper = (8, Weapons.ParaTrooper)
|
||||
|
||||
class Pylon9:
|
||||
OV10_SMOKE = (9, Weapons.OV10_SMOKE)
|
||||
|
||||
pylons: Set[int] = {1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
|
||||
tasks = [
|
||||
task.GroundAttack,
|
||||
task.RunwayAttack,
|
||||
task.PinpointStrike,
|
||||
task.CAS,
|
||||
task.AFAC,
|
||||
task.CAP,
|
||||
task.Escort,
|
||||
task.FighterSweep,
|
||||
task.Intercept,
|
||||
]
|
||||
task_default = task.CAS
|
||||
17
pydcs_extensions/pylon_injector.py
Normal file
17
pydcs_extensions/pylon_injector.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from typing import Tuple, Any
|
||||
|
||||
|
||||
def inject_pylon(to_pylon: Any, from_pylon: Any) -> None:
|
||||
"""
|
||||
Inject weapons/ordnance added by mods into the pylons of existing aircraft.
|
||||
This is done to support mods such as the CJS Super Hornet, which modify aircraft
|
||||
that exist in stock DCS. Ornance is injected pydcs aircraft classes via introspection
|
||||
:param to_pylon: The pydcs pylon class of the target aircraft
|
||||
:param from_pylon: The custom pylon class containing tuples with added weapon info
|
||||
:return: None
|
||||
"""
|
||||
for key, value in from_pylon.__dict__.items():
|
||||
if key.startswith("__"):
|
||||
continue
|
||||
if isinstance(value, Tuple):
|
||||
setattr(to_pylon, key, value)
|
||||
@@ -28,6 +28,7 @@ from qt_ui import (
|
||||
liberation_theme,
|
||||
uiconstants,
|
||||
)
|
||||
from qt_ui.uiflags import UiFlags
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.QLiberationWindow import QLiberationWindow
|
||||
from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
|
||||
@@ -56,12 +57,24 @@ def inject_custom_payloads(user_path: Path) -> None:
|
||||
PayloadDirectories.set_preferred(user_path / "MissionEditor" / "UnitPayloads")
|
||||
|
||||
|
||||
def inject_mod_payloads(mod_path: Path) -> None:
|
||||
if mod_path.exists():
|
||||
payloads = mod_path
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Could not find mod payloads at {mod_path}."
|
||||
f"Aircraft will have no payloads."
|
||||
)
|
||||
# We configure these as preferred so the mod's loadouts override the stock ones.
|
||||
PayloadDirectories.set_preferred(payloads)
|
||||
|
||||
|
||||
def on_game_load(game: Game | None) -> None:
|
||||
EventStream.drain()
|
||||
EventStream.put_nowait(GameUpdateEvents().game_loaded(game))
|
||||
|
||||
|
||||
def run_ui(game: Game | None, dev: bool) -> None:
|
||||
def run_ui(game: Game | None, ui_flags: UiFlags) -> None:
|
||||
os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" # Potential fix for 4K screens
|
||||
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
||||
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
||||
@@ -153,7 +166,7 @@ def run_ui(game: Game | None, dev: bool) -> None:
|
||||
GameUpdateSignal.get_instance().game_loaded.connect(on_game_load)
|
||||
|
||||
# Start window
|
||||
window = QLiberationWindow(game, dev)
|
||||
window = QLiberationWindow(game, ui_flags)
|
||||
window.showMaximized()
|
||||
splash.finish(window)
|
||||
qt_execution_code = app.exec_()
|
||||
@@ -183,6 +196,19 @@ def parse_args() -> argparse.Namespace:
|
||||
|
||||
parser.add_argument("--dev", action="store_true", help="Enable development mode.")
|
||||
|
||||
speed_controls_group = parser.add_argument_group()
|
||||
speed_controls_group.add_argument(
|
||||
"--show-sim-speed-controls",
|
||||
action="store_true",
|
||||
help="Shows the sim speed controls in the top panel.",
|
||||
)
|
||||
speed_controls_group.add_argument(
|
||||
"--no-show-sim-speed-controls",
|
||||
dest="show_sim_speed_controls",
|
||||
action="store_false",
|
||||
help="Hides the sim speed controls in the top panel (default).",
|
||||
)
|
||||
|
||||
parser.add_argument("--new-map", help="Deprecated. Does nothing.")
|
||||
parser.add_argument("--old-map", help="Deprecated. Does nothing.")
|
||||
|
||||
@@ -373,7 +399,7 @@ def main():
|
||||
return
|
||||
|
||||
with Server().run_in_thread():
|
||||
run_ui(game, args.dev)
|
||||
run_ui(game, UiFlags(args.dev, args.show_sim_speed_controls))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -205,6 +205,7 @@ def load_aircraft_icons():
|
||||
os.path.join("./resources/ui/units/aircrafts/icons/", aircraft)
|
||||
)
|
||||
AIRCRAFT_ICONS["F-16C_50"] = AIRCRAFT_ICONS["F-16C"]
|
||||
AIRCRAFT_ICONS["F-16A_MLU"] = AIRCRAFT_ICONS["F-16A"]
|
||||
AIRCRAFT_ICONS["FA-18C_hornet"] = AIRCRAFT_ICONS["FA-18C"]
|
||||
AIRCRAFT_ICONS["A-10C_2"] = AIRCRAFT_ICONS["A-10C"]
|
||||
f1_refuel = ["Mirage-F1CT", "Mirage-F1EE", "Mirage-F1M-EE", "Mirage-F1EQ"]
|
||||
|
||||
17
qt_ui/uiflags.py
Normal file
17
qt_ui/uiflags.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UiFlags:
|
||||
"""Flags for the UI that are not exposed in settings.
|
||||
|
||||
These flags are hidden from the settings UI because they can only be controlled from
|
||||
the command-line during startup.
|
||||
"""
|
||||
|
||||
# True if the front-end should connect to the development react webserver instead of
|
||||
# the built front-end app.
|
||||
dev_ui_webserver: bool
|
||||
|
||||
# True if the play/pause/speed controls should be visible in the top panel.
|
||||
show_sim_speed_controls: bool
|
||||
@@ -17,6 +17,7 @@ from game.profiling import logged_duration
|
||||
from game.utils import meters
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.simcontroller import SimController
|
||||
from qt_ui.uiflags import UiFlags
|
||||
from qt_ui.widgets.QBudgetBox import QBudgetBox
|
||||
from qt_ui.widgets.QConditionsWidget import QConditionsWidget
|
||||
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
|
||||
@@ -30,7 +31,9 @@ from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResul
|
||||
|
||||
|
||||
class QTopPanel(QFrame):
|
||||
def __init__(self, game_model: GameModel, sim_controller: SimController) -> None:
|
||||
def __init__(
|
||||
self, game_model: GameModel, sim_controller: SimController, ui_flags: UiFlags
|
||||
) -> None:
|
||||
super(QTopPanel, self).__init__()
|
||||
self.game_model = game_model
|
||||
self.sim_controller = sim_controller
|
||||
@@ -82,7 +85,8 @@ class QTopPanel(QFrame):
|
||||
|
||||
self.proceedBox = QGroupBox("Proceed")
|
||||
self.proceedBoxLayout = QHBoxLayout()
|
||||
self.proceedBoxLayout.addLayout(self.simSpeedControls)
|
||||
if ui_flags.show_sim_speed_controls:
|
||||
self.proceedBoxLayout.addLayout(self.simSpeedControls)
|
||||
self.proceedBoxLayout.addLayout(MaxPlayerCount(self.game_model.ato_model))
|
||||
self.proceedBoxLayout.addWidget(self.passTurnButton)
|
||||
self.proceedBoxLayout.addWidget(self.proceedButton)
|
||||
|
||||
@@ -7,7 +7,6 @@ from game.missiongenerator.frontlineconflictdescription import (
|
||||
FrontLineConflictDescription,
|
||||
)
|
||||
from game.theater.controlpoint import ControlPointType
|
||||
from game.theater.theatergroundobject import BuildingGroundObject, IadsGroundObject
|
||||
from game.utils import Distance
|
||||
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
|
||||
|
||||
@@ -69,7 +68,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
if self.include_frontlines:
|
||||
for front_line in self.game.theater.conflicts():
|
||||
pos = FrontLineConflictDescription.frontline_position(
|
||||
front_line, self.game.theater
|
||||
front_line, self.game.theater, self.game.settings
|
||||
)[0]
|
||||
wptname = f"Frontline {front_line.name} [CAS]"
|
||||
wpt = FlightWaypoint(
|
||||
@@ -81,66 +80,25 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
wpt.description = "Frontline"
|
||||
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
||||
|
||||
if self.include_targets:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if (self.include_enemy and not cp.captured) or (
|
||||
self.include_friendly and cp.captured
|
||||
):
|
||||
for ground_object in cp.ground_objects:
|
||||
if not ground_object.is_dead and isinstance(
|
||||
ground_object, BuildingGroundObject
|
||||
):
|
||||
wpt = FlightWaypoint(
|
||||
ground_object.waypoint_name,
|
||||
FlightWaypointType.CUSTOM,
|
||||
ground_object.position,
|
||||
Distance.from_meters(0),
|
||||
)
|
||||
wpt.alt_type = "RADIO"
|
||||
wpt.pretty_name = wpt.name
|
||||
wpt.obj_name = ground_object.obj_name
|
||||
wpt.targets.append(ground_object)
|
||||
if cp.captured:
|
||||
wpt.description = "Friendly Building"
|
||||
else:
|
||||
wpt.description = "Enemy Building"
|
||||
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
||||
|
||||
if self.include_units:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if (self.include_enemy and not cp.captured) or (
|
||||
self.include_friendly and cp.captured
|
||||
):
|
||||
for ground_object in cp.ground_objects:
|
||||
if not ground_object.is_dead and (
|
||||
isinstance(ground_object, IadsGroundObject)
|
||||
):
|
||||
for g in ground_object.groups:
|
||||
for j, u in enumerate(g.units):
|
||||
wptname = (
|
||||
"["
|
||||
+ str(ground_object.obj_name)
|
||||
+ "] : "
|
||||
+ u.name
|
||||
+ " #"
|
||||
+ str(j)
|
||||
)
|
||||
wpt = FlightWaypoint(
|
||||
wptname,
|
||||
FlightWaypointType.CUSTOM,
|
||||
u.position,
|
||||
Distance.from_meters(0),
|
||||
)
|
||||
wpt.alt_type = "RADIO"
|
||||
wpt.pretty_name = wptname
|
||||
wpt.targets.append(u)
|
||||
wpt.obj_name = ground_object.obj_name
|
||||
wpt.waypoint_type = FlightWaypointType.CUSTOM
|
||||
if cp.captured:
|
||||
wpt.description = "Friendly unit: " + u.name
|
||||
else:
|
||||
wpt.description = "Enemy unit: " + u.name
|
||||
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
||||
for tgo in self.game.theater.ground_objects:
|
||||
for target_idx, target in enumerate(tgo.strike_targets):
|
||||
wptname = f"[{tgo.obj_name}] : {target.name} #{target_idx}"
|
||||
wpt = FlightWaypoint(
|
||||
wptname,
|
||||
FlightWaypointType.CUSTOM,
|
||||
target.position,
|
||||
Distance.from_meters(0),
|
||||
)
|
||||
wpt.alt_type = "RADIO"
|
||||
wpt.pretty_name = wptname
|
||||
wpt.targets.append(target)
|
||||
wpt.obj_name = tgo.obj_name
|
||||
wpt.waypoint_type = FlightWaypointType.CUSTOM
|
||||
if tgo.is_friendly(to_player=True):
|
||||
wpt.description = f"Friendly unit: {target.name}"
|
||||
else:
|
||||
wpt.description = f"Enemy unit: {target.name}"
|
||||
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
||||
|
||||
if self.include_airbases:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||
|
||||
from game import Game
|
||||
from game.theater import SamGroundObject
|
||||
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
|
||||
|
||||
|
||||
class StrikeTargetInfo:
|
||||
def __init__(self):
|
||||
self.name = ""
|
||||
self.location = None
|
||||
self.units = []
|
||||
self.buildings = []
|
||||
|
||||
|
||||
class QStrikeTargetSelectionComboBox(QFilteredComboBox):
|
||||
def __init__(self, game: Game, parent=None):
|
||||
super(QStrikeTargetSelectionComboBox, self).__init__(parent)
|
||||
self.game = game
|
||||
self.find_possible_strike_targets()
|
||||
|
||||
for t in self.targets:
|
||||
print(t.name + " - " + str(len(t.units)) + " " + str(len(t.buildings)))
|
||||
|
||||
def get_selected_target(self) -> StrikeTargetInfo:
|
||||
n = self.currentText()
|
||||
for target in self.targets:
|
||||
if target.name == n:
|
||||
return target
|
||||
|
||||
def find_possible_strike_targets(self):
|
||||
|
||||
self.targets = []
|
||||
i = 0
|
||||
model = QStandardItemModel()
|
||||
|
||||
def add_model_item(i, model, target):
|
||||
item = QStandardItem(target.name)
|
||||
model.setItem(i, 0, item)
|
||||
self.targets.append(target)
|
||||
return i + 1
|
||||
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.captured:
|
||||
continue
|
||||
|
||||
added_obj_names = []
|
||||
|
||||
for g in cp.ground_objects:
|
||||
if g.obj_name in added_obj_names:
|
||||
continue
|
||||
|
||||
target = StrikeTargetInfo()
|
||||
target.location = g
|
||||
target.name = g.obj_name
|
||||
|
||||
if isinstance(g, SamGroundObject):
|
||||
target.name = g.obj_name + " [units]"
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
target.units.append(u)
|
||||
else:
|
||||
target.name = g.obj_name + " [" + g.category + "]"
|
||||
for g2 in cp.ground_objects:
|
||||
if g2 is not g and g2.obj_name == g.obj_name:
|
||||
target.buildings.append(g2)
|
||||
|
||||
i = add_model_item(i, model, target)
|
||||
added_obj_names.append(g.obj_name)
|
||||
|
||||
self.setModel(model)
|
||||
@@ -1,69 +0,0 @@
|
||||
from PySide2.QtGui import QStandardItemModel, QStandardItem
|
||||
from PySide2.QtWidgets import (
|
||||
QGroupBox,
|
||||
QVBoxLayout,
|
||||
QListView,
|
||||
QAbstractItemView,
|
||||
)
|
||||
|
||||
from qt_ui.widgets.combos.QStrikeTargetSelectionComboBox import StrikeTargetInfo
|
||||
|
||||
|
||||
class QStrikeTargetInfoView(QGroupBox):
|
||||
"""
|
||||
UI Component to display info about a strike target
|
||||
"""
|
||||
|
||||
def __init__(self, strike_target_infos: StrikeTargetInfo):
|
||||
|
||||
if strike_target_infos is None:
|
||||
strike_target_infos = StrikeTargetInfo()
|
||||
|
||||
super(QStrikeTargetInfoView, self).__init__(
|
||||
"Target : " + strike_target_infos.name
|
||||
)
|
||||
|
||||
self.strike_target_infos = strike_target_infos
|
||||
|
||||
self.listView = QListView()
|
||||
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.addWidget(self.listView)
|
||||
self.setLayout(layout)
|
||||
|
||||
def setTarget(self, target):
|
||||
|
||||
self.setTitle(target.name)
|
||||
self.strike_target_infos = target
|
||||
model = QStandardItemModel()
|
||||
self.listView.setSelectionMode(QAbstractItemView.NoSelection)
|
||||
|
||||
if len(self.strike_target_infos.units) > 0:
|
||||
dic = {}
|
||||
for u in self.strike_target_infos.units:
|
||||
if u.type.id in dic.keys():
|
||||
dic[u.type.id] = dic[u.type.id] + 1
|
||||
else:
|
||||
dic[u.type.id] = 1
|
||||
for k, v in dic.items():
|
||||
model.appendRow(QStandardItem(k + " x " + str(v)))
|
||||
print(k + " x " + str(v))
|
||||
else:
|
||||
dic = {}
|
||||
for b in self.strike_target_infos.buildings:
|
||||
id = b.dcs_identifier
|
||||
if b.is_dead:
|
||||
id = id + "[Destroyed]"
|
||||
if id in dic.keys():
|
||||
dic[id] = dic[id] + 1
|
||||
else:
|
||||
dic[id] = 1
|
||||
for k, v in dic.items():
|
||||
model.appendRow(QStandardItem(k + " x " + str(v)))
|
||||
print(k + " x " + str(v))
|
||||
|
||||
self.listView.setModel(model)
|
||||
@@ -161,6 +161,14 @@ class SquadronLiverySelector(QComboBox):
|
||||
valid_livery = livery.countries is None or cc in livery.countries
|
||||
if valid_livery or cc in ["BLUE", "RED"]:
|
||||
liveries.add(livery)
|
||||
faction = squadron.coalition.faction
|
||||
overrides = [
|
||||
x
|
||||
for x in faction.liveries_overrides.get(self.aircraft_type, [])
|
||||
if x in [y.id.lower() for y in liveries]
|
||||
]
|
||||
if len(overrides) > 0:
|
||||
self.addItem("Use livery overrides", userData=None)
|
||||
for livery in sorted(liveries):
|
||||
self.addItem(livery.name, userData=livery.id)
|
||||
if selected_livery is not None:
|
||||
|
||||
@@ -8,7 +8,9 @@ from PySide2.QtWidgets import (
|
||||
QGroupBox,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from game.debriefing import Debriefing
|
||||
@@ -52,6 +54,19 @@ class LossGrid(QGridLayout):
|
||||
self.addWidget(QLabel(str(count)), row, 1)
|
||||
|
||||
|
||||
class ScrollingCasualtyReportContainer(QGroupBox):
|
||||
def __init__(self, debriefing: Debriefing, player: bool) -> None:
|
||||
country = debriefing.player_country if player else debriefing.enemy_country
|
||||
super().__init__(f"{country}'s lost units:")
|
||||
scroll_content = QWidget()
|
||||
scroll_content.setLayout(LossGrid(debriefing, player))
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidget(scroll_content)
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(scroll_area)
|
||||
self.setLayout(layout)
|
||||
|
||||
|
||||
class QDebriefingWindow(QDialog):
|
||||
def __init__(self, debriefing: Debriefing):
|
||||
super(QDebriefingWindow, self).__init__()
|
||||
@@ -75,12 +90,10 @@ class QDebriefingWindow(QDialog):
|
||||
title = QLabel("<b>Casualty report</b>")
|
||||
layout.addWidget(title)
|
||||
|
||||
player_lost_units = QGroupBox(f"{self.debriefing.player_country}'s lost units:")
|
||||
player_lost_units.setLayout(LossGrid(debriefing, player=True))
|
||||
player_lost_units = ScrollingCasualtyReportContainer(debriefing, player=True)
|
||||
layout.addWidget(player_lost_units)
|
||||
|
||||
enemy_lost_units = QGroupBox(f"{self.debriefing.enemy_country}'s lost units:")
|
||||
enemy_lost_units.setLayout(LossGrid(debriefing, player=False))
|
||||
enemy_lost_units = ScrollingCasualtyReportContainer(debriefing, player=False)
|
||||
layout.addWidget(enemy_lost_units)
|
||||
|
||||
okay = QPushButton("Okay")
|
||||
|
||||
@@ -19,7 +19,7 @@ from PySide2.QtWidgets import (
|
||||
)
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game, VERSION, persistency
|
||||
from game import Game, VERSION, persistency, Migrator
|
||||
from game.debriefing import Debriefing
|
||||
from game.game import TurnState
|
||||
from game.layout import LAYOUTS
|
||||
@@ -31,6 +31,7 @@ from qt_ui.dialogs import Dialog
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.simcontroller import SimController
|
||||
from qt_ui.uiconstants import URLS
|
||||
from qt_ui.uiflags import UiFlags
|
||||
from qt_ui.uncaughtexceptionhandler import UncaughtExceptionHandler
|
||||
from qt_ui.widgets.QTopPanel import QTopPanel
|
||||
from qt_ui.widgets.ato import QAirTaskingOrderPanel
|
||||
@@ -55,7 +56,7 @@ class QLiberationWindow(QMainWindow):
|
||||
tgo_info_signal = Signal(TheaterGroundObject)
|
||||
control_point_info_signal = Signal(ControlPoint)
|
||||
|
||||
def __init__(self, game: Game | None, dev: bool) -> None:
|
||||
def __init__(self, game: Game | None, ui_flags: UiFlags) -> None:
|
||||
super().__init__()
|
||||
|
||||
self._uncaught_exception_handler = UncaughtExceptionHandler(self)
|
||||
@@ -80,14 +81,16 @@ class QLiberationWindow(QMainWindow):
|
||||
Dialog.set_game(self.game_model)
|
||||
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
|
||||
self.info_panel = QInfoPanel(self.game)
|
||||
self.liberation_map = QLiberationMap(self.game_model, dev, self)
|
||||
self.liberation_map = QLiberationMap(
|
||||
self.game_model, ui_flags.dev_ui_webserver, self
|
||||
)
|
||||
|
||||
self.setGeometry(300, 100, 270, 100)
|
||||
self.updateWindowTitle()
|
||||
self.setWindowIcon(QIcon("./resources/icon.png"))
|
||||
self.statusBar().showMessage("Ready")
|
||||
|
||||
self.initUi()
|
||||
self.initUi(ui_flags)
|
||||
self.initActions()
|
||||
self.initToolbar()
|
||||
self.initMenuBar()
|
||||
@@ -108,6 +111,7 @@ class QLiberationWindow(QMainWindow):
|
||||
try:
|
||||
logging.info("Loading last saved game : " + str(last_save_file))
|
||||
game = persistency.load_game(last_save_file)
|
||||
Migrator(game)
|
||||
self.onGameGenerated(game)
|
||||
self.updateWindowTitle(last_save_file if game else None)
|
||||
except:
|
||||
@@ -117,7 +121,7 @@ class QLiberationWindow(QMainWindow):
|
||||
else:
|
||||
self.onGameGenerated(self.game)
|
||||
|
||||
def initUi(self):
|
||||
def initUi(self, ui_flags: UiFlags) -> None:
|
||||
hbox = QSplitter(Qt.Horizontal)
|
||||
vbox = QSplitter(Qt.Vertical)
|
||||
hbox.addWidget(self.ato_panel)
|
||||
@@ -130,7 +134,7 @@ class QLiberationWindow(QMainWindow):
|
||||
hbox.setSizes([1, 10000000])
|
||||
vbox.setSizes([600, 100])
|
||||
|
||||
self.top_panel = QTopPanel(self.game_model, self.sim_controller)
|
||||
self.top_panel = QTopPanel(self.game_model, self.sim_controller, ui_flags)
|
||||
vbox = QVBoxLayout()
|
||||
vbox.setMargin(0)
|
||||
vbox.addWidget(self.top_panel)
|
||||
@@ -317,6 +321,7 @@ class QLiberationWindow(QMainWindow):
|
||||
)
|
||||
if file is not None and file[0] != "":
|
||||
game = persistency.load_game(file[0])
|
||||
Migrator(game)
|
||||
GameUpdateSignal.get_instance().game_loaded.emit(game)
|
||||
|
||||
self.updateWindowTitle(file[0])
|
||||
@@ -482,6 +487,10 @@ class QLiberationWindow(QMainWindow):
|
||||
"<b>Ciribob </b> <i>for the JTACAutoLase.lua script</i><br/>"
|
||||
"<b>Walder </b> <i>for the Skynet-IADS script</i><br/>"
|
||||
"<b>Anubis Yinepu </b> <i>for the Hercules Cargo script</i><br/>"
|
||||
'<a href="https://www.flaticon.com/free-icons/bug" title="bug icons" style="color: #ffffff">Bug icons created by Freepik - Flaticon</a><br />'
|
||||
'Contains information from <a href="https://osmdata.openstreetmap.de/" style="color: #ffffff">OpenStreetMap © OpenStreetMap contributors</a>, which is made available here under the <a href="https://opendatacommons.org/licenses/odbl/1-0/" style="color: #ffffff">Open Database License (ODbL)</a>.<br />'
|
||||
'<a href="https://download.geofabrik.de/index.html/" style="color: #ffffff">OpenStreetMap Data Extracts from Geofabrik</a><br />'
|
||||
'<a href="https://www.earthdata.nasa.gov/" style="color: #ffffff">NASA EarthData</a><br />'
|
||||
+ "<h4>Splash Screen :</h4>"
|
||||
+ "Artwork by Andriy Dankovych (CC BY-SA)"
|
||||
" <a href='https://www.facebook.com/AndriyDankovych' style='color:white'>"
|
||||
@@ -546,10 +555,13 @@ class QLiberationWindow(QMainWindow):
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"Quit Retribution?",
|
||||
"Are you sure you want to quit? All unsaved progress will be lost.",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
"Would you like to save before quitting?",
|
||||
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,
|
||||
QMessageBox.Cancel,
|
||||
)
|
||||
if result == QMessageBox.Yes:
|
||||
if result in [QMessageBox.Yes, QMessageBox.No]:
|
||||
if result == QMessageBox.Yes:
|
||||
self.saveGame()
|
||||
self._save_window_geometry()
|
||||
super().closeEvent(event)
|
||||
self.dialog = None
|
||||
|
||||
@@ -238,5 +238,5 @@ class QWaitingForMissionResultWindow(QDialog):
|
||||
# Stop the current waiting thread as we manually submit the results
|
||||
self.wait_thread.stop()
|
||||
self.on_debriefing_update(
|
||||
self.sim_controller.debrief_current_state(Path(file[0], force_end=True))
|
||||
self.sim_controller.debrief_current_state(Path(file[0]), force_end=True)
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from PySide2.QtWidgets import (
|
||||
|
||||
from game import Game
|
||||
from game.ato.flight import Flight
|
||||
from game.ato.flightplans.custom import CustomFlightPlan, CustomLayout
|
||||
from game.ato.flightplans.custom import CustomFlightPlan
|
||||
from game.ato.flightplans.formationattack import FormationAttackFlightPlan
|
||||
from game.ato.flightplans.planningerror import PlanningError
|
||||
from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
||||
@@ -147,15 +147,7 @@ class QFlightWaypointTab(QFrame):
|
||||
|
||||
def degrade_to_custom_flight_plan(self) -> None:
|
||||
if not isinstance(self.flight.flight_plan, CustomFlightPlan):
|
||||
self.flight.flight_plan = CustomFlightPlan(
|
||||
self.flight,
|
||||
CustomLayout(
|
||||
departure=WaypointBuilder(self.flight, self.coalition).takeoff(
|
||||
self.flight.departure
|
||||
),
|
||||
custom_waypoints=self.flight.flight_plan.waypoints[1:],
|
||||
),
|
||||
)
|
||||
self.flight.degrade_to_custom_flight_plan()
|
||||
|
||||
def confirm_recreate(self, task: FlightType) -> None:
|
||||
result = QMessageBox.question(
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import List
|
||||
|
||||
from PySide2 import QtGui, QtWidgets
|
||||
from PySide2.QtCore import QDate, QItemSelectionModel, QPoint, Qt, Signal
|
||||
from PySide2.QtWidgets import QCheckBox, QLabel, QTextEdit, QVBoxLayout
|
||||
from PySide2.QtWidgets import QCheckBox, QLabel, QTextEdit, QVBoxLayout, QTextBrowser
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
from game.campaignloader.campaign import Campaign, DEFAULT_BUDGET
|
||||
@@ -142,6 +142,7 @@ class NewGameWizard(QtWidgets.QWizard):
|
||||
),
|
||||
automate_aircraft_reinforcements=self.field("automate_aircraft_purchases"),
|
||||
supercarrier=self.field("supercarrier"),
|
||||
max_frontline_length=campaign.data.get("max_frontline_length", 80),
|
||||
)
|
||||
generator_settings = GeneratorSettings(
|
||||
start_date=start_date,
|
||||
@@ -162,6 +163,7 @@ class NewGameWizard(QtWidgets.QWizard):
|
||||
a6a_intruder=self.field("a6a_intruder"),
|
||||
f4b_phantom=self.field("f4b_phantom"),
|
||||
f15d_baz=self.field("f15d_baz"),
|
||||
f_16_idf=self.field("f_16_idf"),
|
||||
f22_raptor=self.field("f22_raptor"),
|
||||
f84g_thunderjet=self.field("f84g_thunderjet"),
|
||||
f100_supersabre=self.field("f100_supersabre"),
|
||||
@@ -172,6 +174,7 @@ class NewGameWizard(QtWidgets.QWizard):
|
||||
jas39_gripen=self.field("jas39_gripen"),
|
||||
su30_flanker_h=self.field("su30_flanker_h"),
|
||||
su57_felon=self.field("su57_felon"),
|
||||
ov10a_bronco=self.field("ov10a_bronco"),
|
||||
frenchpack=self.field("frenchpack"),
|
||||
high_digit_sams=self.field("high_digit_sams"),
|
||||
)
|
||||
@@ -257,11 +260,13 @@ class FactionSelection(QtWidgets.QWizardPage):
|
||||
redFaction.setBuddy(self.redFactionSelect)
|
||||
|
||||
# Faction description
|
||||
self.blueFactionDescription = QTextEdit("")
|
||||
self.blueFactionDescription = QTextBrowser()
|
||||
self.blueFactionDescription.setReadOnly(True)
|
||||
self.blueFactionDescription.setOpenExternalLinks(True)
|
||||
|
||||
self.redFactionDescription = QTextEdit("")
|
||||
self.redFactionDescription = QTextBrowser()
|
||||
self.redFactionDescription.setReadOnly(True)
|
||||
self.redFactionDescription.setOpenExternalLinks(True)
|
||||
|
||||
# Setup default selected factions
|
||||
for i, r in enumerate(FACTIONS):
|
||||
@@ -383,8 +388,9 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
|
||||
self.registerField("selectedCampaign", self.campaignList)
|
||||
|
||||
# Faction description
|
||||
self.campaignMapDescription = QTextEdit("")
|
||||
self.campaignMapDescription = QTextBrowser()
|
||||
self.campaignMapDescription.setReadOnly(True)
|
||||
self.campaignMapDescription.setOpenExternalLinks(True)
|
||||
self.campaignMapDescription.setMaximumHeight(200)
|
||||
|
||||
self.performanceText = QTextEdit("")
|
||||
@@ -668,6 +674,8 @@ class GeneratorOptions(QtWidgets.QWizardPage):
|
||||
self.registerField("f4b_phantom", f4b_phantom)
|
||||
f15d_baz = QtWidgets.QCheckBox()
|
||||
self.registerField("f15d_baz", f15d_baz)
|
||||
f_16_idf = QtWidgets.QCheckBox()
|
||||
self.registerField("f_16_idf", f_16_idf)
|
||||
f22_raptor = QtWidgets.QCheckBox()
|
||||
self.registerField("f22_raptor", f22_raptor)
|
||||
f84g_thunderjet = QtWidgets.QCheckBox()
|
||||
@@ -684,6 +692,8 @@ class GeneratorOptions(QtWidgets.QWizardPage):
|
||||
self.registerField("su30_flanker_h", su30_flanker_h)
|
||||
su57_felon = QtWidgets.QCheckBox()
|
||||
self.registerField("su57_felon", su57_felon)
|
||||
ov10a_bronco = QtWidgets.QCheckBox()
|
||||
self.registerField("ov10a_bronco", ov10a_bronco)
|
||||
frenchpack = QtWidgets.QCheckBox()
|
||||
self.registerField("frenchpack", frenchpack)
|
||||
high_digit_sams = QtWidgets.QCheckBox()
|
||||
@@ -696,93 +706,41 @@ class GeneratorOptions(QtWidgets.QWizardPage):
|
||||
|
||||
modLayout = QtWidgets.QGridLayout()
|
||||
modLayout_row = 1
|
||||
modLayout.addWidget(
|
||||
QtWidgets.QLabel("A-4E Skyhawk (version 2.0.0)"), modLayout_row, 0
|
||||
)
|
||||
modLayout.addWidget(a4_skyhawk, modLayout_row, 1)
|
||||
modLayout_row += 1
|
||||
modLayout.addWidget(
|
||||
QtWidgets.QLabel("A-6A Intruder (v2.7.5.01)"), modLayout_row, 0
|
||||
)
|
||||
modLayout.addWidget(a6a_intruder, modLayout_row, 1)
|
||||
modLayout_row += 1
|
||||
modLayout.addWidget(
|
||||
QtWidgets.QLabel("F-4B Phantom II (v2.7.10.02 EFM + Patch 2022.10.02)"),
|
||||
modLayout_row,
|
||||
0,
|
||||
)
|
||||
modLayout.addWidget(f4b_phantom, modLayout_row, 1)
|
||||
modLayout_row += 1
|
||||
modLayout.addWidget(QtWidgets.QLabel("F-15D Baz (v1.0)"), modLayout_row, 0)
|
||||
modLayout.addWidget(f15d_baz, modLayout_row, 1)
|
||||
modLayout_row += 1
|
||||
modLayout.addWidget(QtWidgets.QLabel("F-22A Raptor"), modLayout_row, 0)
|
||||
modLayout.addWidget(f22_raptor, modLayout_row, 1)
|
||||
modLayout_row += 1
|
||||
# Section break here for readability
|
||||
modLayout.addWidget(QtWidgets.QWidget(), modLayout_row, 0)
|
||||
modLayout_row += 1
|
||||
modLayout.addWidget(
|
||||
QtWidgets.QLabel("F-84G Thunderjet (version 2.5.7.01)"),
|
||||
modLayout_row,
|
||||
0,
|
||||
)
|
||||
modLayout.addWidget(f84g_thunderjet, modLayout_row, 1)
|
||||
modLayout_row += 1
|
||||
modLayout.addWidget(
|
||||
QtWidgets.QLabel("F-100 Super Sabre (version 2.7.18.30765 patch 09.10.22)"),
|
||||
modLayout_row,
|
||||
0,
|
||||
)
|
||||
modLayout.addWidget(f100_supersabre, modLayout_row, 1)
|
||||
modLayout_row += 1
|
||||
modLayout.addWidget(
|
||||
QtWidgets.QLabel("F-104 Starfighter (version 2.7.11.222.01)"),
|
||||
modLayout_row,
|
||||
0,
|
||||
)
|
||||
modLayout.addWidget(f104_starfighter, modLayout_row, 1)
|
||||
modLayout_row += 1
|
||||
modLayout.addWidget(
|
||||
QtWidgets.QLabel("F-105 Thunderchief (version 2.7.12.23x)"),
|
||||
modLayout_row,
|
||||
0,
|
||||
)
|
||||
modLayout.addWidget(f105_thunderchief, modLayout_row, 1)
|
||||
modLayout_row += 1
|
||||
modLayout.addWidget(
|
||||
QtWidgets.QLabel("C-130J-30 Super Hercules"), modLayout_row, 0
|
||||
)
|
||||
modLayout.addWidget(hercules, modLayout_row, 1)
|
||||
modLayout_row += 1
|
||||
# Section break here for readability
|
||||
modLayout.addWidget(QtWidgets.QWidget(), modLayout_row, 0)
|
||||
modLayout_row += 1
|
||||
modLayout.addWidget(
|
||||
QtWidgets.QLabel("UH-60L Black Hawk (version 1.3.1)"), modLayout_row, 0
|
||||
)
|
||||
modLayout.addWidget(uh_60l, modLayout_row, 1)
|
||||
modLayout_row += 1
|
||||
modLayout.addWidget(
|
||||
QtWidgets.QLabel("JAS 39 Gripen (version v1.8.0-beta)"), modLayout_row, 0
|
||||
)
|
||||
modLayout.addWidget(jas39_gripen, modLayout_row, 1)
|
||||
modLayout_row += 1
|
||||
modLayout.addWidget(
|
||||
QtWidgets.QLabel("Su-30 Flanker-H (V2.01B)"), modLayout_row, 0
|
||||
)
|
||||
modLayout.addWidget(su30_flanker_h, modLayout_row, 1)
|
||||
modLayout_row += 1
|
||||
modLayout.addWidget(QtWidgets.QLabel("Su-57 Felon"), modLayout_row, 0)
|
||||
modLayout.addWidget(su57_felon, modLayout_row, 1)
|
||||
modLayout_row += 1
|
||||
modLayout.addWidget(QtWidgets.QLabel("Frenchpack"), modLayout_row, 0)
|
||||
modLayout.addWidget(frenchpack, modLayout_row, 1)
|
||||
|
||||
# new column starts here
|
||||
modLayout_row = 1
|
||||
modLayout.addWidget(QtWidgets.QLabel("High Digit SAMs"), modLayout_row, 2)
|
||||
modLayout.addWidget(high_digit_sams, modLayout_row, 3)
|
||||
mod_pairs = [
|
||||
("A-4E Skyhawk (v2.1.0)", a4_skyhawk),
|
||||
("A-6A Intruder (v2.7.5.01)", a6a_intruder),
|
||||
("C-130J-30 Super Hercules", hercules),
|
||||
("F-4B Phantom II (v2.7.10.02 EFM + Patch 2022.10.02)", f4b_phantom),
|
||||
("F-15D Baz (v1.0)", f15d_baz),
|
||||
("F-16I Sufa & F-16D (v2.2 by IDF Mods Project)", f_16_idf),
|
||||
("F-22A Raptor", f22_raptor),
|
||||
("F-84G Thunderjet (v2.5.7.01)", f84g_thunderjet),
|
||||
("F-100 Super Sabre (v2.7.18.30765 patch 20.10.22)", f100_supersabre),
|
||||
("F-104 Starfighter (v2.7.11.222.01)", f104_starfighter),
|
||||
("F-105 Thunderchief (v2.7.12.23x)", f105_thunderchief),
|
||||
("Frenchpack", frenchpack),
|
||||
("High Digit SAMs", high_digit_sams),
|
||||
("JAS 39 Gripen (v1.8.0-beta)", jas39_gripen),
|
||||
("OV-10A Bronco", ov10a_bronco),
|
||||
("Su-30 Flanker-H (V2.01B)", su30_flanker_h),
|
||||
("Su-57 Felon", su57_felon),
|
||||
("UH-60L Black Hawk (v1.3.1)", uh_60l),
|
||||
]
|
||||
|
||||
for i in range(len(mod_pairs)):
|
||||
if i % 15 == 0:
|
||||
modLayout_row = 1
|
||||
col = 2 * (i // 15)
|
||||
if i % 5 == 0:
|
||||
# Section break here for readability
|
||||
modLayout.addWidget(QtWidgets.QWidget(), modLayout_row, col)
|
||||
modLayout_row += 1
|
||||
label, cb = mod_pairs[i]
|
||||
modLayout.addWidget(QLabel(label), modLayout_row, col)
|
||||
modLayout.addWidget(cb, modLayout_row, col + 1)
|
||||
modLayout_row += 1
|
||||
|
||||
modSettingsGroup.setLayout(modLayout)
|
||||
|
||||
mlayout = QVBoxLayout()
|
||||
|
||||
@@ -4,37 +4,36 @@ asgiref==3.5.2
|
||||
atomicwrites==1.4.1
|
||||
attrs==22.1.0
|
||||
black==22.10.0
|
||||
certifi==2022.9.24
|
||||
certifi==2022.12.7
|
||||
cfgv==3.3.1
|
||||
click==8.1.3
|
||||
colorama==0.4.6
|
||||
distlib==0.3.6
|
||||
Faker==15.1.1
|
||||
fastapi==0.85.1
|
||||
Faker==15.3.3
|
||||
fastapi==0.87.0
|
||||
filelock==3.8.0
|
||||
future==0.18.2
|
||||
h11==0.14.0
|
||||
httptools==0.5.0
|
||||
identify==2.5.7
|
||||
identify==2.5.9
|
||||
idna==3.4
|
||||
iniconfig==1.1.1
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.1
|
||||
mypy==0.982
|
||||
mypy==0.991
|
||||
mypy-extensions==0.4.3
|
||||
nodeenv==1.7.0
|
||||
packaging==21.3
|
||||
pathspec==0.10.1
|
||||
pathspec==0.10.2
|
||||
pefile==2022.5.30
|
||||
Pillow==9.2.0
|
||||
platformdirs==2.5.2
|
||||
Pillow==9.3.0
|
||||
platformdirs==2.5.4
|
||||
pluggy==1.0.0
|
||||
pre-commit==2.20.0
|
||||
py==1.11.0
|
||||
pydantic==1.10.2
|
||||
-e git+https://github.com/pydcs/dcs@16ea5d01c57a395ea5868630615c8a42dd2ea1a9#egg=pydcs
|
||||
pyinstaller==5.6.1
|
||||
pyinstaller-hooks-contrib==2022.10
|
||||
-e git+https://github.com/dcs-retribution/pydcs@f0bb4da4d458d714dedd1f3bba30deae5f25a80d#egg=pydcs
|
||||
pyinstaller==5.6.2
|
||||
pyinstaller-hooks-contrib==2022.13
|
||||
pyparsing==3.0.9
|
||||
pyproj==3.4.0
|
||||
pyshp==2.3.1
|
||||
@@ -48,18 +47,18 @@ Shapely==1.8.5.post1
|
||||
shiboken2==5.15.2.1
|
||||
six==1.16.0
|
||||
sniffio==1.3.0
|
||||
starlette==0.20.4
|
||||
starlette==0.21.0
|
||||
tabulate==0.9.0
|
||||
text-unidecode==1.3
|
||||
toml==0.10.2
|
||||
tomli==2.0.1
|
||||
types-Jinja2==2.11.9
|
||||
types-MarkupSafe==1.1.10
|
||||
types-Pillow==9.2.2.2
|
||||
types-PyYAML==6.0.12
|
||||
types-Pillow==9.3.0.2
|
||||
types-PyYAML==6.0.12.2
|
||||
types-tabulate==0.9.0.0
|
||||
typing_extensions==4.4.0
|
||||
uvicorn==0.19.0
|
||||
virtualenv==20.16.5
|
||||
uvicorn==0.20.0
|
||||
virtualenv==20.16.7
|
||||
watchgod==0.8.2
|
||||
websockets==10.3
|
||||
websockets==10.4
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{# TODO: DO NOT REMOVE NEWLINES, THEY ARE PART OF THE FORMATTING! #}
|
||||
DCS Retribution Turn {{ game.turn }}
|
||||
====================
|
||||
|
||||
Most briefing information, including communications and flight plan information, can be found on your kneeboard.
|
||||
|
||||
Current situation:
|
||||
@@ -62,17 +62,20 @@ On this location, our ground forces have been ordered to hold still, and defend
|
||||
|
||||
{%+ endfor %}{% endif %}
|
||||
|
||||
{%- if player_flights|length > 0 %}
|
||||
Your flights:
|
||||
====================
|
||||
{% for flight in flights if flight.client_units %}
|
||||
{% for flight in player_flights %}
|
||||
--------------------------------------------------
|
||||
{{ flight.flight_type }} {{ flight.units[0].type }} x {{ flight.size }}, departing in {{ flight.departure_delay }}, {{ flight.package.target.name}}
|
||||
Freq : {{ flight|intra_flight_channel }}
|
||||
{% for waypoint in flight.waypoints %}
|
||||
{{ loop.index0 }} {{waypoint|waypoint_timing("Depart ")}}-- {{waypoint.name}} : {{ waypoint.description}}
|
||||
{% endfor %}
|
||||
--------------------------------------------------{% endfor %}
|
||||
--------------------------------------------------
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{%- if allied_flights_by_departure|length > 0 %}
|
||||
Planned ally flights:
|
||||
@@ -83,6 +86,7 @@ Planned ally flights:
|
||||
{% for flight in allied_flights_by_departure[dep] %}
|
||||
{{ flight.flight_type }} {{ flight.units[0].type }} x {{flight.size}}, departing in {{ flight.departure_delay }}, {{ flight.package.target.name}}
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -98,12 +102,22 @@ TACAN : {{ runway.tacan }} {{ runway.tacan_callsign }}
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
{%- if awacs|length > 0 %}
|
||||
AWACS:
|
||||
====================
|
||||
{% for i in awacs %}{{ i.callsign }} -- Freq : {{i.freq.mhz}}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{%- if tankers|length > 0 %}
|
||||
TANKERS:
|
||||
====================
|
||||
{% for i in tankers %}{{ i.callsign }} -- Freq : {{i.freq.mhz}}, TACAN : {{ i.tacan }}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{%- if jtacs|length > 0 %}
|
||||
@@ -111,4 +125,5 @@ JTACS [F-10 Menu] :
|
||||
====================
|
||||
{% for jtac in jtacs %}Frontline {{ jtac.region }} -- Code : {{ jtac.code }}, Freq : {{ jtac.freq.mhz }}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user