30 Commits

Author SHA1 Message Date
MetalStormGhost
fe2fc70816 Added the CFTs to pylon 13 on Sufa loadouts which carry CFTs. 2022-10-29 15:06:42 +03:00
MetalStormGhost
07abdc1a34 Remove the stock Viper when the F-16I mod is enabled because that DCS ID is then used by the Sufa. 2022-10-29 15:06:03 +03:00
MetalStormGhost
c1b7911d56 Merge branch 'f16i-mod-support-v2.2' of https://github.com/dcs-retribution/dcs-retribution into f16i-mod-support-v2.2 2022-10-29 14:15:35 +03:00
MetalStormGhost
a7e5c7c425 Added banners for the F-16D Block 52 variants. 2022-10-29 13:08:18 +03:00
MetalStormGhost
756e86d891 Renamed loadouts to Retribution* 2022-10-29 13:01:13 +03:00
MetalStormGhost
0e9bbfd4e3 Merge branch 'dev' into f16i-mod-support-v2.2 2022-10-29 12:50:03 +03:00
MetalStormGhost
ed4b3e1166 Fixed the F-16I mod pydcs extension flyable flags and livery scanner objects. Also added aircraft icons for the F-16D variants. 2022-10-29 12:03:36 +03:00
MetalStormGhost
223d70e15f Merge remote-tracking branch 'remotes/MetalStormGhost/dcs_liberation/f16i-mod-support-v2.2' into f16i-mod-support-v2.2
Added a pydcs extension for the IDF Mods Project F-16I mod version 2.2, utilizing the pylon_injector written for the CJS Superbug mod support. Includes banner by Schmokedpancake.

The mod alters the stock F-16C aircraft, which requires a different approach when injecting the mod information to pydcs. The extension changes the F-16C parameters by setattr(F_16C_50, "fuel_max", 2585.48), adds new weapons to the existing pylons and adds five new ones (for example, the mod allows internal ECM equipment in the dorsal spine, freeing the central hardpoint for a fuel tank).

The mod also adds additional Vipers: F-16D (both with and without the dorsal spine, GE and P&W engines) and the stock F-16C Block 50 under a different DCS unit ID.

Also added a separate mod_payloads directory under resources which can be used when aircraft added by mods use the same "unitType" in the loadout lua files. The loadouts are only loaded if the mod is enabled (uses the existing inject_custom_payloads function).

The mod settings are now saved in the Liberation save inside the Faction, so loading the aircraft icons, banners, loadouts etc and the pydcs injection can be re-applied on game load.
2022-10-23 20:54:23 +03:00
MetalStormGhost
69c70e1598 Added loadouts and aircraft yamls for F-16D variants. 2022-10-20 19:13:04 +03:00
MetalStormGhost
fa2fa63cd3 Updated the IDF Mod Project F-16I mod pydcs extension to version 2.2, which now also includes the F-16D variants. Still needs aircraft .yamls, icon & banner and loadouts for F-16Ds. F-16I loadouts may also need to be updated. 2022-10-05 08:51:54 +03:00
MetalStormGhost
ad09548c69 Force re-scanning of loadouts after the injection: inject_mod_payloads(Path(THIS_DIR.parent / "resources/mod_payloads/f16i_idf"))
This should fix a KeyError on a loadout path with the loadout files in resources/mod_payloads/
2022-08-14 17:01:37 +03:00
MetalStormGhost
ae7a3ffa70 Renamed the weapon GBU_24_Paveway_III___2000lb_Laser_Guided_Bomb_ to GBU_24_Paveway_III___2000lb_Laser_Guided_Bomb, since the former doesn't exist on pydcs. 2022-08-06 23:51:15 +03:00
MetalStormGhost
bf7838e6cc Removed /MissionEditor/UnitPayloads/ from mod_payloads paths. Implemented a separate inject_mod_payloads method to support it. 2022-08-06 20:28:38 +03:00
MetalStormGhost
849b4e7e4e Removed /MissionEditor/UnitPayloads/ from mod_payloads paths. 2022-08-06 20:28:37 +03:00
MetalStormGhost
ef78824b05 Moved the section break one row up so it splits at five. 2022-08-06 20:28:36 +03:00
MetalStormGhost
53f28c6ad9 Added a new banner for the F-16C Barak and improved the handling of swapped icons. 2022-08-06 20:28:35 +03:00
MetalStormGhost
c3ea0c0df0 Added the flyable flag to F-16 IDF and F-16C-BARAK 2020. 2022-08-06 20:28:34 +03:00
MetalStormGhost
1c35b18cd9 Added squadrons and icon&banner for the F-16 (IDF) and Barak. 2022-08-06 20:28:34 +03:00
MetalStormGhost
b4e5efe3c0 Added "F-16I Sufa" and "F-16C Fighting Falcon (IDF mod)" to Israel factions and added missing planemod decorators. 2022-08-06 20:28:32 +03:00
MetalStormGhost
4dc619a140 Changed the mod setting save/load evaluation flow in Faction to satisfy mypy. 2022-08-06 20:28:07 +03:00
MetalStormGhost
9e9941ba94 Renamed inject_pylons -> inject_pylon 2022-08-06 20:28:06 +03:00
MetalStormGhost
b3ab2aed87 Updated the mod version number to v1.4.7 in QNewGameWizard.py and updated the changelog. 2022-08-06 20:28:06 +03:00
MetalStormGhost
840cbf66d9 Added aircraft yaml for the IDF mod F-16C_50. 2022-08-06 20:25:42 +03:00
MetalStormGhost
fccdd82d87 Changed the F-16I Liberation OCA/Runway loadout back to dumb bombs because the DCS AI cannot use the GBU-24 penetrator against runways. Added loadouts for the F-16C-BARAK 2020 and IDF mod F-16C_50. 2022-08-06 20:25:41 +03:00
MetalStormGhost
1a440f40b8 Added a separate directory for mod loadouts for stock aircraft which are modified by mods. Uses the existing inject_custom_payloads function to override the loadouts from there in case the mod is selected. 2022-08-06 20:25:41 +03:00
MetalStormGhost
9276db05d7 Loading saved mod settings from the faction now works. 2022-08-06 20:25:40 +03:00
MetalStormGhost
716224da13 Changed mod_settings into Optional[ModSettings] 2022-08-06 20:25:40 +03:00
MetalStormGhost
204060665f 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. 2022-08-06 20:25:39 +03:00
MetalStormGhost
1dd1ef6933 Updated the IDF Mod Project F-16I mod pydcs extension to version 1.4.7 2022-08-06 20:24:03 +03:00
MetalStormGhost
4fe0102b7b Added a pydcs extension for the IDF Mod Project F-16I mod version 1.4, utilizing the pylon_injector written for the CJS Superbug mod support. Includes banner by Schmokedpancake. 2022-08-06 20:24:03 +03:00
209 changed files with 1907 additions and 4601 deletions

View File

@@ -8,13 +8,13 @@ assignees: ''
---
This form should only be used for submitted updated miz/json files for campaigns
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
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
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-retribution/dcs-retribution/wiki/Campaign-maintenance, please start
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance, please start
there.
Otherwise, delete everything above the line below and fill out the following form. Note:

View File

@@ -1,17 +0,0 @@
---
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

View File

@@ -1,12 +1,12 @@
blank_issues_enabled: false
contact_links:
- name: FAQ
url: https://discord.gg/b4x34Bg4We
url: https://discord.gg/PXHA6AXw
about: Check to see if your issue is in the FAQ.
- name: Manual
url: https://github.com/dcs-retribution/dcs-retribution/wiki/
url: https://github.com/dcs-liberation/dcs_liberation/wiki/
- name: Feature blocking DCS AI bugs
url: https://github.com/dcs-retribution/dcs-retribution#dcs-bugs
url: https://github.com/dcs-liberation/dcs_liberation#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

View File

@@ -0,0 +1,21 @@
---
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.

View File

@@ -1,31 +0,0 @@
---
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.

View File

@@ -1,6 +1,6 @@
---
name: Mod support request
about: Request Retribution support for new mods, or updates to existing mods
about: Request Liberation support for new mods, or updates to existing mods
title: Add/update <mod name>
labels: mod support
assignees: ''

View File

@@ -45,7 +45,8 @@ 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

View File

@@ -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/Betas.
To download preview builds of the next version of DCS Retribution, see https://github.com/dcs-retribution/dcs-retribution/wiki/Preview-builds.
## 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/Betas)
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Preview-builds)
for that release. Items in the To do column are planned to be added to that release.
## Resources

View File

@@ -1,119 +1,79 @@
# 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 patch 20.10.22
* **[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-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.
* **[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.
* **[Data]** Added data to support C-47 Skytrain.
* **[Data]** Added data to support KS-19 & SON-9, including support for "AAA Site" layout.
## 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.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. 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.
* **[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 affect how mission is played at target area.
* **[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.
* **[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.
* **[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]** 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.
* **[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 generation issue that would cause AI helicopters to get stuck after taking off from a FARP.
* **[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.
* **[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.
* **[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.
* **[Squadrons]** Fixed issue in air wing configuration that would allow squadrons to be created with no home base if no base was available.
* **[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.
* **[UI]** Add vanilla theme weather and time of day icons
* **[UI]** Disable player slots for non-flyable aircraft.
* **[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.
* **[UI]** Add vanilla theme weather and time of day icons
# 5.2.0

View File

@@ -6424,9 +6424,9 @@
}
},
"node_modules/babel-loader/node_modules/loader-utils": {
"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==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dev": true,
"dependencies": {
"big.js": "^5.2.2",
@@ -13959,9 +13959,9 @@
}
},
"node_modules/loader-utils": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"dev": true,
"dependencies": {
"big.js": "^5.2.2",
@@ -17234,9 +17234,9 @@
}
},
"node_modules/react-dev-utils/node_modules/loader-utils": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
"integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz",
"integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==",
"dev": true,
"engines": {
"node": ">= 12.13.0"
@@ -25872,9 +25872,9 @@
}
},
"loader-utils": {
"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==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
@@ -31521,9 +31521,9 @@
"dev": true
},
"loader-utils": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
@@ -33869,9 +33869,9 @@
"dev": true
},
"loader-utils": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
"integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz",
"integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==",
"dev": true
},
"supports-color": {

View File

@@ -1,3 +1,2 @@
from .game import Game
from .version import VERSION
from .migrator import Migrator

View File

@@ -10,7 +10,6 @@ from dcs.helicopters import (
CH_47D,
CH_53E,
Ka_50,
Ka_50_3,
Mi_24P,
Mi_24V,
Mi_26,
@@ -46,13 +45,11 @@ 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,
@@ -119,7 +116,6 @@ from dcs.planes import (
Tu_95MS,
WingLoong_I,
Yak_40,
MB_339A,
)
from dcs.unittype import FlyingType
@@ -145,7 +141,6 @@ 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
@@ -156,14 +151,13 @@ 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
ESCORT_CAPABLE = [
CAP_CAPABLE = [
Su_57,
F_22A,
F_15C,
F_15D,
F_14B,
F_14A_135_GR,
F_14A,
Su_33,
J_11A,
Su_30,
@@ -183,7 +177,6 @@ ESCORT_CAPABLE = [
FA_18C_hornet,
JF_17,
JAS39Gripen,
F_16A_MLU,
F_16A,
F_4E,
VSN_F4B,
@@ -215,6 +208,7 @@ ESCORT_CAPABLE = [
F_86F_Sabre,
MiG_15bis,
C_101CC,
L_39ZA,
VSN_F84G,
P_51D_30_NA,
P_51D,
@@ -230,11 +224,6 @@ ESCORT_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 = [
@@ -247,6 +236,7 @@ CAS_CAPABLE = [
Su_25,
F_15D,
F_15E,
F_16A,
F_16C_50,
F_16I,
F_16D_Barak,
@@ -261,8 +251,6 @@ CAS_CAPABLE = [
JF_17,
AV8BNA,
A_10A,
F_16A_MLU,
F_16A,
B_1B,
A_4E_C,
F_14B,
@@ -290,7 +278,6 @@ CAS_CAPABLE = [
SA342M,
SA342L,
Ka_50,
Ka_50_3,
Mi_28N,
Mi_24P,
Mi_24V,
@@ -311,8 +298,6 @@ CAS_CAPABLE = [
F_86F_Sabre,
C_101CC,
L_39ZA,
MB_339A,
Bronco_OV_10A,
UH_1H,
A_20G,
Ju_88A4,
@@ -344,7 +329,7 @@ CAS_CAPABLE = [
# Aircraft used for SEAD and SEAD Escort tasks. Must be capable of the CAS DCS task.
SEAD_ESCORT_CAPABLE = [
SEAD_CAPABLE = [
JF_17,
F_16C_50,
F_16I,
@@ -359,6 +344,8 @@ SEAD_ESCORT_CAPABLE = [
Su_25TM,
F_4E,
A_4E_C,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
AV8BNA,
Su_24M,
@@ -372,21 +359,15 @@ SEAD_ESCORT_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,
@@ -400,7 +381,6 @@ DEAD_CAPABLE = SEAD_CAPABLE + [
P_47D_30,
P_51D_30_NA,
P_51D,
Bronco_OV_10A,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
MosquitoFBMkVI,
@@ -440,7 +420,6 @@ STRIKE_CAPABLE = [
FA_18C_hornet,
AV8BNA,
JF_17,
F_16A_MLU,
F_16A,
F_14B,
F_14A_135_GR,
@@ -467,7 +446,6 @@ STRIKE_CAPABLE = [
A_10C,
S_3B,
A_4E_C,
Bronco_OV_10A,
M_2000C,
Mirage_F1B,
Mirage_F1BE,
@@ -484,7 +462,6 @@ STRIKE_CAPABLE = [
F_86F_Sabre,
C_101CC,
L_39ZA,
MB_339A,
B_17G,
A_20G,
Ju_88A4,
@@ -516,10 +493,9 @@ 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,
@@ -548,14 +524,6 @@ 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,
@@ -573,7 +541,6 @@ RUNWAY_ATTACK_CAPABLE = [
FA_18C_hornet,
AV8BNA,
JF_17,
F_16A_MLU,
F_16A,
F_14B,
F_14A_135_GR,
@@ -600,8 +567,15 @@ 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,
@@ -609,7 +583,6 @@ RUNWAY_ATTACK_CAPABLE = [
F_86F_Sabre,
C_101CC,
L_39ZA,
MB_339A,
B_17G,
A_20G,
Ju_88A4,
@@ -708,7 +681,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_ESCORT_CAPABLE
return SEAD_CAPABLE
elif task == FlightType.DEAD:
return DEAD_CAPABLE
elif task == FlightType.OCA_AIRCRAFT:
@@ -718,7 +691,7 @@ def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
elif task == FlightType.STRIKE:
return STRIKE_CAPABLE
elif task == FlightType.ESCORT:
return ESCORT_CAPABLE
return CAP_CAPABLE
elif task == FlightType.AEWC:
return AEWC_CAPABLE
elif task == FlightType.REFUELING:

View File

@@ -63,7 +63,6 @@ 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
@@ -91,12 +90,6 @@ 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

View File

@@ -4,39 +4,28 @@ from dataclasses import dataclass
from datetime import timedelta
from typing import Iterator, TYPE_CHECKING, Type
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
from game.ato.flightplans.airlift import AirliftLayout
from game.ato.flightplans.standard import StandardFlightPlan
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(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.
class AirAssaultLayout(AirliftLayout):
target: FlightWaypoint
nav_to_home: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
if self.pickup is not None:
yield from self.nav_to_pickup
if self.pickup:
yield self.pickup
yield from self.nav_to_ingress
yield self.ingress
yield from self.nav_to_drop_off
yield self.drop_off
yield self.target
yield from self.nav_to_home
@@ -46,7 +35,7 @@ class AirAssaultLayout(StandardLayout):
yield self.bullseye
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@@ -64,48 +53,35 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
return None
@property
def ctld_target_zone_radius(self) -> Distance:
def engagement_distance(self) -> Distance:
# The radius of the WaypointZone created at the target location
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 self.flight.departure.cptype in [
if not self.flight.is_helo or self.flight.departure.cptype in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
ControlPointType.OFF_MAP,
]:
# Off_Map spawns will be preloaded
# Non-Helo flights or Off_Map 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_zone(
pickup = builder.pickup(
MissionTarget(
"Pickup Zone",
self.flight.departure.position.random_point_within(1200, 600),
@@ -114,8 +90,6 @@ 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),
@@ -123,19 +97,21 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
return AirAssaultLayout(
departure=builder.takeoff(self.flight.departure),
pickup=pickup,
nav_to_ingress=builder.nav_path(
nav_to_pickup=builder.nav_path(
self.flight.departure.position,
pickup_position,
self.package.waypoints.ingress,
altitude,
altitude_is_agl,
),
ingress=builder.ingress(
FlightWaypointType.INGRESS_AIR_ASSAULT,
self.package.waypoints.ingress,
self.package.target,
pickup=pickup,
nav_to_drop_off=builder.nav_path(
pickup_position,
drop_off_zone.position,
altitude,
altitude_is_agl,
),
drop_off=builder.dropoff_zone(drop_off_zone),
drop_off=builder.drop_off(drop_off_zone),
stopover=None,
target=assault_area,
nav_to_home=builder.nav_path(
drop_off_zone.position,

View File

@@ -19,20 +19,10 @@ 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]
# 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
drop_off: FlightWaypoint
stopover: FlightWaypoint | None
nav_to_home: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
@@ -40,13 +30,10 @@ 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
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 self.drop_off
if self.stopover is not None:
yield self.stopover
yield from self.nav_to_home
yield self.arrival
if self.divert is not None:
@@ -61,11 +48,7 @@ class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
@property
def tot_waypoint(self) -> FlightWaypoint:
# 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
return self.layout.drop_off
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
# TOT planning isn't really useful for transports. They're behind the front
@@ -94,31 +77,31 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
builder = WaypointBuilder(self.flight, self.coalition)
pickup = 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)
stopover = None
if self.flight.is_helo:
# Create CTLD Zones for Helo flights
pickup_zone = builder.pickup_zone(
MissionTarget(
"Pickup Zone", cargo.origin.position.random_point_within(1000, 200)
)
# Create a pickupzone where the cargo will be spawned
pickup_zone = MissionTarget(
"Pickup Zone", cargo.origin.position.random_point_within(1000, 200)
)
drop_off_zone = builder.dropoff_zone(
MissionTarget(
"Dropoff zone",
cargo.next_stop.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),
)
# Show the zone waypoints only to the player
pickup_zone.only_for_player = True
drop_off_zone.only_for_player = True
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")
nav_to_pickup = builder.nav_path(
self.flight.departure.position,
@@ -127,11 +110,20 @@ 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,
@@ -139,14 +131,14 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
altitude_is_agl,
),
drop_off=drop_off,
ctld_drop_off_zone=drop_off_zone,
stopover=stopover,
nav_to_home=builder.nav_path(
cargo.origin.position,
self.flight.arrival.position,
altitude,
altitude_is_agl,
),
arrival=builder.land(self.flight.arrival),
arrival=arrival,
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)

View File

@@ -10,7 +10,6 @@ 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
@@ -29,13 +28,13 @@ class CasLayout(PatrollingLayout):
yield self.target
yield self.patrol_end
yield from self.nav_from
yield self.arrival
yield self.departure
if self.divert is not None:
yield self.divert
yield self.bullseye
class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
class CasFlightPlan(PatrollingFlightPlan[CasLayout]):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@@ -66,12 +65,6 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
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:

View File

@@ -8,7 +8,6 @@ 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:
@@ -56,17 +55,9 @@ 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), self.waypoints)
return CustomLayout(builder.takeoff(self.flight.departure), [])
def build(self) -> CustomFlightPlan:
return CustomFlightPlan(self.flight, self.layout())

View File

@@ -8,8 +8,6 @@ from .formationattack import (
FormationAttackLayout,
)
from .waypointbuilder import WaypointBuilder
from .. import FlightType
from ...utils import Distance
class EscortFlightPlan(FormationAttackFlightPlan):
@@ -26,17 +24,12 @@ 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 = 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)
)
refuel = None
if self.package.waypoints.refuel is not None:
refuel = builder.refuel(self.package.waypoints.refuel)
return FormationAttackLayout(
departure=builder.takeoff(self.flight.departure),
@@ -46,12 +39,11 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
),
join=join,
ingress=ingress,
initial=initial,
targets=[target],
split=split,
refuel=refuel,
nav_from=builder.nav_path(
refuel.position,
split.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
),

View File

@@ -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, Optional
from typing import TYPE_CHECKING, TypeVar
from dcs import Point
from game.flightplan import HoldZoneGeometry
from game.theater import MissionTarget
from game.utils import Speed, meters, Distance
from game.utils import Speed, meters
from .flightplan import FlightPlan
from .formation import FormationFlightPlan, FormationLayout
from .ibuilder import IBuilder
@@ -134,7 +134,6 @@ 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
@@ -142,8 +141,6 @@ 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:
@@ -184,11 +181,9 @@ 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 = builder.refuel(self.package.waypoints.refuel)
ingress = builder.ingress(
ingress_type, self.package.waypoints.ingress, self.package.target
)
refuel = None
if self.package.waypoints.refuel is not None:
refuel = builder.refuel(self.package.waypoints.refuel)
return FormationAttackLayout(
departure=builder.takeoff(self.flight.departure),
@@ -197,12 +192,14 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
hold.position, join.position, self.doctrine.ingress_altitude
),
join=join,
ingress=ingress,
ingress=builder.ingress(
ingress_type, self.package.waypoints.ingress, self.package.target
),
targets=target_waypoints,
split=split,
refuel=refuel,
nav_from=builder.nav_path(
refuel.position,
split.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
),

View File

@@ -9,7 +9,6 @@ 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
@@ -38,7 +37,7 @@ class PatrollingLayout(StandardLayout):
LayoutT = TypeVar("LayoutT", bound=PatrollingLayout)
class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
class PatrollingFlightPlan(StandardFlightPlan[LayoutT], ABC):
@property
@abstractmethod
def patrol_duration(self) -> timedelta:
@@ -98,9 +97,3 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, 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,
)

View File

@@ -22,6 +22,7 @@ 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]:
@@ -30,6 +31,8 @@ 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:
@@ -89,7 +92,6 @@ class SweepFlightPlan(LoiterFlightPlan):
GroundSpeed.for_flight(self.flight, self.layout.hold.alt),
)
@property
def mission_departure_time(self) -> timedelta:
return self.sweep_end_time
@@ -110,6 +112,11 @@ 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,
@@ -123,6 +130,7 @@ 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(),

View File

@@ -105,11 +105,9 @@ 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),
@@ -117,7 +115,7 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
self.flight.departure.position, orbit0p, patrol_alt
),
nav_from=builder.nav_path(
nav_from_origin, self.flight.arrival.position, patrol_alt
orbit1p, self.flight.arrival.position, patrol_alt
),
patrol_start=start,
patrol_end=end,

View File

@@ -1,18 +0,0 @@
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:
...

View File

@@ -304,13 +304,6 @@ 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
@@ -422,24 +415,6 @@ 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.
@@ -519,50 +494,58 @@ class WaypointBuilder:
)
@staticmethod
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)
def stopover(stopover: ControlPoint, name: str = "STOPOVER") -> FlightWaypoint:
"""Creates a stopover waypoint.
Args:
control_point: Pick up location.
"""
return FlightWaypoint(
"PICKUPZONE",
FlightWaypointType.PICKUP_ZONE,
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,
pick_up.position,
meters(0),
"RADIO",
description=f"Pick up cargo from {pick_up.name}",
pretty_name="Pick-up zone",
pretty_name="Pick up location",
control_point=control_point,
)
@staticmethod
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)
def drop_off(drop_off: MissionTarget) -> FlightWaypoint:
"""Creates a cargo drop-off waypoint.
Args:
control_point: Drop-off location.
"""
control_point = drop_off if isinstance(drop_off, ControlPoint) else None
return FlightWaypoint(
"DROPOFFZONE",
FlightWaypointType.DROPOFF_ZONE,
"DROP OFF",
FlightWaypointType.DROP_OFF,
drop_off.position,
meters(0),
"RADIO",
description=f"Drop off cargo at {drop_off.name}",
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",
pretty_name="Drop off location",
control_point=control_point,
)

View File

@@ -137,7 +137,6 @@ 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

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass, field
from datetime import timedelta
from typing import Literal, TYPE_CHECKING
@@ -11,7 +12,8 @@ from game.theater.theatergroup import TheaterUnit
from game.utils import Distance, meters
if TYPE_CHECKING:
from game.theater import ControlPoint
from game.theater import ControlPoint, MissionTarget
AltitudeReference = Literal["BARO", "RADIO"]
@@ -30,7 +32,7 @@ class FlightWaypoint:
# having three names. A short and long form is enough.
description: str = ""
targets: list[TheaterUnit] = field(default_factory=list)
targets: Sequence[MissionTarget | TheaterUnit] = field(default_factory=list)
obj_name: str = ""
pretty_name: str = ""
only_for_player: bool = False

View File

@@ -43,9 +43,8 @@ class FlightWaypointType(IntEnum):
DIVERT = 23
INGRESS_OCA_RUNWAY = 24
INGRESS_OCA_AIRCRAFT = 25
PICKUP_ZONE = 26 # Pickup Zone for cargo or troops
DROPOFF_ZONE = 27 # Dropoff Zone for cargo or troops
PICKUP = 26
DROP_OFF = 27
BULLSEYE = 28
REFUEL = 29 # Should look for nearby tanker to refuel from.
CARGO_STOP = 30 # Stopover landing point using the LandingReFuAr waypoint type
INGRESS_AIR_ASSAULT = 31
STOPOVER = 30 # Stopover landing point using the LandingReFuAr waypoint type

View File

@@ -151,22 +151,10 @@ class Loadout:
# names, so those have been included here too. The priority goes from first to
# last - the first element in the tuple will be tried first, then the second,
# etc.
loadout_names = {
t: [f"Retribution {t.value}", f"Liberation {t.value}"] for t in FlightType
}
loadout_names = {t: [f"Retribution {t.value}"] for t in FlightType}
legacy_names = {
FlightType.TARCAP: (
"CAP HEAVY",
"CAP",
"Retribution BARCAP",
"Liberation BARCAP",
),
FlightType.BARCAP: (
"CAP HEAVY",
"CAP",
"Retribution TARCAP",
"Liberation TARCAP",
),
FlightType.TARCAP: ("CAP HEAVY", "CAP", "Retribution BARCAP"),
FlightType.BARCAP: ("CAP HEAVY", "CAP", "Retribution TARCAP"),
FlightType.CAS: ("CAS MAVERICK F", "CAS"),
FlightType.STRIKE: ("STRIKE",),
FlightType.ANTISHIP: ("ANTISHIP",),

View File

@@ -129,16 +129,6 @@ 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:

View File

@@ -1,15 +1,13 @@
from __future__ import annotations
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING
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
@@ -20,7 +18,6 @@ if TYPE_CHECKING:
class PackageWaypoints:
join: Point
ingress: Point
initial: Point
split: Point
refuel: Point
@@ -35,11 +32,6 @@ 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,
@@ -59,7 +51,6 @@ class PackageWaypoints:
return PackageWaypoints(
WaypointBuilder.perturb(join_point),
ingress_point,
initial_point,
WaypointBuilder.perturb(join_point),
refuel_point,
)

View File

@@ -139,6 +139,14 @@ 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.

View File

@@ -7,6 +7,7 @@ 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
@@ -18,6 +19,7 @@ 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):

View File

@@ -0,0 +1,35 @@
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)

View File

@@ -42,5 +42,6 @@ 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)
self.propose_flight(FlightType.SEAD_ESCORT, 2, EscortType.Sead)
else:
self.propose_flight(FlightType.SEAD_ESCORT, 2, EscortType.Sead)
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)

View File

@@ -45,6 +45,7 @@ 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]
@@ -109,6 +110,7 @@ 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),
@@ -159,6 +161,7 @@ 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()],

View File

@@ -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, Speed, knots
from game.utils import Distance, feet, nautical_miles
@dataclass
@@ -77,16 +77,6 @@ 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,
@@ -123,11 +113,6 @@ 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(
@@ -165,11 +150,6 @@ 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(
@@ -206,9 +186,4 @@ 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),
)

View File

@@ -338,7 +338,9 @@ class Debriefing:
seen = set()
captures = []
for capture in reversed(self.state_data.base_capture_events):
# The ID string in the JSON file will be the UUID generated from liberation
# 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.
cp_id, new_owner_id_str, _name = capture.split("||")
# Only the most recent capture event matters.
@@ -347,8 +349,13 @@ 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, ValueError):
except KeyError:
# Captured base is not a part of the campaign. This happens when neutral
# bases are near the conflict. Nothing to do.
continue

View File

@@ -116,9 +116,6 @@ 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
@@ -287,16 +284,6 @@ 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
@@ -356,7 +343,6 @@ 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")
@@ -375,8 +361,6 @@ 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")

View File

@@ -21,8 +21,7 @@ class RefuelZoneGeometry:
def find_best_refuel_point(self) -> Point:
# Do simple at first.
# TODO: Avoid threatened areas, minimize backtracking.
# https://github.com/dcs-liberation/dcs_liberation/issues/2542
# TODO: Consider threats.
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)

View File

@@ -528,20 +528,12 @@ class Game:
packages = itertools.chain(self.blue.ato.packages, self.red.ato.packages)
for package in packages:
if package.primary_task in [
FlightType.BARCAP,
FlightType.TRANSPORT,
FlightType.AEWC,
FlightType.REFUELING,
]:
if package.primary_task is FlightType.BARCAP:
# BARCAPs will be planned at most locations on smaller theaters,
# rendering culling fairly useless. BARCAP packages don't really
# need the ground detail since they're defensive. SAMs nearby
# are only interesting if there are enemies in the area, and if
# there are they won't be culled because of the enemy's mission.
# Don't create culling exclusion zones around FlightType.TRANSPORT,
# FlightType.AEWC & FlightType.REFUELING mission targets.
continue
zones.append(package.target.position)

View File

@@ -31,7 +31,7 @@ from game.profiling import logged_duration
from game.version import VERSION
LAYOUT_DIR = "resources/layouts/"
LAYOUT_DUMP = "Retribution/layouts.p"
LAYOUT_DUMP = "Liberation/layouts.p"
LAYOUT_TYPES = {
GroupRole.AIR_DEFENSE: AntiAirLayout,

View File

@@ -1,44 +0,0 @@
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)

View File

@@ -8,7 +8,6 @@ from dcs.task import (
CAP,
CAS,
EPLRS,
Escort,
FighterSweep,
GroundAttack,
Nothing,
@@ -20,9 +19,6 @@ from dcs.task import (
Refueling,
RunwayAttack,
Transport,
SEAD,
SwitchWaypoint,
OptJettisonEmptyTanks,
)
from dcs.unitgroup import FlyingGroup
@@ -87,7 +83,6 @@ 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))
@@ -109,8 +104,7 @@ class AircraftBehavior:
for unit in group.units:
unit.gun = 0
group.points[0].tasks.append(OptRTBOnBingoFuel(rtb_on_bingo))
group.points[0].tasks.append(OptJettisonEmptyTanks())
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
# 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
@@ -271,9 +265,7 @@ 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 = Escort.name
if flight.package.primary_task == FlightType.STRIKE:
group.add_trigger_action(SwitchWaypoint(None, 5))
group.task = CAP.name
self.configure_behavior(
flight, group, roe=OptROE.Values.OpenFire, restrict_jettison=True
)
@@ -282,9 +274,7 @@ 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 = SEAD.name
if flight.package.primary_task == FlightType.STRIKE:
group.add_trigger_action(SwitchWaypoint(None, 5))
group.task = CAS.name
self.configure_behavior(
flight,
group,
@@ -314,7 +304,6 @@ 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:

View File

@@ -5,12 +5,9 @@ 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
@@ -114,19 +111,6 @@ 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 is not package.primary_flight:
splittrigger.add_action(AITaskPush(flight.group_id, 1))
self.mission.triggerrules.triggers.append(splittrigger)
def spawn_unused_aircraft(
self, player_country: Country, enemy_country: Country
@@ -151,9 +135,6 @@ 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
@@ -171,7 +152,7 @@ class AircraftGenerator:
flight.state = Completed(flight, self.game.settings)
group = FlightGroupSpawner(
flight, country, self.mission, self.helipads, self.mission_data
flight, country, self.mission, self.helipads
).create_idle_aircraft()
AircraftPainter(flight, group).apply_livery()
self.unit_map.add_aircraft(group, flight)
@@ -181,7 +162,7 @@ class AircraftGenerator:
) -> FlyingGroup[Any]:
"""Creates and configures the flight group in the mission."""
group = FlightGroupSpawner(
flight, country, self.mission, self.helipads, self.mission_data
flight, country, self.mission, self.helipads
).create_flight_group()
self.flights.append(
FlightGroupConfigurator(

View File

@@ -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 F_14A, Su_33
from dcs.planes import Su_33
from dcs.ships import KUZNECOW
from dcs.terrain import Airport, NoParkingSlotError
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
@@ -15,7 +15,6 @@ 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
@@ -33,8 +32,6 @@ 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
@@ -47,13 +44,11 @@ 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.
@@ -79,12 +74,8 @@ class FlightGroupSpawner:
self.flight.state.is_waiting_for_start
or self.flight.state.spawn_type is not StartType.IN_FLIGHT
):
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
return self.generate_flight_at_departure()
return self.generate_mid_mission()
def create_idle_aircraft(self) -> FlyingGroup[Any]:
airport = self.flight.squadron.location.dcs_airport
@@ -145,10 +136,6 @@ 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.
@@ -158,9 +145,8 @@ 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 < self.mission_data.cp_stack[cp]:
alt = self.mission_data.cp_stack[cp]
self.mission_data.cp_stack[cp] += STACK_SEPARATION
if alt_type == "RADIO" and alt < MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL:
alt = MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL
group = self.mission.flight_group(
country=self.country,
@@ -273,9 +259,7 @@ 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)
# 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 (
if (
self.flight.unit_type.dcs_unit_type == Su_33
and group_units[0] is not None
and group_units[0].type == KUZNECOW.id

View File

@@ -1,25 +0,0 @@
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)

View File

@@ -1,14 +1,24 @@
from dcs.point import MovingPoint, PointAction
from dcs.point import MovingPoint
from dcs.point import PointAction
from dcs.task import Land
from game.utils import feet
from .pydcswaypointbuilder import PydcsWaypointBuilder
class CargoStopBuilder(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
# 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
return waypoint

View File

@@ -4,7 +4,6 @@ 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
@@ -23,10 +22,8 @@ class HoldPointBuilder(PydcsWaypointBuilder):
return
push_time = self.flight.flight_plan.push_time
self.waypoint.departure_time = push_time
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
loiter.stop_after_time(
int((push_time - self.elapsed_mission_time).total_seconds())
)
waypoint.add_task(loiter)
waypoint.add_task(OptFormation.finger_four_close())

View File

@@ -1,60 +1,60 @@
import random
from typing import List
from typing import List, Type
from dcs.point import MovingPoint
from dcs.task import (
ControlledTask,
EscortTaskAction,
EngageTargets,
OptECMUsing,
OptFormation,
TargetType,
Targets,
)
from game.ato import FlightType
from game.theater import NavalControlPoint
from game.utils import nautical_miles, feet
from game.utils import nautical_miles
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.id,
Targets.All.Air.Planes.MultiroleFighters.id,
Targets.All.Air.Planes.Fighters,
Targets.All.Air.Planes.MultiroleFighters,
],
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.id,
Targets.All.GroundUnits.AirDefence.AAA.SAMRelated.id,
Targets.All.Naval,
Targets.All.GroundUnits.AirDefence.AAA.SAMRelated,
],
max_dist=doctrine.sead_escort_engagement_range.nautical_miles,
vertical_spacing=doctrine.sead_escort_spacing.feet,
)
else:
self.configure_escort_tasks(
waypoint,
[Targets.All.GroundUnits.AirDefence.AAA.SAMRelated.id],
max_dist=doctrine.sead_escort_engagement_range.nautical_miles,
vertical_spacing=doctrine.sead_escort_spacing.feet,
waypoint, [Targets.All.GroundUnits.AirDefence.AAA.SAMRelated]
)
# 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,32 +64,48 @@ 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(
self,
waypoint: MovingPoint,
target_types: List[str],
max_dist: float = 30.0,
vertical_spacing: float = 2000.0,
waypoint: MovingPoint, target_types: List[Type[TargetType]]
) -> None:
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,
# 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,
)
)
)
escort.stop_if_user_flag(f"split-{id(self.package)}", True)
waypoint.tasks.append(escort)
# 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.

View File

@@ -1,17 +0,0 @@
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

View File

@@ -12,7 +12,6 @@ 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
@@ -58,11 +57,7 @@ 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
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
racetrack.stop_after_time(int(loiter_duration.total_seconds()))
waypoint.add_task(racetrack)
def configure_refueling_actions(self, waypoint: MovingPoint) -> None:

View File

@@ -1,12 +1,10 @@
from dcs.point import MovingPoint
from dcs.task import RefuelingTaskAction, ControlledTask
from dcs.task import RefuelingTaskAction
from .pydcswaypointbuilder import PydcsWaypointBuilder
class RefuelPointBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
refuel = ControlledTask(RefuelingTaskAction())
refuel.start_probability(10)
waypoint.add_task(refuel)
waypoint.add_task(RefuelingTaskAction())
return super().add_tasks(waypoint)

View File

@@ -1,5 +1,5 @@
from dcs.point import MovingPoint
from dcs.task import OptECMUsing, OptFormation, RunScript
from dcs.task import OptECMUsing, OptFormation
from .pydcswaypointbuilder import PydcsWaypointBuilder
@@ -16,12 +16,4 @@ class SplitPointBuilder(PydcsWaypointBuilder):
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfOnlyLockByRadar)
waypoint.tasks.append(ecm_option)
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)
waypoint.tasks.append(OptFormation.finger_four_close())

View File

@@ -0,0 +1,14 @@
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

View File

@@ -1,22 +1,15 @@
import copy
from dcs import Point
from dcs.planes import B_17G, B_52H, Tu_22M3, B_1B
from dcs.planes import B_17G, B_52H, Tu_22M3
from dcs.point import MovingPoint
from dcs.task import Bombing, Expend, OptFormation, WeaponType
from dcs.task import Bombing, OptFormation, WeaponType, Expend
from game.utils import mach, Distance
from .pydcswaypointbuilder import PydcsWaypointBuilder
class StrikeIngressBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
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:
if self.group.units[0].unit_type in [B_17G, B_52H, Tu_22M3]:
self.add_bombing_tasks(waypoint)
else:
self.add_strike_tasks(waypoint)
@@ -28,28 +21,24 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
if not targets:
return
center: Point = copy.copy(targets[0].position)
center = 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=ga
center, weapon_type=WeaponType.Bombs, expend=Expend.All, group_attack=True
)
waypoint.tasks.append(bombing)
def add_strike_tasks(
self, waypoint: MovingPoint, weapon_type: WeaponType = WeaponType.Auto
) -> None:
ga = True if self.flight.count > 1 else False
def add_strike_tasks(self, waypoint: MovingPoint) -> None:
for target in self.waypoint.targets:
bombing = Bombing(target.position, weapon_type=weapon_type, group_attack=ga)
bombing = Bombing(
target.position, weapon_type=WeaponType.Auto, group_attack=True
)
# 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)

View File

@@ -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.cargostop import CargoStopBuilder
from game.missiongenerator.aircraft.waypoints.stopover import StopoverBuilder
from game.missiongenerator.missiondata import MissionData
from game.settings import Settings
from game.utils import pairwise
from .baiingress import BaiIngressBuilder
from .landingzone import LandingZoneBuilder
from .cargostop import CargoStopBuilder
from .casingress import CasIngressBuilder
from .deadingress import DeadIngressBuilder
from .default import DefaultWaypointBuilder
@@ -118,6 +118,7 @@ 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,
@@ -132,10 +133,9 @@ class WaypointGenerator:
FlightWaypointType.LOITER: HoldPointBuilder,
FlightWaypointType.PATROL: RaceTrackEndBuilder,
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
FlightWaypointType.PICKUP_ZONE: LandingZoneBuilder,
FlightWaypointType.DROPOFF_ZONE: LandingZoneBuilder,
FlightWaypointType.PICKUP: CargoStopBuilder,
FlightWaypointType.REFUEL: RefuelPointBuilder,
FlightWaypointType.CARGO_STOP: CargoStopBuilder,
FlightWaypointType.STOPOVER: StopoverBuilder,
}
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
return builder(

View File

@@ -58,7 +58,6 @@ 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] = []
@@ -88,8 +87,6 @@ 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.

View File

@@ -10,7 +10,6 @@ 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
@@ -73,7 +72,6 @@ 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]
@@ -99,7 +97,6 @@ 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

View File

@@ -17,7 +17,6 @@ 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:

View File

@@ -1,6 +1,5 @@
from __future__ import annotations
import logging
import math
import random
from typing import List, Optional, TYPE_CHECKING, Tuple
@@ -44,7 +43,6 @@ 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
@@ -223,7 +221,7 @@ class FlotGenerator:
u = random.choices(
manpads, weights=[m.spawn_weight for m in manpads]
)[0]
vg = self.mission.vehicle_group(
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, u),
u.dcs_unit_type,
@@ -232,8 +230,6 @@ 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))
@@ -250,7 +246,7 @@ class FlotGenerator:
weights=[u.spawn_weight for u in infantry_choices],
k=INFANTRY_GROUP_SIZE,
)
vg = self.mission.vehicle_group(
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, units[0]),
units[0].dcs_unit_type,
@@ -259,12 +255,10 @@ 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)
vg = self.mission.vehicle_group(
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, unit),
unit.dcs_unit_type,
@@ -273,8 +267,6 @@ 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
@@ -777,8 +769,6 @@ 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),
@@ -793,6 +783,5 @@ class FlotGenerator:
for c in range(count):
vehicle: Vehicle = group.units[c]
vehicle.player_can_drive = True
GroundForcePainter(faction, vehicle).apply_livery()
return group

View File

@@ -1,82 +0,0 @@
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

View File

@@ -400,9 +400,6 @@ 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
@@ -639,8 +636,6 @@ 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
@@ -660,21 +655,17 @@ class StrikeTaskPage(KneeboardPage):
writer.title(f"{self.flight.callsign} Strike Task Info{custom_name_title}")
writer.table(
[self.target_info_row(t, writer) for t in self.targets],
headers=["STPT", "Description", "Location"],
[self.target_info_row(t) for t in self.targets],
headers=["Steerpoint", "Description", "Location"],
)
writer.write(path)
@staticmethod
def target_info_row(
target: NumberedWaypoint, writer: KneeboardPageWriter
) -> list[str]:
def target_info_row(target: NumberedWaypoint) -> list[str]:
return [
str(target.number),
writer.wrap_line(
target.waypoint.pretty_name, StrikeTaskPage.WAYPOINT_DESC_MAX_LEN
),
target.waypoint.pretty_name,
target.waypoint.position.latlng().format_dms(include_decimal_seconds=True),
]

View File

@@ -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.ctld_target_zone_radius.meters,
self.flight.flight_plan.engagement_distance.meters,
False,
target_zone,
)
@@ -57,8 +57,8 @@ class LogisticsGenerator:
if (
waypoint.waypoint_type
not in [
FlightWaypointType.PICKUP_ZONE,
FlightWaypointType.DROPOFF_ZONE,
FlightWaypointType.PICKUP,
FlightWaypointType.DROP_OFF,
]
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_ZONE:
if waypoint.waypoint_type == FlightWaypointType.PICKUP:
pickup_point = waypoint.position
logistics_info.pickup_zone = zone_name
else:

View File

@@ -40,7 +40,7 @@ class LuaGenerator:
self.inject_plugins()
def generate_plugin_data(self) -> None:
lua_data = LuaData("dcsRetribution")
lua_data = LuaData("dcsLiberation")
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 Retribution data")
trigger = TriggerStart(comment="Set DCS Liberation 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("DCSRetribution|: setting configuration table")
env.info("DCSLiberation|: setting configuration table")
"""
return lua_prefix + self.serialize()

View File

@@ -11,8 +11,6 @@ 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
@@ -96,4 +94,3 @@ 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)

View File

@@ -44,7 +44,7 @@ if TYPE_CHECKING:
from game import Game
COMBINED_ARMS_SLOTS = 3
COMBINED_ARMS_SLOTS = 1
def country_id_from_name(name: str) -> int:
@@ -330,12 +330,6 @@ 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 = 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
self.mission.groundControl.blue_observer = 1

View File

@@ -40,10 +40,6 @@ 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
@@ -126,7 +122,6 @@ 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,
@@ -139,13 +134,11 @@ 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:
@@ -161,7 +154,6 @@ 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,
@@ -174,14 +166,12 @@ 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:

View File

@@ -44,7 +44,7 @@ class Silence(Option):
class TriggerGenerator:
capture_zone_types = (Fob, Airfield)
capture_zone_types = (Fob,)
capture_zone_flag = 600
def __init__(self, mission: Mission, game: Game) -> None:
@@ -159,9 +159,7 @@ 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, unit_type="GROUND"
)
AllOfCoalitionOutsideZone(defending_coalition, trigger_zone.id)
)
capture_trigger.add_condition(
PartOfCoalitionInZone(
@@ -178,9 +176,7 @@ class TriggerGenerator:
recapture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
recapture_trigger.add_condition(
AllOfCoalitionOutsideZone(
attacking_coalition, trigger_zone.id, unit_type="GROUND"
)
AllOfCoalitionOutsideZone(attacking_coalition, trigger_zone.id)
)
recapture_trigger.add_condition(
PartOfCoalitionInZone(

View File

@@ -162,7 +162,7 @@ class LuaPlugin(PluginSettings):
for option in self.options:
enabled = str(option.enabled).lower()
name = option.identifier
option_decls.append(f" dcsRetribution.plugins.{name} = {enabled}")
option_decls.append(f" dcsLiberation.plugins.{name} = {enabled}")
joined_options = "\n".join(option_decls)
@@ -170,11 +170,11 @@ class LuaPlugin(PluginSettings):
f"""\
-- {self.identifier} plugin configuration.
if dcsRetribution then
if not dcsRetribution.plugins then
dcsRetribution.plugins = {{}}
if dcsLiberation then
if not dcsLiberation.plugins then
dcsLiberation.plugins = {{}}
end
dcsRetribution.plugins.{self.identifier} = {{}}
dcsLiberation.plugins.{self.identifier} = {{}}
{joined_options}
end

View File

@@ -256,52 +256,6 @@ 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),)),
),
),
),
]

View File

@@ -4,7 +4,9 @@ from fastapi import APIRouter, Depends
from shapely.geometry import LineString, Point as ShapelyPoint
from game import Game
from game.ato.flightplans.uizonedisplay import UiZoneDisplay
from game.ato.flightplans.airassault import AirAssaultFlightPlan
from game.ato.flightplans.cas import CasFlightPlan
from game.ato.flightplans.patrolling import PatrollingFlightPlan
from game.server import GameContext
from game.server.flights.models import FlightJs
from game.server.leaflet import LeafletPoly, ShapelyUtil
@@ -38,12 +40,23 @@ def commit_boundary(
flight_id: UUID, game: Game = Depends(GameContext.require)
) -> LeafletPoly:
flight = game.db.flights.get(flight_id)
if not isinstance(flight.flight_plan, UiZoneDisplay):
return []
zone = flight.flight_plan.ui_zone()
if len(zone.points) == 1:
center = ShapelyPoint(zone.points[0].x, zone.points[0].y)
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:
center = LineString([ShapelyPoint(p.x, p.y) for p in zone.points])
bubble = center.buffer(zone.radius.meters)
return []
bubble = commit_center.buffer(flight.flight_plan.engagement_distance.meters)
return ShapelyUtil.poly_to_leaflet(bubble, game.theater)

View File

@@ -14,7 +14,6 @@ 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
@@ -57,19 +56,19 @@ class Settings:
"Player coalition skill",
page=DIFFICULTY_PAGE,
section=AI_DIFFICULTY_SECTION,
default="High",
default="Good",
)
enemy_skill: str = skill_option(
"Enemy coalition skill",
page=DIFFICULTY_PAGE,
section=AI_DIFFICULTY_SECTION,
default="High",
default="Average",
)
enemy_vehicle_skill: str = skill_option(
"Enemy AA and vehicles skill",
page=DIFFICULTY_PAGE,
section=AI_DIFFICULTY_SECTION,
default="High",
default="Average",
)
player_income_multiplier: float = bounded_float_option(
"Player income multiplier",
@@ -106,16 +105,11 @@ class Settings:
section=MISSION_DIFFICULTY_SECTION,
default=True,
)
night_day_missions: NightMissions = choices_option(
"Night/day mission options",
night_disabled: bool = boolean_option(
"No night missions",
page=DIFFICULTY_PAGE,
section=MISSION_DIFFICULTY_SECTION,
choices={
"Generate night and day missions": NightMissions.DayAndNight,
"Only generate day missions": NightMissions.OnlyDay,
"Only generate night missions": NightMissions.OnlyNight,
},
default=NightMissions.DayAndNight,
default=False,
)
# Mission Restrictions
labels: str = choices_option(
@@ -211,10 +205,10 @@ class Settings:
)
#: Feature flag for squadron limits.
enable_squadron_pilot_limits: bool = boolean_option(
"Enable per-squadron pilot limits",
"Enable per-squadron pilot limits (WIP)",
CAMPAIGN_MANAGEMENT_PAGE,
PILOTS_AND_SQUADRONS_SECTION,
default=True,
default=False,
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"
@@ -231,7 +225,7 @@ class Settings:
CAMPAIGN_MANAGEMENT_PAGE,
PILOTS_AND_SQUADRONS_SECTION,
default=12,
min=6,
min=12,
max=72,
detail=(
"Sets the maximum number of pilots a squadron may have active. "
@@ -482,12 +476,6 @@ 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",

View File

@@ -37,7 +37,6 @@ from dcs.ships import (
LHA_Tarawa,
Stennis,
Type_071,
Hms_invincible,
)
from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unitgroup import ShipGroup, StaticGroup
@@ -1204,7 +1203,6 @@ class NavalControlPoint(ControlPoint, ABC):
LHA_Tarawa,
KUZNECOW,
Type_071,
Hms_invincible,
]:
return True
return False

View File

@@ -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(f"{group.name} has no skynet usable units")
raise IadsNetworkException("Group has no skynet usable units")
else:
# Use the GroupName for SAMs, SAMAsEWR and PDs
return group.group_name
@@ -121,10 +121,6 @@ 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
@@ -198,11 +194,8 @@ class IadsNetwork:
# Not participating
return
events.update_iads_node(node)
if self.advanced_iads:
if self.iads_config:
self._add_connections_from_config(node)
else:
self._make_advanced_connections_by_range(node)
if self.advanced_iads and not self.iads_config:
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"""
@@ -224,7 +217,7 @@ class IadsNetwork:
node: Optional[IadsNetworkNode] = None
for group in tgo.groups:
# TODO Cleanup
if isinstance(group, IadsGroundGroup) and group.alive_units > 0:
if isinstance(group, IadsGroundGroup):
# The first IadsGroundGroup is always the primary Group
if not node and group.iads_role.participate:
# Primary Node
@@ -262,17 +255,17 @@ class IadsNetwork:
def initialize_network_from_config(self) -> None:
"""Initialize the IADS Network from a configuration"""
for primary_node in self.iads_config.keys():
for element_name, connections in self.iads_config.items():
warning_msg = (
f"IADS: No ground object found for {primary_node}."
f"IADS: No ground object found for {element_name}."
f" This can be normal behaviour."
)
if primary_node in self.ground_objects:
node = self.node_for_tgo(self.ground_objects[primary_node])
if element_name in self.ground_objects:
node = self.node_for_tgo(self.ground_objects[element_name])
else:
node = None
warning_msg = (
f"IADS: No ground object found for connection {primary_node}"
f"IADS: No ground object found for connection {element_name}"
)
if node is None:
@@ -281,20 +274,16 @@ class IadsNetwork:
# available. Therefore the TGO will not get populated at all
logging.warning(warning_msg)
continue
self._add_connections_from_config(node)
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
# 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 initialize_network_from_range(self) -> None:
"""Initialize the IADS Network by range"""

View File

@@ -45,9 +45,4 @@ 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]

View File

@@ -68,7 +68,6 @@ class ModSettings:
su57_felon: bool = False
frenchpack: bool = False
high_digit_sams: bool = False
ov10a_bronco: bool = False
class GameGenerator:

View File

@@ -57,23 +57,6 @@ 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")
@@ -130,7 +113,6 @@ 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"
@@ -147,32 +129,12 @@ 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,

View File

@@ -161,15 +161,10 @@ 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_date: 2022-08-31 13:30:00` will have the
#: field. For example, `recommended_start_data: 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.
#:
#: 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)
CAMPAIGN_FORMAT_VERSION = (10, 4)

View File

@@ -2,10 +2,8 @@ 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
@@ -13,15 +11,7 @@ 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,
knots,
meters,
)
from game.utils import Distance, Heading, Pressure, inches_hg, interpolate, meters
if TYPE_CHECKING:
from game.settings import Settings
@@ -29,12 +19,6 @@ 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.
@@ -43,9 +27,6 @@ class AtmosphericConditions:
#: Temperature at sea level in Celcius.
temperature_celsius: float
#: Turbulence per 10 cm.
turbulence_per_10cm: float
@dataclass(frozen=True)
class WindConditions:
@@ -118,38 +99,18 @@ 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(
@@ -166,10 +127,6 @@ 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
@@ -190,62 +147,24 @@ 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 = 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
at_2000m_factor = 2
at_8000m_factor = 3
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,
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),
),
at_2000m=Wind(wind_direction_2000m.degrees, base_wind * at_2000m_factor),
at_8000m=Wind(wind_direction_8000m.degrees, base_wind * at_8000m_factor),
)
@staticmethod
def random_cloud_base() -> int:
return random.randint(1000, 5000)
return random.randint(2000, 3000)
@staticmethod
def random_cloud_thickness() -> int:
# values lower than 400m can generate clear skies in some cases
return random.randint(400, 2000)
return random.randint(100, 400)
@staticmethod
def random_pressure(average_pressure: float) -> Pressure:
@@ -278,42 +197,6 @@ 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
@@ -324,10 +207,6 @@ 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
@@ -347,10 +226,6 @@ 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)
@@ -359,7 +234,7 @@ class Cloudy(Weather):
return None
def generate_wind(self) -> WindConditions:
return self.random_wind(1, 5)
return self.random_wind(1, 4)
class Raining(Weather):
@@ -371,10 +246,6 @@ 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)
@@ -383,7 +254,7 @@ class Raining(Weather):
return None
def generate_wind(self) -> WindConditions:
return self.random_wind(2, 6)
return self.random_wind(1, 6)
class Thunderstorm(Weather):
@@ -395,10 +266,6 @@ 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(),
@@ -408,7 +275,7 @@ class Thunderstorm(Weather):
)
def generate_wind(self) -> WindConditions:
return self.random_wind(2, 8)
return self.random_wind(1, 8)
@dataclass
@@ -431,7 +298,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_day_missions
theater, day, time_of_day, settings.night_disabled
)
return cls(
@@ -446,11 +313,11 @@ class Conditions:
theater: ConflictTheater,
day: datetime.date,
time_of_day: TimeOfDay,
night_day_missions: NightMissions,
night_disabled: bool,
) -> datetime.datetime:
from game.theater import DaytimeMap
if night_disabled:
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)),
@@ -458,14 +325,6 @@ 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)

View File

@@ -13,7 +13,6 @@ from .jas39 import *
from .su30 import *
from .su57 import *
from .uh60l import *
from .ov10a import *
def load_mods() -> None:

View File

@@ -88,49 +88,39 @@ class WeaponsA4EC:
"name": "BDU-33 *6 (MER)",
"weight": 167.6,
}
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 = {"clsid": "{CBU-1/A}", "name": "CBU-1/A", "weight": 458.921706}
CBU_1_A__2 = {
"clsid": "{CBU-1/A_TER_2_L}",
"name": "CBU-1/A *2",
"weight": 722.1820224,
"weight": 713.473056,
}
CBU_1_A__2_ = {
"clsid": "{CBU-1/A_TER_2_R}",
"name": "CBU-1/A *2",
"weight": 722.1820224,
"weight": 713.473056,
}
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 = {"clsid": "{CBU-2B/A}", "name": "CBU-2B/A", "weight": 379.543106}
CBU_2B_A__2 = {
"clsid": "{CBU-2B/A_TER_2_L}",
"name": "CBU-2B/A *2",
"weight": 827.324648,
"weight": 806.686212,
}
CBU_2B_A__2_ = {
"clsid": "{CBU-2B/A_TER_2_R}",
"name": "CBU-2B/A *2",
"weight": 827.324648,
"weight": 806.686212,
}
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 = {"clsid": "{CBU-2/A}", "name": "CBU-2/A", "weight": 343.822736}
CBU_2_A__2 = {
"clsid": "{CBU-2/A_TER_2_L}",
"name": "CBU-2/A *2",
"weight": 736.833044,
"weight": 735.245472,
}
CBU_2_A__2_ = {
"clsid": "{CBU-2/A_TER_2_R}",
"name": "CBU-2/A *2",
"weight": 736.833044,
"weight": 735.245472,
}
CBU_DUMMY = {"clsid": "{CBU_DUMMY}", "name": "CBU_DUMMY", "weight": 58.059776}
Fuel_Tank_150_gallons = {
"clsid": "{DFT-150gal}",
"name": "Fuel Tank 150 gallons",

View File

@@ -297,10 +297,6 @@ 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>

View File

@@ -1,174 +0,0 @@
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

View File

@@ -28,7 +28,6 @@ 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 (
@@ -74,7 +73,7 @@ def on_game_load(game: Game | None) -> None:
EventStream.put_nowait(GameUpdateEvents().game_loaded(game))
def run_ui(game: Game | None, ui_flags: UiFlags) -> None:
def run_ui(game: Game | None, dev: bool) -> None:
os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" # Potential fix for 4K screens
QApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
@@ -166,7 +165,7 @@ def run_ui(game: Game | None, ui_flags: UiFlags) -> None:
GameUpdateSignal.get_instance().game_loaded.connect(on_game_load)
# Start window
window = QLiberationWindow(game, ui_flags)
window = QLiberationWindow(game, dev)
window.showMaximized()
splash.finish(window)
qt_execution_code = app.exec_()
@@ -196,19 +195,6 @@ 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.")
@@ -399,7 +385,7 @@ def main():
return
with Server().run_in_thread():
run_ui(game, UiFlags(args.dev, args.show_sim_speed_controls))
run_ui(game, args.dev)
if __name__ == "__main__":

View File

@@ -205,7 +205,6 @@ 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"]

View File

@@ -1,17 +0,0 @@
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

View File

@@ -17,7 +17,6 @@ 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
@@ -31,9 +30,7 @@ from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResul
class QTopPanel(QFrame):
def __init__(
self, game_model: GameModel, sim_controller: SimController, ui_flags: UiFlags
) -> None:
def __init__(self, game_model: GameModel, sim_controller: SimController) -> None:
super(QTopPanel, self).__init__()
self.game_model = game_model
self.sim_controller = sim_controller
@@ -85,8 +82,7 @@ class QTopPanel(QFrame):
self.proceedBox = QGroupBox("Proceed")
self.proceedBoxLayout = QHBoxLayout()
if ui_flags.show_sim_speed_controls:
self.proceedBoxLayout.addLayout(self.simSpeedControls)
self.proceedBoxLayout.addLayout(self.simSpeedControls)
self.proceedBoxLayout.addLayout(MaxPlayerCount(self.game_model.ato_model))
self.proceedBoxLayout.addWidget(self.passTurnButton)
self.proceedBoxLayout.addWidget(self.proceedButton)

View File

@@ -7,6 +7,7 @@ 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
@@ -68,7 +69,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, self.game.settings
front_line, self.game.theater
)[0]
wptname = f"Frontline {front_line.name} [CAS]"
wpt = FlightWaypoint(
@@ -80,25 +81,66 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
wpt.description = "Frontline"
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_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)
if self.include_airbases:
for cp in self.game.theater.controlpoints:

View File

@@ -0,0 +1,71 @@
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)

View File

@@ -0,0 +1,69 @@
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)

View File

@@ -161,14 +161,6 @@ 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:

View File

@@ -8,9 +8,7 @@ from PySide2.QtWidgets import (
QGroupBox,
QLabel,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
from game.debriefing import Debriefing
@@ -54,19 +52,6 @@ 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__()
@@ -90,10 +75,12 @@ class QDebriefingWindow(QDialog):
title = QLabel("<b>Casualty report</b>")
layout.addWidget(title)
player_lost_units = ScrollingCasualtyReportContainer(debriefing, player=True)
player_lost_units = QGroupBox(f"{self.debriefing.player_country}'s lost units:")
player_lost_units.setLayout(LossGrid(debriefing, player=True))
layout.addWidget(player_lost_units)
enemy_lost_units = ScrollingCasualtyReportContainer(debriefing, player=False)
enemy_lost_units = QGroupBox(f"{self.debriefing.enemy_country}'s lost units:")
enemy_lost_units.setLayout(LossGrid(debriefing, player=False))
layout.addWidget(enemy_lost_units)
okay = QPushButton("Okay")

View File

@@ -19,7 +19,7 @@ from PySide2.QtWidgets import (
)
import qt_ui.uiconstants as CONST
from game import Game, VERSION, persistency, Migrator
from game import Game, VERSION, persistency
from game.debriefing import Debriefing
from game.game import TurnState
from game.layout import LAYOUTS
@@ -31,7 +31,6 @@ 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
@@ -56,7 +55,7 @@ class QLiberationWindow(QMainWindow):
tgo_info_signal = Signal(TheaterGroundObject)
control_point_info_signal = Signal(ControlPoint)
def __init__(self, game: Game | None, ui_flags: UiFlags) -> None:
def __init__(self, game: Game | None, dev: bool) -> None:
super().__init__()
self._uncaught_exception_handler = UncaughtExceptionHandler(self)
@@ -81,16 +80,14 @@ 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, ui_flags.dev_ui_webserver, self
)
self.liberation_map = QLiberationMap(self.game_model, dev, self)
self.setGeometry(300, 100, 270, 100)
self.updateWindowTitle()
self.setWindowIcon(QIcon("./resources/icon.png"))
self.statusBar().showMessage("Ready")
self.initUi(ui_flags)
self.initUi()
self.initActions()
self.initToolbar()
self.initMenuBar()
@@ -111,7 +108,6 @@ 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:
@@ -121,7 +117,7 @@ class QLiberationWindow(QMainWindow):
else:
self.onGameGenerated(self.game)
def initUi(self, ui_flags: UiFlags) -> None:
def initUi(self):
hbox = QSplitter(Qt.Horizontal)
vbox = QSplitter(Qt.Vertical)
hbox.addWidget(self.ato_panel)
@@ -134,7 +130,7 @@ class QLiberationWindow(QMainWindow):
hbox.setSizes([1, 10000000])
vbox.setSizes([600, 100])
self.top_panel = QTopPanel(self.game_model, self.sim_controller, ui_flags)
self.top_panel = QTopPanel(self.game_model, self.sim_controller)
vbox = QVBoxLayout()
vbox.setMargin(0)
vbox.addWidget(self.top_panel)
@@ -321,7 +317,6 @@ 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])
@@ -487,10 +482,6 @@ 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'>"
@@ -555,13 +546,10 @@ class QLiberationWindow(QMainWindow):
result = QMessageBox.question(
self,
"Quit Retribution?",
"Would you like to save before quitting?",
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,
QMessageBox.Cancel,
"Are you sure you want to quit? All unsaved progress will be lost.",
QMessageBox.Yes | QMessageBox.No,
)
if result in [QMessageBox.Yes, QMessageBox.No]:
if result == QMessageBox.Yes:
self.saveGame()
if result == QMessageBox.Yes:
self._save_window_geometry()
super().closeEvent(event)
self.dialog = None

View File

@@ -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))
)

View File

@@ -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
from game.ato.flightplans.custom import CustomFlightPlan, CustomLayout
from game.ato.flightplans.formationattack import FormationAttackFlightPlan
from game.ato.flightplans.planningerror import PlanningError
from game.ato.flightplans.waypointbuilder import WaypointBuilder
@@ -147,7 +147,15 @@ class QFlightWaypointTab(QFrame):
def degrade_to_custom_flight_plan(self) -> None:
if not isinstance(self.flight.flight_plan, CustomFlightPlan):
self.flight.degrade_to_custom_flight_plan()
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:],
),
)
def confirm_recreate(self, task: FlightType) -> None:
result = QMessageBox.question(

View File

@@ -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, QTextBrowser
from PySide2.QtWidgets import QCheckBox, QLabel, QTextEdit, QVBoxLayout
from jinja2 import Environment, FileSystemLoader, select_autoescape
from game.campaignloader.campaign import Campaign, DEFAULT_BUDGET
@@ -142,7 +142,6 @@ 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,
@@ -174,7 +173,6 @@ 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"),
)
@@ -260,13 +258,11 @@ class FactionSelection(QtWidgets.QWizardPage):
redFaction.setBuddy(self.redFactionSelect)
# Faction description
self.blueFactionDescription = QTextBrowser()
self.blueFactionDescription = QTextEdit("")
self.blueFactionDescription.setReadOnly(True)
self.blueFactionDescription.setOpenExternalLinks(True)
self.redFactionDescription = QTextBrowser()
self.redFactionDescription = QTextEdit("")
self.redFactionDescription.setReadOnly(True)
self.redFactionDescription.setOpenExternalLinks(True)
# Setup default selected factions
for i, r in enumerate(FACTIONS):
@@ -388,9 +384,8 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
self.registerField("selectedCampaign", self.campaignList)
# Faction description
self.campaignMapDescription = QTextBrowser()
self.campaignMapDescription = QTextEdit("")
self.campaignMapDescription.setReadOnly(True)
self.campaignMapDescription.setOpenExternalLinks(True)
self.campaignMapDescription.setMaximumHeight(200)
self.performanceText = QTextEdit("")
@@ -692,8 +687,6 @@ 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()
@@ -706,41 +699,103 @@ 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-16I Sufa & F-16D (version 2.2 by IDF Mods Project)"),
modLayout_row,
0,
)
modLayout.addWidget(f_16_idf, 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-22A Raptor"), modLayout_row, 0)
modLayout.addWidget(f22_raptor, modLayout_row, 1)
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
# Section break here for readability
modLayout.addWidget(QtWidgets.QWidget(), modLayout_row, 0)
modLayout_row += 1
modLayout.addWidget(
QtWidgets.QLabel("C-130J-30 Super Hercules"), modLayout_row, 0
)
modLayout.addWidget(hercules, modLayout_row, 1)
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
# Section break here for readability
modLayout.addWidget(QtWidgets.QWidget(), modLayout_row, 0)
modLayout_row += 14
modLayout.addWidget(QtWidgets.QLabel("Frenchpack"), modLayout_row, 0)
modLayout.addWidget(frenchpack, modLayout_row, 1)
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
# 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)
modSettingsGroup.setLayout(modLayout)
mlayout = QVBoxLayout()

View File

@@ -4,36 +4,37 @@ asgiref==3.5.2
atomicwrites==1.4.1
attrs==22.1.0
black==22.10.0
certifi==2022.12.7
certifi==2022.9.24
cfgv==3.3.1
click==8.1.3
colorama==0.4.6
distlib==0.3.6
Faker==15.3.3
fastapi==0.87.0
Faker==15.1.1
fastapi==0.85.1
filelock==3.8.0
future==0.18.2
h11==0.14.0
httptools==0.5.0
identify==2.5.9
identify==2.5.7
idna==3.4
iniconfig==1.1.1
Jinja2==3.1.2
MarkupSafe==2.1.1
mypy==0.991
mypy==0.982
mypy-extensions==0.4.3
nodeenv==1.7.0
packaging==21.3
pathspec==0.10.2
pathspec==0.10.1
pefile==2022.5.30
Pillow==9.3.0
platformdirs==2.5.4
Pillow==9.2.0
platformdirs==2.5.2
pluggy==1.0.0
pre-commit==2.20.0
py==1.11.0
pydantic==1.10.2
-e git+https://github.com/dcs-retribution/pydcs@f0bb4da4d458d714dedd1f3bba30deae5f25a80d#egg=pydcs
pyinstaller==5.6.2
pyinstaller-hooks-contrib==2022.13
-e git+https://github.com/pydcs/dcs@16ea5d01c57a395ea5868630615c8a42dd2ea1a9#egg=pydcs
pyinstaller==5.6.1
pyinstaller-hooks-contrib==2022.10
pyparsing==3.0.9
pyproj==3.4.0
pyshp==2.3.1
@@ -47,18 +48,18 @@ Shapely==1.8.5.post1
shiboken2==5.15.2.1
six==1.16.0
sniffio==1.3.0
starlette==0.21.0
starlette==0.20.4
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.3.0.2
types-PyYAML==6.0.12.2
types-Pillow==9.2.2.2
types-PyYAML==6.0.12
types-tabulate==0.9.0.0
typing_extensions==4.4.0
uvicorn==0.20.0
virtualenv==20.16.7
uvicorn==0.19.0
virtualenv==20.16.5
watchgod==0.8.2
websockets==10.4
websockets==10.3

View File

@@ -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,20 +62,17 @@ 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 player_flights %}
{% for flight in flights if flight.client_units %}
--------------------------------------------------
{{ 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:
@@ -86,7 +83,6 @@ 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 %}
@@ -102,22 +98,12 @@ 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 %}
@@ -125,5 +111,4 @@ JTACS [F-10 Menu] :
====================
{% for jtac in jtacs %}Frontline {{ jtac.region }} -- Code : {{ jtac.code }}, Freq : {{ jtac.freq.mhz }}
{% endfor %}
{% endif %}

View File

@@ -1,20 +1,13 @@
---
name: Syria - WRL - Assault on Damascus (v1)
name: Syria - Assault on Damascus
theater: Syria
authors: HolyOrangeJuice
recommended_player_faction: WRL - Task Force Blue
recommended_enemy_faction: WRL - Task Force Red
recommended_start_date: 2022-06-04
description: "<p>Edited by HolyOrangeJuice. Original Golan Heights by Khopa. Modified for We Run Liberation group.
<a style=\"color: white\" href=\"https://werunliberation.com\">werunliberation.com</a></p>"
description: <p>Edited by HolyOrangeJuice. Original Golan Heights by Khopa. Modified for We Run Liberation group.</p>
miz: AssaultonDamascus.miz
performance: 2
version: "10.5"
max_frontline_length: 30
recommended_player_money: 2000
recommended_enemy_money: 2000
recommended_player_income_multiplier: 1.0
recommended_enemy_income_multiplier: 1.0
performance: 1
version: "10.2"
advanced_iads: false
squadrons:
CVN-74 John Stennis:
@@ -54,13 +47,7 @@ squadrons:
aircraft:
- SH-60B Seahawk
# AIR SPAWN BLUE
BLUEAIR:
- primary: AEW&C
aircraft:
- E-3A
- primary: Refueling
aircraft:
- KC-135 Stratotanker MPRS
BLUEAIR:
- primary: Strike
secondary: air-to-ground
aircraft:
@@ -77,15 +64,15 @@ squadrons:
- primary: Strike
secondary: any
aircraft:
- F-117A Nighthawk
- F-117A Nighthawk
- primary: Strike
secondary: any
aircraft:
- Tornado GR4
- primary: BARCAP
secondary: air-to-air
- Tornado GR4
- primary: Strike
secondary: any
aircraft:
- F-15C Eagle
- Tornado IDS
# Ramat-David
30:
- primary: BARCAP
@@ -124,7 +111,7 @@ squadrons:
secondary: air-to-ground
aircraft:
- VMFA-251
#Hornet - Marines
#Hornet - Marines
- primary: CAS
secondary: air-to-ground
aircraft:
@@ -133,6 +120,36 @@ squadrons:
secondary: air-to-ground
aircraft:
- A-10A Thunderbolt II
- primary: AEW&C
aircraft:
- E-3A
- primary: Refueling
aircraft:
- KC-135 Stratotanker MPRS
- primary: SEAD
secondary: air-to-ground
aircraft:
- Su-25T Frogfoot
- primary: CAS
secondary: air-to-ground
aircraft:
- Su-25 Frogfoot
- primary: BARCAP
secondary: air-to-air
aircraft:
- MiG-29S Fulcrum-C
- primary: BARCAP
secondary: air-to-air
aircraft:
- Su-33 Flanker-D
- primary: BARCAP
secondary: air-to-air
aircraft:
- Su-27 Flanker-B
- primary: BARCAP
secondary: air-to-air
aircraft:
- J-11A Flanker-L
# Golan South
Golan South:
- primary: BAI
@@ -148,8 +165,7 @@ squadrons:
- primary: BAI
secondary: air-to-ground
aircraft:
- A Company, 1-211th ARB
#AH-64D Apache Longbow
- AH-64D Apache Longbow
- primary: CAS
secondary: air-to-ground
aircraft:
@@ -157,24 +173,31 @@ squadrons:
- primary: CAS
secondary: air-to-ground
aircraft:
- Ka-50 Hokum III
- Ka-50 Hokum
- primary: CAS
secondary: air-to-ground
aircraft:
- SA 342L Gazelle
- primary: CAS
secondary: air-to-ground
aircraft:
- SA 342M Gazelle
- primary: CAS
secondary: air-to-ground
aircraft:
- SA 342M Gazelle Mistral
- primary: Transport
secondary: any
aircraft:
- HMLA-169 (UH-1H)
#UH-1H Iroquois
- UH-1H Iroquois
- primary: Transport
secondary: any
aircraft:
- Mi-8MTV2 Hip
- Mi-8MTV2 Hip
# Marj Ruhayyil
23:
- primary: BARCAP
secondary: air-to-air
secondary: any
aircraft:
- MiG-21bis Fishbed-N
# Al-Dumayr
@@ -182,19 +205,22 @@ squadrons:
- primary: BARCAP
secondary: any
aircraft:
- Su-27 Flanker-B
- MiG-29S Fulcrum-C
# Khalkhalah
18:
- primary: BARCAP
secondary: any
aircraft:
- MiG-23MLD Flogger-K
- MiG-21bis Fishbed-N
# Damascus
7:
- primary: AEW&C
aircraft:
- A-50
- primary: BARCAP
secondary: any
aircraft:
- MiG-29S Fulcrum-C
- Su-30 Flanker-C
- primary: Strike
secondary: air-to-ground
aircraft:
@@ -203,3 +229,23 @@ squadrons:
secondary: air-to-ground
aircraft:
- Su-25T Frogfoot
- primary: CAS
secondary: air-to-ground
aircraft:
- Su-25 Frogfoot
- primary: Strike
secondary: air-to-ground
aircraft:
- Tu-142 Bear-F
- primary: Strike
secondary: air-to-ground
aircraft:
- Tu-160 Blackjack
- primary: Strike
secondary: air-to-ground
aircraft:
- Tu-22M3 Backfire-C
- primary: Strike
secondary: air-to-ground
aircraft:
- Tu-95MS Bear-H

Some files were not shown because too many files have changed in this diff Show More